diff --git a/.gitattributes b/.gitattributes index 0e4a34658..b4e784d8f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,2 @@ -# Vendored files preserve upstream license spacing and unified-diff context lines. tools/comet/LICENSE whitespace=-blank-at-eof tools/comet/droiddeck.patch whitespace=-blank-at-eol,-blank-at-eof diff --git a/.github/workflows/build-aaudio-sink.yml b/.github/workflows/build-aaudio-sink.yml index 63133e545..b6ecd5e04 100644 --- a/.github/workflows/build-aaudio-sink.yml +++ b/.github/workflows/build-aaudio-sink.yml @@ -1,7 +1,5 @@ name: Build AAudio sink module -# A quick check of tools/aaudio-sink on its own; the APK build compiles the same module into the -# audio bundle. Runs on pushes that touch the module. on: push: paths: diff --git a/.github/workflows/build-desktop-kde.yml b/.github/workflows/build-desktop-kde.yml index 9390e1af8..5a9933abc 100644 --- a/.github/workflows/build-desktop-kde.yml +++ b/.github/workflows/build-desktop-kde.yml @@ -1,13 +1,5 @@ name: Build desktop package (KDE Plasma) -# The desktop the app installs into its Linux runtime on first use: KDE Plasma as SteamOS's desktop -# mode has it, the closure of tools/desktop-kde/seeds.txt over Arch Linux ARM minus what the runtime -# already carries, plus tools/desktop-kde/overlay (tools/desktop-kde/build.sh), every package -# signature-checked. Published as a release in Droid-Deck/DroidDeck-Components with DroidDeck's -# other components; the apk pins it in tools/desktop-kde/release.env (tag, sha256, size). A -# published tag is never rebuilt: the packages are Arch Linux ARM's current ones, so a rebuild -# would not match the pinned sha256. A changed package gets a new tag (desktop-kde-r2, ...) and a -# release.env update. on: push: paths: @@ -72,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)" - # The package itself too, briefly: enough to try a build on a device before publishing it. - uses: actions/upload-artifact@v4 with: name: desktop-kde diff --git a/.github/workflows/build-gamescope.yml b/.github/workflows/build-gamescope.yml index 649c01e9f..0a91fea43 100644 --- a/.github/workflows/build-gamescope.yml +++ b/.github/workflows/build-gamescope.yml @@ -1,9 +1,5 @@ name: Build gamescope (patched) -# The runtime's own gamescope (Arch Linux ARM's 3.16.29 package) rebuilt with the patches under -# tools/gamescope/patches, in an Arch Linux ARM container on an arm64 runner, then published as a -# release asset the apk build fetches (tools/gamescope/release.env names the tag and sha). The -# hosted runtime image is never changed: the apk stages the binary over /usr/local/bin. on: push: paths: @@ -43,6 +39,4 @@ jobs: run: | set -euo pipefail gh release create "${{ inputs.tag }}" --latest=false --prerelease=false --title "${{ inputs.tag }}" --notes "gamescope 3.16.29 with tools/gamescope/patches; staged by the apk over /usr/local/bin/gamescope. sha256 $(cut -d' ' -f1 gamescope.tzst.sha256)" gamescope.tzst gamescope.tzst.sha256 || gh release upload "${{ inputs.tag }}" gamescope.tzst gamescope.tzst.sha256 --clobber - # A component the apk fetches by tag, never something users install: it must not - # become the repo's Latest release or appear as a pre-release, also when re-uploaded. gh release edit "${{ inputs.tag }}" --latest=false --prerelease=false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b854e59d..7bd2d1d08 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,8 +22,6 @@ permissions: pull-requests: write concurrency: - # Replace older builds only for the same PR. Each push/dispatch gets its own group so one main - # build cannot cancel another before it signs and publishes. group: build-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} @@ -211,14 +209,12 @@ jobs: aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -nostdlib -ffreestanding -fno-stack-protector \ -fno-tree-loop-distribute-patterns -Wl,-z,defs -o "$D/libblaudit.so" tools/proot/fastpath/audit.c aarch64-linux-gnu-strip --strip-unneeded "$D/libblaudit.so" - # Force-SSBS adapter, preloaded into a game's Wine only when the game asks for it. aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \ -o "$D/libssbs.so" tools/linuxfs/ssbs/ssbs_adapter.c -ldl aarch64-linux-gnu-strip --strip-unneeded "$D/libssbs.so" mkdir -p "$D/usr/local/bin" aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$D/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-clipboard" - # Cardboard mode's SteamVR driver and Vulkan layer (tools/vr), Vulkan headers from the runner's NDK. VK_INCLUDE=$(ls -d "${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"/toolchains/llvm/prebuilt/*/sysroot/usr/include | head -1) \ tools/vr/build.sh --native "$D/usr/local/lib/droiddeck-vr" for script in tools/linuxfs/overlay/usr/local/bin/droiddeck-* tools/linuxfs/overlay/usr/local/bin/steam-compatibility; do @@ -254,14 +250,12 @@ jobs: for so in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1; do cp -L "mango-pkgs/usr/lib/$so" "$M/$so" done - # Ours, not the package's: GPU memory without tracefs (tools/mangoapp/libtracefs-shim.c). aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wl,-soname,libtracefs.so.1 \ -o "$M/libtracefs.so.1" tools/mangoapp/libtracefs-shim.c aarch64-linux-gnu-strip --strip-unneeded "$M/libtracefs.so.1" install -Dm644 tools/mangoapp/mangoapp "$D/usr/local/bin/mangoapp" rm -rf mango-pkgs ls -l "$M" - # The Windows components installer's tools (tools/msitools/packages.txt). T="$D/usr/local/lib/droiddeck-msitools" mkdir -p "$T" msi-pkgs grep -v '^#' tools/msitools/packages.txt | while read -r sum url; do @@ -273,7 +267,6 @@ jobs: done install -m644 msi-pkgs/usr/bin/msiinfo "$T/msiinfo" install -m644 msi-pkgs/usr/bin/cabextract "$T/cabextract" - # 7-Zip (the full build with its format plugin) opens installer .exe wrappers. install -m644 msi-pkgs/usr/lib/7zip/7z "$T/7z" install -m644 msi-pkgs/usr/lib/7zip/7z.so "$T/7z.so" for so in libmsi-1.0.so.0 libgsf-1.so.114 libgcab-1.0.so.0; do @@ -291,10 +284,6 @@ jobs: install -Dm644 tools/linuxfs/desktop/firefox-droiddeck.js "$D/usr/lib/firefox/defaults/pref/droiddeck.js" ls -lR "$D" - # The Debian 11 image the preloads are compiled in comes from a release on this repo - # (published once by ci-debian-image.yml, pinned in tools/linuxfs/debian-image.env): the - # public registry mirrors rate-limit the shared runner addresses and failed builds on a - # busy evening. Only if that download fails is the image pulled, with retries. - name: Fetch the Debian image the preloads build in if: steps.preloads.outputs.cache-hit != 'true' env: @@ -370,9 +359,6 @@ jobs: set -euo pipefail curl -fsSL -o pa.tar.gz https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz mkdir -p pa13 && tar -xzf pa.tar.gz -C pa13 --strip-components=1 && rm pa.tar.gz - # DirectAudio - the game driver (one set per Wine audio interface), the Android-side helper and - # the Steam client's sinks - comes from a pinned release of The412Banner/directaudio, checksummed, - # exactly like gamescope; nothing compiled for audio is kept in this repo. - name: Fetch DirectAudio (tools/directaudio/release.env) run: tools/directaudio/fetch.sh "$PWD" "$PWD/sink-out" - name: Build the AAudio sink into the audio bundle @@ -410,12 +396,6 @@ jobs: set -euo pipefail bash tools/linuxfs/check-preloads.sh app/src/main/assets/linuxfs - # The store download engines (app/src/main/rust/stores) are Rust, built by Gradle's - # buildRustStores task through cargo-ndk. rustup and cargo are on the runner image; the Android - # target and cargo-ndk are added here. Cache = cargo registry + cargo-ndk + the crate's target - # tree, keyed on the rustc version and Cargo.lock, so a source-only change rebuilds this crate - # on top of the cached dependency objects, and a lockfile change still starts from the - # previous objects (restore-keys). - name: Rust toolchain for the store engines (aarch64-linux-android) id: rust run: | @@ -474,9 +454,6 @@ jobs: python3 tools/release/runtime_inputs.py apk app/build/outputs/apk/release/app-release.apk - name: Check the store engines' library - # The exports Kotlin binds must be in the apk's libdroiddeckstores.so, or the stores fail on - # the device with an UnsatisfiedLinkError instead of here. The lockfile check is the - # reproducibility guard: cargo must not have had to re-resolve anything. run: | set -euo pipefail NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}" @@ -487,11 +464,6 @@ jobs: || { echo "::error::Cargo.lock changed during the build; commit the regenerated lockfile (uploaded as stores-cargo-lock)"; exit 1; } - name: Store engines host unit tests - # The crate's #[test] functions are pure host-side logic (manifest parsing, chunk plans, - # assembly from a cache, hashes), so a plain `cargo test` on the runner's own architecture - # runs them; the Android build above compiles none of them. Informational on this first - # run - the point is to learn whether they pass - and bounded so a hang can never hold - # the apk. Make it blocking once it is known green. timeout-minutes: 10 continue-on-error: true run: | @@ -626,13 +598,6 @@ jobs: variants: ${{ needs.build.outputs.variants }} secrets: inherit - # Each signed main build as a prerelease of the public Droid-Deck/DroidDeck-CI (main-, the - # last KEEP kept), where anyone can download it without a GitHub account, then a post in the - # Discord's #preview-builds channel linking to it. The release is made with a token of the org's - # "DroidDeck CI Publisher" app (installed on DroidDeck-CI only, Contents: write): its ID in the - # DROIDDECK_CI_APP_ID variable, its private key in the DROIDDECK_CI_APP_KEY secret. - # DISCORD_BUILDS_WEBHOOK is the channel's webhook URL. Keep this secret name when renaming - # the channel: Discord routes the existing webhook by channel ID, not its display name. announce: needs: [build, sign] if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' @@ -659,8 +624,6 @@ jobs: owner: Droid-Deck repositories: DroidDeck-CI - # What this build is, for the top of the release notes: the merged PR's title and the first - # paragraph of its description, or the commit's subject for a push made without a PR. - name: Describe the change env: GH_TOKEN: ${{ github.token }} @@ -710,15 +673,12 @@ jobs: gh release delete "$tag" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null || true gh release create "$tag" -R "$CI_REPO" --target main --prerelease \ --title "main ${GITHUB_SHA::7}" --notes "$notes" signed/* - # Keep the newest main builds by publication time. GitHub App releases can share createdAt. gh release list -R "$CI_REPO" -L 200 --json tagName,publishedAt \ --jq '[.[] | select(.tagName | startswith("main-"))] | sort_by(.publishedAt) | reverse | .['"$KEEP"':][] | .tagName' | while IFS= read -r old; do - # Never remove this run's release, even if the API returns an unexpected ordering. if [ "$old" = "$tag" ]; then continue fi - # Another main publish may have removed the same old release concurrently. if ! gh release delete "$old" -R "$CI_REPO" --yes --cleanup-tag 2>/dev/null; then if gh release view "$old" -R "$CI_REPO" >/dev/null 2>&1; then echo "::error::Could not delete old release $old" @@ -748,8 +708,6 @@ jobs: fi commit=$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA") subject=$(jq -r '.commit.message | split("\n")[0]' <<<"$commit") - # A merge commit's subject is "Merge pull request #N from ..."; its body's first line is - # the PR's title, which says more. title=$(jq -r '.commit.message | split("\n") | map(select(length > 0)) | .[1] // ""' <<<"$commit") pr=$(sed -n 's/^Merge pull request #\([0-9]*\).*/\1/p' <<<"$subject") if [ -n "$pr" ]; then diff --git a/.github/workflows/ci-debian-image.yml b/.github/workflows/ci-debian-image.yml index 1635ed407..a43e6ad7b 100644 --- a/.github/workflows/ci-debian-image.yml +++ b/.github/workflows/ci-debian-image.yml @@ -1,8 +1,5 @@ name: Publish the CI Debian image -# Pulls the Debian 11 image the x86 preloads build in, once, and publishes it as a release asset -# on this repo so build.yml never depends on a registry mirror's rate limit. Run it again (with a -# new tag) only to refresh the image; tools/linuxfs/debian-image.env names the one builds use. on: workflow_dispatch: @@ -32,7 +29,6 @@ jobs: run: | set -euo pipefail image=public.ecr.aws/docker/library/debian:bullseye - # A published image is pinned by checksum in the build; never replace one by accident. if [ "$REPLACE" != true ] && gh release view "$TAG" -R "$GITHUB_REPOSITORY" --json assets -q '.assets[].name' 2>/dev/null | grep -qx debian-bullseye.tar.zst; then echo "::error::$TAG already carries debian-bullseye.tar.zst; publish to a new tag, or set replace and re-pin tools/linuxfs/debian-image.env" exit 1 diff --git a/.github/workflows/pr-release-cleanup.yml b/.github/workflows/pr-release-cleanup.yml index af4581f45..edb76a778 100644 --- a/.github/workflows/pr-release-cleanup.yml +++ b/.github/workflows/pr-release-cleanup.yml @@ -1,9 +1,9 @@ name: PR Release Cleanup -run-name: remove PR #${{ github.event.pull_request.number }}'s test build +run-name: remove PR + + + -# A merged or closed PR's test build leaves DroidDeck-CI, so the app's Updates page stops offering it -# (a merged fix is in the next Preview). pull_request_target for the publisher app's secrets; nothing -# from the PR is checked out or run. on: pull_request_target: types: [ closed ] @@ -32,7 +32,6 @@ jobs: set -euo pipefail state=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .state) echo "closed=$([ "$state" = closed ] && echo true || echo false)" >> "$GITHUB_OUTPUT" - # A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses. - uses: actions/create-github-app-token@v2 if: ${{ steps.state.outputs.closed == 'true' }} id: ci-token diff --git a/.github/workflows/pr-release-publish.yml b/.github/workflows/pr-release-publish.yml index 6c5070b27..b55d2a214 100644 --- a/.github/workflows/pr-release-publish.yml +++ b/.github/workflows/pr-release-publish.yml @@ -11,8 +11,6 @@ on: pull_request_target: types: [labeled] branches: [main] - # A PR from a branch of this repo is signed on its own after each green build: only people who - # can push here can change it. A fork's PR waits for a maintainer's publish-test label. workflow_run: workflows: [Build APK] types: [completed] @@ -53,7 +51,6 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false - # Every variant a main build ships, so a PR build can be tried under any of them. - name: The packages a signed build ships as id: variants run: echo "list=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT" @@ -68,8 +65,6 @@ jobs: run: | set -euo pipefail [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Invalid PR number '$PR_NUMBER'"; exit 1; } - # The automatic path stands down quietly, never failing: a draft, a PR labelled - # no-test-build, or a build that a later push has already overtaken. skip() { echo "::notice::Not publishing PR #$PR_NUMBER: $1"; echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; } if [ "$EVENT_NAME" = workflow_run ]; then echo "automatic=true" >> "$GITHUB_OUTPUT" @@ -94,9 +89,6 @@ jobs: head=$(jq -r '.head.sha' <<<"$pr") base=$(jq -r '.base.sha' <<<"$pr") - # The label is often added while the PR's build is still running, or just after a push - # before it has started: wait for the latest build of the head commit to finish (a PR - # build takes about 4 minutes) rather than fail and need the label added again. run_id= for attempt in $(seq 1 60); do latest=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml \ @@ -116,8 +108,6 @@ jobs: sleep 30 done [[ -n "$run_id" ]] || { echo '::error::The PR build did not finish within 30 minutes'; exit 1; } - # GitHub leaves a run's pull_requests empty when its head is in a fork, so the build is - # tied to this PR by its head repository, branch and commit instead. run=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id") jq -e --arg head "$head" --arg repo "$(jq -r .head.repo.full_name <<<"$pr")" --arg ref "$(jq -r .head.ref <<<"$pr")" ' .event == "pull_request" and .head_sha == $head and .conclusion == "success" and @@ -127,8 +117,6 @@ jobs: sdk=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}} tools=$(find "$sdk/build-tools" -mindepth 1 -maxdepth 1 -type d | sort -V | tail -1) - # A PR build is made from the PR merged onto main, so it must install over main's own - # build. main is what this job checked out; no release has to exist or be marked latest. main_code=$(sed -n 's/^[[:space:]]*versionCode[[:space:]][[:space:]]*\([0-9][0-9]*\)[[:space:]]*$/\1/p' app/build.gradle) [[ "$main_code" =~ ^[0-9]+$ ]] || { echo "::error::No numeric versionCode in main's app/build.gradle"; exit 1; } mkdir -p "$RUNNER_TEMP/pr" @@ -188,8 +176,6 @@ jobs: pattern: droiddeck-pr-signed-* merge-multiple: true path: signed - # What this build is, for the top of the release notes: the PR's title and the first - # paragraph of its description. - name: Describe the change env: GH_TOKEN: ${{ github.token }} @@ -208,7 +194,6 @@ jobs: fi } > notes-head.md cat notes-head.md - # A token of the org's "DroidDeck CI Publisher" app, as build.yml's announce job uses. - uses: actions/create-github-app-token@v2 id: ci-token with: @@ -250,7 +235,6 @@ jobs: echo "first=$first" >> "$GITHUB_OUTPUT" gh release upload "$tag" signed/* -R "$repo" --clobber gh release edit "$tag" -R "$repo" --title "PR #$PR_NUMBER" --notes "$notes" --prerelease - # Drop the previous build's files: anything this build did not just upload. keep=$(cd signed && ls | jq -R . | jq -sc .) gh api "repos/$repo/releases/tags/$tag" | jq -r --argjson keep "$keep" '.assets[] | select(.name as $n | $keep | index($n) | not) | .id' | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f443c710..b8fe92060 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,6 @@ jobs: artifact: droiddeck-release secrets: inherit - # The release is made here, from the apks this run signed, so it traces back to its Build APK run. publish: name: publish the release needs: [find, sign] @@ -94,7 +93,6 @@ jobs: if gh release view "$VERSION" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then echo "::error::release $VERSION already exists"; exit 1 fi - # The notes' first line is the title; media paths become raw URLs at this release's tag. title=$(sed -n '1s/^# *//p' "$notes") [ -n "$title" ] || title="DroidDeck $VERSION" raw="https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$VERSION/docs/releases/media/" @@ -110,7 +108,6 @@ jobs: --target "$GITHUB_SHA" --title "$title" --notes-file "$RUNNER_TEMP/body.md" --latest echo "Published [$title]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$VERSION)" >> "$GITHUB_STEP_SUMMARY" - # A release made with this run's token fires no release event, so the in-app catalog is refreshed here. catalog: needs: publish uses: ./.github/workflows/update-catalog.yml diff --git a/.github/workflows/update-catalog.yml b/.github/workflows/update-catalog.yml index d3a4075b5..93a74d5f9 100644 --- a/.github/workflows/update-catalog.yml +++ b/.github/workflows/update-catalog.yml @@ -20,7 +20,6 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - # Always use the current trusted generator, even when a release event points at an older tag. - uses: actions/checkout@v4 with: ref: main diff --git a/.gitignore b/.gitignore index 4293e3af4..e5bdbfffc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,24 +8,17 @@ captures/ .externalNativeBuild/ .cxx/ .kotlin/ -# Built in CI from tools/linuxfs and app/src/main/cpp/fakeinput.cpp, never committed, so the two -# copies cannot drift. app/src/main/assets/linuxfs/ app/src/main/jniLibs/arm64-v8a/libproot.so app/src/main/jniLibs/arm64-v8a/.proot-inputs app/src/main/jniLibs/arm64-v8a/.proot-outputs app/src/main/jniLibs/arm64-v8a/libproot-loader.so app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so -# The store download engines (app/src/main/rust/stores) are cross-compiled by Gradle into -# app/build/rustJniLibs; cargo's own build tree stays next to the crate. Neither is source. app/src/main/rust/stores/target/ app/src/main/assets/droiddeck-esync/ -# Kotlin compiler session files (build-tool state, never source). .kotlin/ -# Signing keys never enter the repo (DroidDeck's lives in GitHub environment secrets; see -# tools/release/make-release-key.sh). keystore/testkey.p12 is the public AOSP testkey. *.jks *.keystore *.p12 @@ -37,7 +30,6 @@ droiddeck-release-key/ password.txt __pycache__/ -# Local signing with the release key (tools/build_local.sh); holds a password. .signing.env app/src/main/assets/directaudio/ /tools/vr/out/ diff --git a/app/build.gradle b/app/build.gradle index f55175d3b..5b29d72f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,19 +49,12 @@ if (!(treeState in ['clean', 'dirty'])) { buildLabel = "${buildOrigin} ${treeState} · ${buildLabel}" def escapedBuildLabel = buildLabel.replace('\\', '\\\\').replace('"', '\\"') -// What the in-app updater compares against the published builds: the commit this build is of (a PR -// build's head, not the merge CI makes of it), its PR, and whether CI made it at all. def sourceCommit = providers.environmentVariable('DROIDDECK_SOURCE_COMMIT').getOrNull() ?: providers.environmentVariable('GITHUB_SHA').getOrNull() ?: gitValue(['rev-parse', 'HEAD']) ?: '' def prNumber = (providers.environmentVariable('DROIDDECK_PR_NUMBER').getOrNull() ?: '0').with { it.isInteger() ? it : '0' } def ciBuild = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true' -// The release key's certificate: a build signed with it, from CI or a computer, updates in place. def releaseSigner = rootProject.file('keystore/release-signer.sha256').text.trim() -// When the source was committed, for "built 3 hours ago": fixed per commit, so it costs no rebuilds. def commitTime = (gitValue(['show', '-s', '--format=%ct', 'HEAD']) ?: '0').with { it.isLong() ? it : '0' } -// The desktop package this apk installs (tools/desktop-kde/release.env): published once per tag in -// DroidDeck-Components, pinned here with its checksum like gamescope and the esync packs, so the -// desktop scripts in the apk always get the package they were made for. 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()] } @@ -72,7 +65,6 @@ if (desktopRemovalSources.archives[desktopKde.DESKTOP_KDE_TAG] != desktopKde.DES if (!file('src/main/assets/desktop-removal/versions.txt').readLines().contains(desktopKde.DESKTOP_KDE_TAG)) { throw new GradleException('Regenerate desktop removal inventories for ' + desktopKde.DESKTOP_KDE_TAG) } -// tools/release/variants.txt as "package=suffix;...", so the updater picks the apk of the package it runs as. def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines() .findAll { it.trim() && !it.startsWith('#') } .collect { def f = it.trim().split(/\s+/); "${f[1]}=${f[2] == '-' ? '' : f[2]}" } @@ -81,16 +73,11 @@ def variantSuffixes = rootProject.file('tools/release/variants.txt').readLines() android { namespace 'com.droiddeck.launcher' compileSdk 34 - // The NDK is pinned by property so CI can hand in whatever the runner already has rather than - // downloading a second copy; see gradle.properties. ndkVersion project.findProperty('ndkVersion') ?: '27.3.13750724' defaultConfig { applicationId "com.droiddeck.launcher" minSdkVersion 26 - // 28 is not a leftover. An app on targetSdk 29+ runs in a domain that may not execute a - // file out of its own data directory, and this app's entire job is to exec a rootfs: - // proot, gamescope, the Steam client and everything it launches all live under files/. targetSdkVersion 28 versionCode 13 versionName "0.4.0" @@ -125,10 +112,6 @@ android { } signingConfigs { - // AOSP testkey (android@android.com), the same key Bannerlator ships with. It is publicly - // known, so it proves nothing about who built the apk - what it buys is ONE stable - // signature: every build installs over the last one instead of asking the user to - // uninstall first. v1 (JAR) + v2 + v3 so every installer and Android version accepts it. testkey { storeFile file("${rootDir}/keystore/testkey.p12") storeType "PKCS12" @@ -153,24 +136,15 @@ android { } lint { - // targetSdk 28 is deliberate and structural: an app on 29+ may not execute a file out of - // its own data directory, and executing a rootfs is the whole of what this app does. Lint - // fails a release build over it because Google Play will not accept an sdk that old - - // true, and not something this app can trade away. Every other check stays on. disable 'ExpiredTargetSdkVersion' } packagingOptions { resources { - // Kadb's Bouncy Castle artifacts repeat the same multi-release JAR manifest. excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF'] } jniLibs { - // The PulseAudio daemon is executed from the native library directory and its - // libraries are read out of the apk by the class loader: both want the old, - // uncompressed-and-extracted layout. useLegacyPackaging = true - // Prefer the PTY helper built by this app's NDK target over Termux's prebuilt copy. pickFirsts += ['**/libtermux.so'] } } @@ -181,9 +155,7 @@ android { } testOptions { - // Plain JVM tests: a stray android.util.Log call returns a default instead of throwing. unitTests.returnDefaultValues = true - // Robolectric tests start activities whose text is now all string resources. unitTests.includeAndroidResources = true } @@ -196,8 +168,6 @@ android { } } -// Session scripts must follow this checkout even when the native asset bundle is cached. -// A stale local bundle can omit a newly required helper on a fresh runtime install. def stageSessionScripts = tasks.register('stageSessionScripts', Copy) { from(rootProject.file('tools/linuxfs/overlay')) { include 'usr/local/bin/droiddeck-*', 'usr/local/bin/steam-compatibility', 'usr/bin/**' @@ -206,8 +176,6 @@ def stageSessionScripts = tasks.register('stageSessionScripts', Copy) { } tasks.named('preBuild').configure { dependsOn(stageSessionScripts) } -// Prepared audio lives under build/, so native builds never rewrite the tracked base bundle. -// Copying into one asset root also prevents an old base bundle winning a duplicate asset merge. def runtimeAssetsDir = layout.buildDirectory.dir('runtimeAssets') def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) { dependsOn(stageSessionScripts) @@ -218,12 +186,7 @@ def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) { android.sourceSets.main.assets.setSrcDirs([runtimeAssetsDir]) tasks.named('preBuild').configure { dependsOn(stageRuntimeAssets) } -// Unit tests and previews remain usable without Docker. Packaging an APK requires the complete -// runtime payload prepared by the local helper or CI, with matching sources and output hashes. def verifyRuntimeInputs = tasks.register('verifyRuntimeInputs', Exec) { - // The session scripts are re-copied into the asset tree by stageSessionScripts; with parallel - // task execution this check once hashed a file mid-copy and failed a main build. Hash only - // once the staging has finished. mustRunAfter(stageSessionScripts, stageRuntimeAssets) workingDir rootProject.projectDir commandLine 'python3', 'tools/release/runtime_inputs.py', 'verify' @@ -232,14 +195,6 @@ tasks.configureEach { task -> if (task.name in ['packageDebug', 'packageRelease']) task.dependsOn(verifyRuntimeInputs) } -// The GOG / Epic / Amazon download engines are Rust (app/src/main/rust/stores, ported from -// Bannerlator's bl-steam-client crate). cargo-ndk cross-compiles them for arm64-v8a into the build -// directory, which is one more jniLibs source below: nothing compiled is committed, the same rule -// the preloads, proot and the DirectAudio relay follow. cargo-ndk exports the NDK clang/llvm-ar as -// the C compiler for the one C-compiling crate (ring, under rustls) - the step Bannerlator's CI -// does by hand - and reads the NDK from ANDROID_NDK_HOME, which is set to the NDK this build pins. -// -PskipRust=true builds without the library, for a checkout with no Rust toolchain: the stores -// then report themselves unavailable instead of the build failing. def rustCrateDir = layout.projectDirectory.dir('src/main/rust/stores') def rustJniLibsDir = layout.buildDirectory.dir('rustJniLibs') def skipRust = (project.findProperty('skipRust') ?: 'false').toString().toBoolean() @@ -255,9 +210,6 @@ def buildRustStores = tasks.register('buildRustStores', Exec) { }.standardOutput.asText) outputs.dir(rustJniLibsDir) workingDir rustCrateDir - // The configuration cache serialises the task, so everything it needs is resolved here, not - // in doFirst. ~/.cargo/bin is where rustup puts cargo and cargo-ndk; an IDE-launched Gradle - // does not always have it on PATH. def cargoBin = "${System.getProperty('user.home')}/.cargo/bin" environment 'PATH', "${cargoBin}${File.pathSeparator}${System.getenv('PATH')}" environment 'ANDROID_NDK_HOME', android.ndkDirectory.absolutePath @@ -265,47 +217,32 @@ def buildRustStores = tasks.register('buildRustStores', Exec) { if (!skipRust) environment 'RUSTC', providers.exec { commandLine 'rustup', 'which', '--toolchain', 'stable', 'rustc' }.standardOutput.asText.get().trim() - // --platform 26 = minSdkVersion: the engines link against the API 26 libc, like the C++ code. - // Homebrew's cargo/rustc can precede rustup on macOS and use a different sysroot, despite - // rustup reporting the Android target installed. Run both through the selected toolchain. commandLine 'rustup', 'run', 'stable', 'cargo', 'ndk', '--target', 'arm64-v8a', '--platform', '26', '--output-dir', rustJniLibsDir.get().asFile.absolutePath, 'build', '--release', '--locked' } -// Skipping Rust must omit its library even if another build left a cached .so behind. if (!skipRust) android.sourceSets.main.jniLibs.srcDir(rustJniLibsDir) tasks.named('preBuild').configure { dependsOn(buildRustStores) } dependencies { - // The runtime tarball is a zstd-compressed tar. implementation 'com.github.luben:zstd-jni:1.5.2-3@aar' implementation 'org.apache.commons:commons-compress:1.27.1' - // Bannerlator's Windows components come as .tar.xz (WinComponents). implementation 'org.tukaani:xz:1.10' - // The UI: every menu, dialog and overlay is Compose. The session's SurfaceView and the - // on-screen pad are Views underneath it, which is what they are in every Compose app. implementation platform('androidx.compose:compose-bom:2024.02.00') implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.material3:material3' - // @Preview for the composables; the tooling that renders them is a debug-only dependency. implementation 'androidx.compose.ui:ui-tooling-preview' debugImplementation 'androidx.compose.ui:ui-tooling' implementation 'androidx.activity:activity-compose:1.8.2' - // The File Manager, Bannerlator's: its icon set, image thumbnails and the coroutines it - // copies on. Same versions as Bannerlator so the screen stays a straight copy. implementation 'androidx.compose.material:material-icons-extended' implementation 'io.coil-kt:coil-compose:2.6.0' implementation 'com.caverock:androidsvg-aar:1.4' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' - // ADB-over-Wi-Fi pairing and authenticated shell client. Users still approve pairing from - // Android's Wireless debugging screen; DroidDeck only runs its one fixed setting command. implementation 'com.flyfishxu:kadb-android:1.2.1' - // Android terminal view/emulator used by the in-session secondary-display Linux shell. implementation 'com.termux.termux-app:terminal-view:0.118.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.16.1' testImplementation 'androidx.compose.ui:ui-test-junit4' - // The Android archive does not include the host natives used by JVM archive tests. testRuntimeOnly 'com.github.luben:zstd-jni:1.5.2-3@jar' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d833ca2ae..1518aed57 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,34 +4,19 @@ - - - - - - - - @@ -50,11 +35,6 @@ android:appCategory="game" android:isGame="true"> - - - - - - - - - SPIR-V translator (zlib licence), vendored under thirdparty/dxbc from the same -# subset upstream lsfg-vk uses. Native LSFG frame generation needs it: the Lossless Scaling -# shader chain is DXBC in every build downloadable from Steam. +# DXVK's DXBC->SPIR-V translator is zlib licensed (vendored under thirdparty/dxbc). add_subdirectory(thirdparty/dxbc) -# The Win-FG chain self-identifies by two halves: the upstream tag it was copied from (hand-set, -# UPDATE IT when re-copying) and a hash over the sources themselves, computed here, which cannot -# rot. The compositor's frame-generation leg (waylandcomp/CMakeLists.txt) stamps both in. set(WINFG_UPSTREAM "v0.3.0") set(WINFG_CHAIN_SRC framegen/winfg/framegen.cpp @@ -44,11 +33,7 @@ add_subdirectory(waylandcomp) add_library(sdnative SHARED sdnative/sdnative.c) target_link_libraries(sdnative log) -# The device info window's Vulkan section: the phone's own driver, asked once through the system -# loader (opened with dlopen, so a phone without Vulkan still loads the app). add_library(deviceinfo SHARED deviceinfo/vkinfo.c) target_link_libraries(deviceinfo dl) -# Rebuild Termux terminal-emulator's PTY JNI helper with the app's NDK instead of carrying its -# prebuilt libtermux.so with a redundant libstdc++.so dependency. add_library(termux SHARED termux/termux.c) diff --git a/app/src/main/cpp/adrenotools/build_asm.sh b/app/src/main/cpp/adrenotools/build_asm.sh index b53d25c1b..6f90a46a0 100755 --- a/app/src/main/cpp/adrenotools/build_asm.sh +++ b/app/src/main/cpp/adrenotools/build_asm.sh @@ -3,7 +3,6 @@ aarch64-linux-gnu-gcc -c src/bcenabler_patch.s -o /tmp/patch.o aarch64-linux-gnu-objcopy -O binary -j .text /tmp/patch.o /tmp/patch.bin -# Must be smaller than a single page bin2c -o gen/bcenabler_patch.h -n PatchRawData /tmp/patch.bin rm /tmp/patch.o diff --git a/app/src/main/cpp/adrenotools/gen/bcenabler_patch.h b/app/src/main/cpp/adrenotools/gen/bcenabler_patch.h index 3909a25ca..c68424524 100644 --- a/app/src/main/cpp/adrenotools/gen/bcenabler_patch.h +++ b/app/src/main/cpp/adrenotools/gen/bcenabler_patch.h @@ -1,6 +1,4 @@ -/* Generated by bin2c, do not edit manually */ -/* Contents of file /tmp/patch.bin */ const long int PatchRawData_size = 264; const unsigned char PatchRawData[264] = { 0x1F, 0x0C, 0x02, 0x71, 0x61, 0x00, 0x00, 0x54, 0xE0, 0x08, 0x80, 0x52, 0x3E, 0x00, 0x00, 0x14, diff --git a/app/src/main/cpp/adrenotools/include/adrenotools/bcenabler.h b/app/src/main/cpp/adrenotools/include/adrenotools/bcenabler.h index 3d3a76e7d..5f97f11c1 100644 --- a/app/src/main/cpp/adrenotools/include/adrenotools/bcenabler.h +++ b/app/src/main/cpp/adrenotools/include/adrenotools/bcenabler.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #ifdef __cplusplus @@ -11,29 +12,14 @@ extern "C" { #include -/** - * @brief Describes the level of BCeNabler support for a driver - */ enum adrenotools_bcn_type { - ADRENOTOOLS_BCN_INCOMPATIBLE, //!< Driver isn't supported by BCeNabler - ADRENOTOOLS_BCN_BLOB, //!< Driver already supports BCn textures so BCeNabler isn't necessary - ADRENOTOOLS_BCN_PATCH //!< Driver can be patched with BCeNabler to support BCn textures + ADRENOTOOLS_BCN_INCOMPATIBLE, + ADRENOTOOLS_BCN_BLOB, + ADRENOTOOLS_BCN_PATCH }; -/** - * @brief Checks the status of BCn support in the supplied driver - * @param major The major part of VkPhysicalDeviceProperties::driverVersion - * @param minor The minor part of VkPhysicalDeviceProperties::driverVersion - * @param vendorId VkPhysicalDeviceProperties::vendorId - */ enum adrenotools_bcn_type adrenotools_get_bcn_type(uint32_t major, uint32_t minor, uint32_t vendorId); -/** - * @brief Patches the Adreno graphics driver to enable support for BCn compressed formats - * @note adrenotools_get_bcn_type MUST be checked to equal ADRENOTOOLS_BCN_PATCH before calling this - * @param vkGetPhysicalDeviceFormatPropertiesFn A pointer to vkGetPhysicalDeviceFormatProperties obtained through vkGetInstanceProcAddr. This is used to find the correct function to patch - * @return If the patching succeeded, if false the driver will be in an undefined state - */ bool adrenotools_patch_bcn(void *vkGetPhysicalDeviceFormatPropertiesFn); #ifdef __cplusplus diff --git a/app/src/main/cpp/adrenotools/include/adrenotools/driver.h b/app/src/main/cpp/adrenotools/include/adrenotools/driver.h index f81e8fe34..a81f98593 100644 --- a/app/src/main/cpp/adrenotools/include/adrenotools/driver.h +++ b/app/src/main/cpp/adrenotools/include/adrenotools/driver.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #ifdef __cplusplus @@ -10,60 +11,16 @@ extern "C" { #include #include "priv.h" -/** - * @brief Opens a new libvulkan.so instance according to `flags` - * @remark IMPORTANT: The app MUST be packaged w/ useLegacyPackaging = true. The reason for this is - * that we install the hook in hookLibDir; but this folder won't be set correctly when useLegacyPackaging == false - * because Android expects to read the *.so directly from the apk (instead of decompressing them into nativeLibraryDir). - * @param dlopenMode The dlopen mode to use when opening libvulkan - * @param featureFlags Which adrenotools driver features to enable - * @param tmpLibDir A writable directory to hold patched libraries, only used on api < 29 due to the lack of memfd support. If nullptr is passed and the API version is < 29 memfd usage will be attempted and if unsupported nullptr will be returned - * @param hookLibDir The directory holding the built hooks. - * IMPORTANT: This path MUST point to whatever `getApplicationInfo().nativeLibraryDir` (Kotlin code) returns. - * Failing to do so will result in this function returning a valid ptr but then the hook will fail and either the original driver is loaded as fallback, or vkEnumeratePhysicalDevices returns 0 devices. - * @param customDriverDir The directory to load a custom GPU driver named according to `customDriverName` from. Only used if ADRENOTOOLS_DRIVER_CUSTOM is set in `featureFlags`. - * IMPORTANT: This path MUST NOT be on sdcard/storage. i.e. use ANativeActivity::internalDataPath and often looks like "/data/user/0/com.example.app/files/". - * Otherwise you will get permission denied because dlopen() can't be done on *.so libraries that any random user or application can manipulate (which would be a security risk). - * @param customDriverName The soname of the custom driver to load. Only used if ADRENOTOOLS_DRIVER_CUSTOM is set in `featureFlags` - * @param fileRedirectDir The directory which to redirect all file accesses performed by the driver to. Only used if ADRENOTOOLS_DRIVER_FILE_REDIRECT is set in `featureFlags` - * @param userMappingHandle A pointer to a void* which will be set to the mapping handle if ADRENOTOOLS_DRIVER_GPU_MAPPING_IMPORT is set in `featureFlags` - */ void *adrenotools_open_libvulkan(int dlopenMode, int featureFlags, const char *tmpLibDir, const char *hookLibDir, const char *customDriverDir, const char *customDriverName, const char *fileRedirectDir, void **userMappingHandle); -/** - * @brief Imports the given CPU mapped memory range into the GSL allocator. This should then be followed by a call to vkAllocateMemory with a matching size which will return a VkDeviceMemory view over the input region - * @param handle Mapping handle that was returned by adrenotools_open_libvulkan - * @param hostPtr The host pointer to import - * @param size The size of the region to import - * @return True on success - */ bool adrenotools_import_user_mem(void *handle, void *hostPtr, uint64_t size); -/** - * @brief Maps GPU memory and imports it into the GSL allocator. This should then be followed by a call to adrenotools_mem_cpu_map to map the memory on the CPU, then finally vkAllocateMemory with a matching size - * @param handle Mapping handle that was returned by adrenotools_open_libvulkan - * @param size Pointer to a variable containing the size of the region to import, will be updated to contain the required size of the region to allocate CPU side - * @return true on success - */ bool adrenotools_mem_gpu_allocate(void *handle, uint64_t *size); -/** - * @brief Maps the last mapping allocated using adrenotools_mem_gpu_allocate into the given host memory region, such that vkAllocateMemory can then be called - * @param handle Mapping handle that was returned by adrenotools_open_libvulkan - * @param hostPtr A pointer to where the mapping should be mapped - * @param size The size of the mapping. MUST be equal to the size returned by adrenotools_mem_gpu_allocate - */ bool adrenotools_mem_cpu_map(void *handle, void *hostPtr, uint64_t size); -/** - * @note This function should be called after adrenotools_open_libvulkan and Vulkan driver init to check if the mapping import hook loaded successfully - * @return True if the mapping was successfully imported (or initialization succeeded) - */ bool adrenotools_validate_gpu_mapping(void *handle); -/** - * @brief Provides a way to force the GPU to run at the maximum possible clocks (thermal constraints will still be applied) - */ void adrenotools_set_turbo(bool turbo); #ifdef __cplusplus diff --git a/app/src/main/cpp/adrenotools/include/adrenotools/priv.h b/app/src/main/cpp/adrenotools/include/adrenotools/priv.h index 742d24f24..77135d2c9 100644 --- a/app/src/main/cpp/adrenotools/include/adrenotools/priv.h +++ b/app/src/main/cpp/adrenotools/include/adrenotools/priv.h @@ -1,13 +1,11 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #include -/** - * @brief Bitfield enum of additional driver features that can be used with adrenotools_open_libvulkan - */ enum { ADRENOTOOLS_DRIVER_CUSTOM = 1 << 0, ADRENOTOOLS_DRIVER_FILE_REDIRECT = 1 << 1, @@ -16,12 +14,9 @@ enum { #define ADRENOTOOLS_GPU_MAPPING_SUCCEEDED_MAGIC 0xDEADBEEF -/** - * @brief A replacement GPU memory mapping for use with ADRENOTOOLS_DRIVER_GPU_MAPPING_IMPORT - */ struct adrenotools_gpu_mapping { void *host_ptr; - uint64_t gpu_addr; //!< The GPU address of the mapping to import, if mapping import/init succeeds this will be set to ADRENOTOOLS_GPU_MAPPING_SUCCEEDED_MAGIC + uint64_t gpu_addr; uint64_t size; uint64_t flags; }; \ No newline at end of file diff --git a/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.cpp b/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.cpp index 3bd58992c..8220824eb 100644 --- a/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.cpp +++ b/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #include #include #include @@ -21,7 +22,6 @@ static loader_android_create_namespace_t loader_android_create_namespace; static bool lib_loaded; -/* Public API */ bool linkernsbypass_load_status() { return lib_loaded; } @@ -55,8 +55,6 @@ android_link_namespaces_all_libs_t android_link_namespaces_all_libs; android_link_namespaces_t android_link_namespaces; bool linkernsbypass_link_namespace_to_default_all_libs(android_namespace_t *to) { - // Creating a shared namespace with the default parent will give a copy of the default namespace that we can actually access - // This is needed since there is no way to access a direct handle to the default namespace as it's not exported static auto defaultNs{android_create_namespace_escape("default_copy", nullptr, nullptr, ANDROID_NAMESPACE_TYPE_SHARED, nullptr, nullptr)}; return android_link_namespaces_all_libs(to, defaultNs); } @@ -81,7 +79,6 @@ void *linkernsbypass_namespace_dlopen(const char *filename, int flags, android_n void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *libTargetDir, int flags, android_namespace_t *ns) { static std::array PathBuf{}; - // Used as a unique ID for overwriting soname and creating target lib files static uint16_t TargetId{}; int libTargetFd{[&] () { @@ -89,7 +86,6 @@ void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *li snprintf(PathBuf.data(), PathBuf.size(), "%s/%d_patched.so", libTargetDir, TargetId); return open(PathBuf.data(), O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); } else { - // If memfd isn't supported errno will contain ENOSYS after calling errno = 0; int fd{static_cast(syscall(__NR_memfd_create, libPath, 0))}; if (errno == ENOSYS || fd < 0) @@ -101,21 +97,18 @@ void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *li if (libTargetFd == -1) return nullptr; - // Partially overwrite soname with 3 digits (replacing lib...) with to make sure a cached so isn't loaded std::array sonameOverwrite{}; snprintf(sonameOverwrite.data(), sonameOverwrite.size(), "%03u", TargetId++); if (!elf_soname_patch(libPath, libTargetFd, sonameOverwrite.data())) return nullptr; - // Load our patched library into the hook namespace android_dlextinfo hookExtInfo{ .flags = ANDROID_DLEXT_USE_NAMESPACE | ANDROID_DLEXT_USE_LIBRARY_FD, .library_fd = libTargetFd, .library_namespace = ns }; - // Make a path that looks about right snprintf(PathBuf.data(), PathBuf.size(), "/proc/self/fd/%d", libTargetFd); return android_dlopen_ext(PathBuf.data(), flags, &hookExtInfo); @@ -125,21 +118,19 @@ static void *align_ptr(void *ptr) { return reinterpret_cast(reinterpret_cast(ptr) & ~(getpagesize() - 1)); } -/* Private */ __attribute__((constructor)) static void resolve_linker_symbols() { using loader_dlopen_t = void *(*)(const char *, int, const void *); if (android_get_device_api_level() < 28) return; - // ARM64 specific function walking to locate the internal dlopen handler auto loader_dlopen{[]() { union BranchLinked { uint32_t raw; struct { - int32_t offset : 26; //!< 26-bit branch offset - uint8_t sig : 6; //!< 6-bit signature + int32_t offset : 26; + uint8_t sig : 6; }; bool Verify() { @@ -148,10 +139,8 @@ __attribute__((constructor)) static void resolve_linker_symbols() { }; static_assert(sizeof(BranchLinked) == 4, "BranchLinked is wrong size"); - // Some devices ship with --X mapping for exexecutables so work around that mprotect(align_ptr(reinterpret_cast(&dlopen)), getpagesize(), PROT_WRITE | PROT_READ | PROT_EXEC); - // dlopen is just a wrapper for __loader_dlopen that passes the return address as the third arg hence we can just walk it to find __loader_dlopen auto blInstr{reinterpret_cast(&dlopen)}; while (!blInstr->Verify()) blInstr++; @@ -159,10 +148,8 @@ __attribute__((constructor)) static void resolve_linker_symbols() { return reinterpret_cast(blInstr + blInstr->offset); }()}; - // Protect the loader_dlopen function to remove the BTI attribute (since this is an internal function that isn't intended to be jumped indirectly to) mprotect(align_ptr(reinterpret_cast(&loader_dlopen)), getpagesize(), PROT_WRITE | PROT_READ | PROT_EXEC); - // Passing dlopen as a caller address tricks the linker into using the internal unrestricted namespace letting us access libraries that are normally forbidden in the classloader namespace imposed on apps auto ldHandle{loader_dlopen("ld-android.so", RTLD_LAZY, reinterpret_cast(&dlopen))}; if (!ldHandle) return; @@ -187,6 +174,5 @@ __attribute__((constructor)) static void resolve_linker_symbols() { if (!android_get_exported_namespace) return; - // Lib is now safe to use lib_loaded = true; } diff --git a/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.h b/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.h index 77ec4005a..cc775b0de 100644 --- a/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.h +++ b/app/src/main/cpp/adrenotools/lib/linkernsbypass/android_linker_ns.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #ifdef __cplusplus @@ -9,7 +10,6 @@ extern "C" { #include -// https://cs.android.com/android/platform/superproject/+/0a492a4685377d41fef2b12e9af4ebfa6feef9c2:art/libnativeloader/include/nativeloader/dlext_namespaces.h;l=25;bpv=1;bpt=1 enum { ANDROID_NAMESPACE_TYPE_REGULAR = 0, ANDROID_NAMESPACE_TYPE_ISOLATED = 1, @@ -19,14 +19,8 @@ enum { ANDROID_NAMESPACE_TYPE_SHARED_ISOLATED = ANDROID_NAMESPACE_TYPE_SHARED | ANDROID_NAMESPACE_TYPE_ISOLATED, }; -/** - * @brief Checks if linkernsbypass loaded successfully and is safe to use - * @note IMPORTANT: This should be called before any calls to the rest of the library are made - * @return true if loading succeeded - */ bool linkernsbypass_load_status(); -// https://cs.android.com/android/platform/superproject/+/0a492a4685377d41fef2b12e9af4ebfa6feef9c2:art/libnativeloader/include/nativeloader/dlext_namespaces.h;l=86;bpv=1;bpt=1 struct android_namespace_t *android_create_namespace(const char *name, const char *ld_library_path, const char *default_library_path, @@ -41,39 +35,19 @@ struct android_namespace_t *android_create_namespace_escape(const char *name, const char *permitted_when_isolated_path, struct android_namespace_t *parent_namespace); -// https://cs.android.com/android/platform/superproject/+/dcb01ef31026b3b8aeb72dada3370af63fe66bbd:bionic/linker/linker.cpp;l=3554 typedef struct android_namespace_t *(*android_get_exported_namespace_t)(const char *); extern android_get_exported_namespace_t android_get_exported_namespace; -// https://cs.android.com/android/platform/superproject/+/dcb01ef31026b3b8aeb72dada3370af63fe66bbd:bionic/linker/linker.cpp;l=2499 typedef bool (*android_link_namespaces_all_libs_t)(struct android_namespace_t *, struct android_namespace_t *); extern android_link_namespaces_all_libs_t android_link_namespaces_all_libs; -// https://cs.android.com/android/platform/superproject/+/dcb01ef31026b3b8aeb72dada3370af63fe66bbd:bionic/linker/linker.cpp;l=2473 typedef bool (*android_link_namespaces_t)(struct android_namespace_t *, struct android_namespace_t *, const char *); extern android_link_namespaces_t android_link_namespaces; -/** - * @brief Like android_link_namespaces_all_libs but links from the default namespace - */ bool linkernsbypass_link_namespace_to_default_all_libs(struct android_namespace_t *to); -/** - * @brief Loads a library into a namespace - * @note IMPORTANT: If `filename` is compiled with the '-z global' linker flag and RTLD_GLOBAL is supplied in `flags` the library will be added to the namespace's LD_PRELOAD list - * @param filename The name of the library to load - * @param flags The rtld flags for `filename` - * @param ns The namespace to dlopen into - */ void *linkernsbypass_namespace_dlopen(const char *filename, int flags, struct android_namespace_t *ns); -/** - * @brief Force loads a unique instance of a library into a namespace - * @param libPath The path to the library to load with hooks applied - * @param libTargetDir A temporary directory to hold the soname patched library at `libPath`, will attempt to use memfd if nullptr - * @param flags The rtld flags for `libName` - * @param ns The namespace to dlopen into - */ void *linkernsbypass_namespace_dlopen_unique(const char *libPath, const char *libTargetDir, int flags, struct android_namespace_t *ns); #ifdef __cplusplus diff --git a/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.cpp b/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.cpp index db619ab53..cd5ca5c47 100644 --- a/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.cpp +++ b/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #include #include #include @@ -20,7 +21,6 @@ bool elf_soname_patch(const char *libPath, int targetFd, const char *sonamePatch if (ftruncate(targetFd, libStat.st_size) == -1) return false; - // Map the memory so we can read our elf into it auto mappedLib{reinterpret_cast(mmap(nullptr, libStat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, targetFd, 0))}; if (!mappedLib) return false; @@ -29,30 +29,25 @@ bool elf_soname_patch(const char *libPath, int targetFd, const char *sonamePatch if (libFd == false) return false; - // Read lib elf into target file if (read(libFd, mappedLib, libStat.st_size) != libStat.st_size) return false; - // No longer needed close(libFd); auto eHdr{reinterpret_cast(mappedLib)}; auto sHdrEntries{reinterpret_cast(mappedLib + eHdr->e_shoff)}; - // Iterate over section headers to find the .dynamic section for (ElfW(Half) i{}; i < eHdr->e_shnum; i++) { auto &sHdr{sHdrEntries[i]}; if (sHdr.sh_type == SHT_DYNAMIC) { auto strTab{reinterpret_cast(mappedLib + sHdrEntries[sHdr.sh_link].sh_offset)}; auto dynHdrEntries{reinterpret_cast(mappedLib + sHdr.sh_offset)}; - // Iterate over .dynamic entries to find DT_SONAME for (ElfW(Xword) k{}; k < (sHdr.sh_size / sHdr.sh_entsize); k++) { auto &dynHdrEntry{dynHdrEntries[k]}; if (dynHdrEntry.d_tag == DT_SONAME) { char *soname{strTab + dynHdrEntry.d_un.d_val}; - // Partially replace the old soname with the soname patch size_t charIdx{}; for (; soname[charIdx] != 0 && sonamePatch[charIdx] != 0; charIdx++) soname[charIdx] = sonamePatch[charIdx]; diff --git a/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.h b/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.h index 0b2c186ff..88ea58005 100644 --- a/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.h +++ b/app/src/main/cpp/adrenotools/lib/linkernsbypass/elf_soname_patcher.h @@ -1,19 +1,13 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #ifdef __cplusplus extern "C" { #endif -/** - * @brief Overwrites a portion of the soname in an elf by loading it into shared memory and modifying .dynstr - * @note IMPORTANT: The supplied soname patch will overwrite the first strlen(sonamePatch) chars of the soname - * @param elfPath Full path to the elf to patch - * @param targetFd FD to use for storing the patched library - * @return True on success - */ bool elf_soname_patch(const char *elfPath, int targetFd, const char *newSoname); #ifdef __cplusplus diff --git a/app/src/main/cpp/adrenotools/lib/linkernsbypass/meson.build b/app/src/main/cpp/adrenotools/lib/linkernsbypass/meson.build index e666aff8a..12919c500 100644 --- a/app/src/main/cpp/adrenotools/lib/linkernsbypass/meson.build +++ b/app/src/main/cpp/adrenotools/lib/linkernsbypass/meson.build @@ -11,8 +11,3 @@ linkernsbypass_lib = static_library ( include_directories: ([linkernsbypass_inc, adrenotools_inc]), dependencies: ([libandroid_dep, libdl_dep]) ) - -# linkernsbypass_include = static_library('linkernsbypass', sources) -# target_compile_options(['linkernsbypass', 'PRIVATE', '-Wall', '-Wextra']) -# target_link_libraries(['linkernsbypass', 'android', 'dl']) -# target_include_directories(['linkernsbypass', 'PUBLIC', '.']) diff --git a/app/src/main/cpp/adrenotools/meson.build b/app/src/main/cpp/adrenotools/meson.build index bf519bb2a..37404c268 100644 --- a/app/src/main/cpp/adrenotools/meson.build +++ b/app/src/main/cpp/adrenotools/meson.build @@ -33,18 +33,3 @@ adrenotools_lib = shared_library( ) subdir('src/hook') - -# target_include_directories(['adrenotools', 'PUBLIC', 'include']) -# target_include_directories(['adrenotools', 'PRIVATE', '.']) -# target_compile_options(['adrenotools', 'PRIVATE', '-Wall', '-Wextra']) -# target_link_libraries(['adrenotools', 'android', 'linkernsbypass']) -# hooks_deb = subproject('src/hook') -# if build_shared_libs - # target_link_options(['adrenotools', 'PRIVATE', '-Wl,-s,--exclude-libs,liblinkernsbypass.a']) -# endif -# if gen_install_target - # configure_file(['adrenotools.pc.in', 'adrenotools.pc', '@ONLY']) - # install(['FILES', cmake_binary_dir, '/adrenotools.pc', 'DESTINATION', 'lib/pkgconfig']) - # install(['TARGETS', 'adrenotools', 'main_hook', 'file_redirect_hook', 'gsl_alloc_hook', 'hook_impl']) - # install(['DIRECTORY', 'include/', 'DESTINATION', 'include', 'FILES_MATCHING', 'PATTERN', '*.h*']) -# endif diff --git a/app/src/main/cpp/adrenotools/src/bcenabler.cpp b/app/src/main/cpp/adrenotools/src/bcenabler.cpp index 1deb41df4..6bf3e98dd 100644 --- a/app/src/main/cpp/adrenotools/src/bcenabler.cpp +++ b/app/src/main/cpp/adrenotools/src/bcenabler.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #include #include #include @@ -19,7 +20,6 @@ enum adrenotools_bcn_type adrenotools_get_bcn_type(uint32_t major, uint32_t mino return ADRENOTOOLS_BCN_PATCH; } -// Searches /proc/self/maps for the first free page after the given address static void *find_free_page(uintptr_t address) { std::ifstream procMaps("/proc/self/maps"); @@ -45,62 +45,48 @@ static void *align_ptr(void *ptr) { bool adrenotools_patch_bcn(void *vkGetPhysicalDeviceFormatPropertiesFn) { union Branch { struct { - int32_t offset : 26; //!< 26-bit branch offset - uint8_t sig : 6; //!< 6-bit signature (0x25 for linked, 0x5 for jump) + int32_t offset : 26; + uint8_t sig : 6; }; uint32_t raw{}; }; static_assert(sizeof(Branch) == 4, "Branch size is invalid"); - // Find the nearest unmapped page where we can place patch code void *patchPage{find_free_page(reinterpret_cast(vkGetPhysicalDeviceFormatPropertiesFn))}; if (!patchPage) return false; - // Map patch region void *ptr{mmap(patchPage, getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0)}; if (ptr != patchPage) return false; - // Allow reading from the blob's .text section since some devices enable ---X - // Protect two pages just in case we happen to land on a page boundary if (mprotect(align_ptr(vkGetPhysicalDeviceFormatPropertiesFn), getpagesize() * 2, PROT_WRITE | PROT_READ | PROT_EXEC)) return false; - // First branch in this function is targeted at the function we want to patch Branch *blInst{reinterpret_cast(vkGetPhysicalDeviceFormatPropertiesFn)}; constexpr uint8_t BranchLinkSignature{0x25}; - // Search for first instruction with the BL signature while (blInst->sig != BranchLinkSignature) blInst++; - // Internal QGL format conversion function that we need to patch uint32_t *convFormatFn{reinterpret_cast(blInst) + blInst->offset}; - // See mprotect call above - // This time we also set PROT_WRITE so we can write our patch to the page if (mprotect(align_ptr(convFormatFn), getpagesize() * 2, PROT_WRITE | PROT_READ | PROT_EXEC)) return false; - // This would normally set the default result to 0 (error) in the format not found case constexpr uint32_t ClearResultSignature{0x2a1f03e0}; - // We replace it with a branch to our own extended if statement which adds in the extra things for BCn uint32_t *clearResultPtr{convFormatFn}; while (*clearResultPtr != ClearResultSignature) clearResultPtr++; - // Ensure we don't write out of bounds if (PatchRawData_size > getpagesize()) return false; - // Copy the patch function to our mapped page memcpy(patchPage, PatchRawData, PatchRawData_size); - // Fixup the patch code so it correctly returns back to the driver after running constexpr uint32_t PatchReturnFixupMagic{0xffffffff}; constexpr uint8_t BranchSignature{0x5}; @@ -130,6 +116,5 @@ bool adrenotools_patch_bcn(void *vkGetPhysicalDeviceFormatPropertiesFn) { asm volatile("ISB"); - // Done! return true; } diff --git a/app/src/main/cpp/adrenotools/src/bcenabler_patch.s b/app/src/main/cpp/adrenotools/src/bcenabler_patch.s index 8766e7880..99fab59bc 100644 --- a/app/src/main/cpp/adrenotools/src/bcenabler_patch.s +++ b/app/src/main/cpp/adrenotools/src/bcenabler_patch.s @@ -80,4 +80,4 @@ .L18: mov w0, 0 .L3: - .word 0xffffffff // Branch fixup + .word 0xffffffff diff --git a/app/src/main/cpp/adrenotools/src/driver.cpp b/app/src/main/cpp/adrenotools/src/driver.cpp index 581e04a67..848beae8c 100644 --- a/app/src/main/cpp/adrenotools/src/driver.cpp +++ b/app/src/main/cpp/adrenotools/src/driver.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #include #include #include @@ -17,17 +18,14 @@ #include void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char *tmpLibDir, const char *hookLibDir, const char *customDriverDir, const char *customDriverName, const char *fileRedirectDir, void **userMappingHandle) { - // Bail out if linkernsbypass failed to load, this probably means we're on api < 28 if (!linkernsbypass_load_status()) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Could not load linkernsbypass\n"); return nullptr; } - // Use memfd on Q+ since it's guaranteed to work, but only if tmpLibDir wasn't set if (android_get_device_api_level() >= 29 && !tmpLibDir) tmpLibDir = nullptr; - // Verify that params for specific features are only passed if they are enabled if (!(featureFlags & ADRENOTOOLS_DRIVER_FILE_REDIRECT) && fileRedirectDir) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "ADRENOTOOLS_DRIVER_FILE_REDIRECT present but no file redirect folder found\n"); return nullptr; @@ -43,7 +41,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char * return nullptr; } - // Verify that params for enabled features are correct struct stat buf{}; if (featureFlags & ADRENOTOOLS_DRIVER_CUSTOM) { @@ -58,7 +55,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char * } } - // Verify that params for enabled features are correct if (featureFlags & ADRENOTOOLS_DRIVER_FILE_REDIRECT) { if (!fileRedirectDir) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "ADRENOTOOLS_DRIVER_REDIRECT_DIR present but no folder parameter was found\n"); @@ -71,22 +67,18 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char * } } - // Create a namespace that can isolate our hook from the classloader namespace auto hookNs{android_create_namespace("adrenotools-libvulkan", hookLibDir, nullptr, ANDROID_NAMESPACE_TYPE_SHARED, nullptr, nullptr)}; - // Link it to the default namespace so the hook can use libandroid etc if (!linkernsbypass_link_namespace_to_default_all_libs(hookNs)) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Failed to hook our namespace to the default one\n"); return nullptr; } - // Preload the hook implementation, otherwise we get a weird issue where despite being in NEEDED of the hook lib the hook's symbols will overwrite ours and cause an infinite loop auto hookImpl{linkernsbypass_namespace_dlopen("libhook_impl.so", RTLD_NOW, hookNs)}; if (!hookImpl) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Couldn't preload the hook implementation\n"); return nullptr; } - // Pass parameters to the hook implementation auto initHookParam{reinterpret_cast(dlsym(hookImpl, "init_hook_param"))}; if (!initHookParam) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Couldn't init hook params\n"); @@ -95,7 +87,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char * auto importMapping{[&]() -> adrenotools_gpu_mapping * { if (featureFlags & ADRENOTOOLS_DRIVER_GPU_MAPPING_IMPORT) { - // This will be leaked, but it's not a big deal since it's only a few bytes adrenotools_gpu_mapping *mapping{new adrenotools_gpu_mapping{}}; *userMappingHandle = mapping; return mapping; @@ -107,7 +98,6 @@ void *adrenotools_open_libvulkan(int dlopenFlags, int featureFlags, const char * initHookParam(new HookImplParams(featureFlags, tmpLibDir, hookLibDir, customDriverDir, customDriverName, fileRedirectDir, importMapping)); - // Load the libvulkan hook into the isolated namespace if (!linkernsbypass_namespace_dlopen("libmain_hook.so", RTLD_GLOBAL, hookNs)) { __android_log_print( ANDROID_LOG_ERROR, "adrenotools", "Failed to load libvulkan into the isolated namespace\n"); return nullptr; @@ -150,7 +140,7 @@ bool adrenotools_import_user_mem(void *handle, void *hostPtr, uint64_t size) { importMapping->host_ptr = hostPtr; importMapping->gpu_addr = info.gpuaddr; importMapping->size = size; - importMapping->flags = 0xc2600; //!< Unknown flags, but they are required for the mapping to work + importMapping->flags = 0xc2600; close(kgslFd); return true; @@ -189,7 +179,7 @@ bool adrenotools_mem_gpu_allocate(void *handle, uint64_t *size) { mapping->host_ptr = nullptr; mapping->gpu_addr = info.gpuaddr; mapping->size = *size; - mapping->flags = 0xc2600; //!< Unknown flags, but they are required for the mapping to work + mapping->flags = 0xc2600; close(kgslFd); return true; diff --git a/app/src/main/cpp/adrenotools/src/hook/hook_impl.cpp b/app/src/main/cpp/adrenotools/src/hook/hook_impl.cpp index 50e79a946..8fa80ee18 100644 --- a/app/src/main/cpp/adrenotools/src/hook/hook_impl.cpp +++ b/app/src/main/cpp/adrenotools/src/hook/hook_impl.cpp @@ -15,7 +15,7 @@ #define TAG "hook_impl" #define LOGI(fmt, ...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##__VA_ARGS__) -const HookImplParams *hook_params; //!< Bunch of info needed to load/patch the driver +const HookImplParams *hook_params; int (*gsl_memory_alloc_pure_sym)(uint32_t, uint32_t, void *); int (*gsl_memory_alloc_pure_64_sym)(uint64_t, uint32_t, void *); int (*gsl_memory_free_pure_sym)(void *); @@ -43,7 +43,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char LOGI("hook_android_dlopen_ext: filename: %s", filename); - // Ignore non-vulkan libraries if (!strstr(filename, "vulkan.")) return android_dlopen_ext(filename, flags, extinfo); @@ -52,7 +51,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char return fallback(); } - // customDriverDir will be empty if ADRENOTOOLS_DRIVER_CUSTOM isn't set therefore it's fine to have either way auto driverNs{android_create_namespace(filename, hook_params->customDriverDir.c_str(), hook_params->hookLibDir.c_str(), ANDROID_NAMESPACE_TYPE_SHARED, nullptr, extinfo->library_namespace)}; @@ -61,16 +59,12 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char return fallback(); } - // We depend on libandroid which is unlikely to be in the supplied driver namespace so we have to link it over android_link_namespaces(driverNs, nullptr, "libandroid.so"); - // Preload ourself, a new instance will be created since we have different linker ancestory - // If we don't preload we get a weird issue where despite being in NEEDED of the hook lib the hook's symbols will overwrite ours and cause an infinite loop auto hookImpl{linkernsbypass_namespace_dlopen("libhook_impl.so", RTLD_NOW, driverNs)}; if (!hookImpl) return nullptr; - // Pass parameters to ourself auto initHookParam{reinterpret_cast(dlsym(hookImpl, "init_hook_param"))}; if (!initHookParam) return nullptr; @@ -86,7 +80,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char LOGI("hook_android_dlopen_ext: applied libfile_redirect_hook"); } - // Use our new namespace to load the vulkan driver auto newExtinfo{*extinfo}; newExtinfo.library_namespace = driverNs; @@ -122,8 +115,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char LOGI("hook_android_dlopen_ext: hook failed: failed to apply libgsl_alloc_hook!"); } - // TODO: If there is already an instance of a Vulkan driver loaded hooks won't be applied, this will only be the case for skiavk generally - // To fix this we would need to search /proc/self/maps for the file to a loaded instance of the library in order to read it to patch the soname and load it uniquely if (hook_params->featureFlags & ADRENOTOOLS_DRIVER_CUSTOM) { LOGI("hook_android_dlopen_ext: loading custom driver: %s%s", hook_params->customDriverDir.c_str(), hook_params->customDriverName.c_str()); void *handle{android_dlopen_ext(hook_params->customDriverName.c_str(), flags, &newExtinfo)}; @@ -142,7 +133,6 @@ __attribute__((visibility("default"))) void *hook_android_dlopen_ext(const char __attribute__((visibility("default"))) void *hook_android_load_sphal_library(const char *filename, int flags) { LOGI("hook_android_load_sphal_library: filename: %s", filename); - // https://android.googlesource.com/platform/system/core/+/master/libvndksupport/linker.cpp for (const char *name : {"sphal", "vendor", "default"}) { if (auto vendorNs{android_get_exported_namespace(name)}) { android_dlextinfo dlextinfo{ diff --git a/app/src/main/cpp/adrenotools/src/hook/hook_impl.h b/app/src/main/cpp/adrenotools/src/hook/hook_impl.h index 2ce2471e1..77be0fd71 100644 --- a/app/src/main/cpp/adrenotools/src/hook/hook_impl.h +++ b/app/src/main/cpp/adrenotools/src/hook/hook_impl.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #ifdef __cplusplus diff --git a/app/src/main/cpp/adrenotools/src/hook/hook_impl_params.h b/app/src/main/cpp/adrenotools/src/hook/hook_impl_params.h index 8a3bd1673..7dbabd7b7 100644 --- a/app/src/main/cpp/adrenotools/src/hook/hook_impl_params.h +++ b/app/src/main/cpp/adrenotools/src/hook/hook_impl_params.h @@ -1,15 +1,12 @@ // SPDX-License-Identifier: BSD-2-Clause // Copyright © 2021 Billy Laws + #pragma once #include #include -/** - * @brief Holds the parameters needed for all hooks - * @note See comments for adrenotools_open_libvulkan as a reference for member variables - */ struct HookImplParams { int featureFlags; std::string tmpLibDir; diff --git a/app/src/main/cpp/adrenotools/src/hook/kgsl.h b/app/src/main/cpp/adrenotools/src/hook/kgsl.h index 6d4d93d6a..fa99b1d34 100644 --- a/app/src/main/cpp/adrenotools/src/hook/kgsl.h +++ b/app/src/main/cpp/adrenotools/src/hook/kgsl.h @@ -3,50 +3,27 @@ * Copyright (c) 2018-2021, The Linux Foundation. All rights reserved. * Copyright (c) 2022 Qualcomm Innovation Center, Inc. All rights reserved. */ - #ifndef _UAPI_MSM_KGSL_H #define _UAPI_MSM_KGSL_H #include #include -/* - * The KGSL version has proven not to be very useful in userspace if features - * are cherry picked into other trees out of order so it is frozen as of 3.14. - * It is left here for backwards compatabilty and as a reminder that - * software releases are never linear. Also, I like pie. - */ #define KGSL_VERSION_MAJOR 3 #define KGSL_VERSION_MINOR 14 -/* - * We have traditionally mixed context and issueibcmds / command batch flags - * together into a big flag stew. This worked fine until we started adding a - * lot more command batch flags and we started running out of bits. Turns out - * we have a bit of room in the context type / priority mask that we could use - * for command batches, but that means we need to split out the flags into two - * coherent sets. - * - * If any future definitions are for both context and cmdbatch add both defines - * and link the cmdbatch to the context define as we do below. Otherwise feel - * free to add exclusive bits to either set. - */ -/* --- context flags --- */ #define KGSL_CONTEXT_SAVE_GMEM 0x00000001 #define KGSL_CONTEXT_NO_GMEM_ALLOC 0x00000002 -/* This is a cmdbatch exclusive flag - use the CMDBATCH equivalent instead */ #define KGSL_CONTEXT_SUBMIT_IB_LIST 0x00000004 #define KGSL_CONTEXT_CTX_SWITCH 0x00000008 #define KGSL_CONTEXT_PREAMBLE 0x00000010 #define KGSL_CONTEXT_TRASH_STATE 0x00000020 #define KGSL_CONTEXT_PER_CONTEXT_TS 0x00000040 #define KGSL_CONTEXT_USER_GENERATED_TS 0x00000080 -/* This is a cmdbatch exclusive flag - use the CMDBATCH equivalent instead */ #define KGSL_CONTEXT_END_OF_FRAME 0x00000100 #define KGSL_CONTEXT_NO_FAULT_TOLERANCE 0x00000200 -/* This is a cmdbatch exclusive flag - use the CMDBATCH equivalent instead */ #define KGSL_CONTEXT_SYNC 0x00000400 #define KGSL_CONTEXT_PWR_CONSTRAINT 0x00000800 #define KGSL_CONTEXT_PRIORITY_MASK 0x0000F000 @@ -80,70 +57,43 @@ #define KGSL_CONTEXT_INVALID 0xffffffff -/* - * --- command batch flags --- - * The bits that are linked to a KGSL_CONTEXT equivalent are either legacy - * definitions or bits that are valid for both contexts and cmdbatches. To be - * safe the other 8 bits that are still available in the context field should be - * omitted here in case we need to share - the other bits are available for - * cmdbatch only flags as needed - */ #define KGSL_CMDBATCH_MEMLIST 0x00000001 #define KGSL_CMDBATCH_MARKER 0x00000002 -#define KGSL_CMDBATCH_SUBMIT_IB_LIST KGSL_CONTEXT_SUBMIT_IB_LIST /* 0x004 */ -#define KGSL_CMDBATCH_CTX_SWITCH KGSL_CONTEXT_CTX_SWITCH /* 0x008 */ +#define KGSL_CMDBATCH_SUBMIT_IB_LIST KGSL_CONTEXT_SUBMIT_IB_LIST +#define KGSL_CMDBATCH_CTX_SWITCH KGSL_CONTEXT_CTX_SWITCH #define KGSL_CMDBATCH_PROFILING 0x00000010 -/* - * KGSL_CMDBATCH_PROFILING must also be set for KGSL_CMDBATCH_PROFILING_KTIME - * to take effect, as the latter only affects the time data returned. - */ #define KGSL_CMDBATCH_PROFILING_KTIME 0x00000020 -#define KGSL_CMDBATCH_END_OF_FRAME KGSL_CONTEXT_END_OF_FRAME /* 0x100 */ -#define KGSL_CMDBATCH_SYNC KGSL_CONTEXT_SYNC /* 0x400 */ -#define KGSL_CMDBATCH_PWR_CONSTRAINT KGSL_CONTEXT_PWR_CONSTRAINT /* 0x800 */ -#define KGSL_CMDBATCH_SPARSE 0x1000 /* 0x1000 */ -/* RECURRING bits must be set for LSR workload with IOCTL_KGSL_RECURRING_COMMAND. */ +#define KGSL_CMDBATCH_END_OF_FRAME KGSL_CONTEXT_END_OF_FRAME +#define KGSL_CMDBATCH_SYNC KGSL_CONTEXT_SYNC +#define KGSL_CMDBATCH_PWR_CONSTRAINT KGSL_CONTEXT_PWR_CONSTRAINT +#define KGSL_CMDBATCH_SPARSE 0x1000 + #define KGSL_CMDBATCH_START_RECURRING 0x00100000 #define KGSL_CMDBATCH_STOP_RECURRING 0x00200000 -/* - * Reserve bits [16:19] and bits [28:31] for possible bits shared between - * contexts and command batches. Update this comment as new flags are added. - */ -/* - * gpu_command_object flags - these flags communicate the type of command or - * memory object being submitted for a GPU command - */ -/* Flags for GPU command objects */ #define KGSL_CMDLIST_IB 0x00000001U #define KGSL_CMDLIST_CTXTSWITCH_PREAMBLE 0x00000002U #define KGSL_CMDLIST_IB_PREAMBLE 0x00000004U -/* Flags for GPU command memory objects */ #define KGSL_OBJLIST_MEMOBJ 0x00000008U #define KGSL_OBJLIST_PROFILE 0x00000010U -/* Flags for GPU command sync points */ #define KGSL_CMD_SYNCPOINT_TYPE_TIMESTAMP 0 #define KGSL_CMD_SYNCPOINT_TYPE_FENCE 1 #define KGSL_CMD_SYNCPOINT_TYPE_TIMELINE 2 -/* --- Memory allocation flags --- */ -/* General allocation hints */ #define KGSL_MEMFLAGS_SECURE (1ULL << 3) #define KGSL_MEMFLAGS_GPUREADONLY (1ULL << 24) #define KGSL_MEMFLAGS_GPUWRITEONLY (1ULL << 25) #define KGSL_MEMFLAGS_FORCE_32BIT (1ULL << 32) -/* Flag for binding all the virt range to single phys data */ #define KGSL_SPARSE_BIND_MULTIPLE_TO_PHYS 0x400000000ULL #define KGSL_SPARSE_BIND 0x1ULL #define KGSL_SPARSE_UNBIND 0x2ULL -/* Memory caching hints */ #define KGSL_CACHEMODE_MASK 0x0C000000U #define KGSL_CACHEMODE_SHIFT 26 @@ -159,7 +109,6 @@ #define KGSL_MEMFLAGS_GUARD_PAGE (1ULL << 33) #define KGSL_MEMFLAGS_VBO (1ULL << 34) -/* Memory types for which allocations are made */ #define KGSL_MEMTYPE_MASK 0x0000FF00 #define KGSL_MEMTYPE_SHIFT 8 @@ -186,10 +135,6 @@ #define KGSL_MEMTYPE_MULTISAMPLE 20 #define KGSL_MEMTYPE_KERNEL 255 -/* - * Alignment hint, passed as the power of 2 exponent. - * i.e 4k (2^12) would be 12, 64k (2^16)would be 16. - */ #define KGSL_MEMALIGN_MASK 0x00FF0000 #define KGSL_MEMALIGN_SHIFT 16 @@ -198,22 +143,12 @@ enum kgsl_user_mem_type { KGSL_USER_MEM_TYPE_ASHMEM = 0x00000001, KGSL_USER_MEM_TYPE_ADDR = 0x00000002, KGSL_USER_MEM_TYPE_ION = 0x00000003, - /* - * ION type is retained for backwards compatibility but Ion buffers are - * dma-bufs so try to use that naming if we can - */ KGSL_USER_MEM_TYPE_DMABUF = 0x00000003, KGSL_USER_MEM_TYPE_MAX = 0x00000007, }; #define KGSL_MEMFLAGS_USERMEM_MASK 0x000000e0 #define KGSL_MEMFLAGS_USERMEM_SHIFT 5 -/* - * Unfortunately, enum kgsl_user_mem_type starts at 0 which does not - * leave a good value for allocated memory. In the flags we use - * 0 to indicate allocated memory and thus need to add 1 to the enum - * values. - */ #define KGSL_USERMEM_FLAG(x) (((x) + 1) << KGSL_MEMFLAGS_USERMEM_SHIFT) #define KGSL_MEMFLAGS_NOT_USERMEM 0 @@ -223,7 +158,6 @@ enum kgsl_user_mem_type { #define KGSL_MEMFLAGS_USERMEM_ADDR KGSL_USERMEM_FLAG(KGSL_USER_MEM_TYPE_ADDR) #define KGSL_MEMFLAGS_USERMEM_ION KGSL_USERMEM_FLAG(KGSL_USER_MEM_TYPE_ION) -/* --- generic KGSL flag values --- */ #define KGSL_FLAGS_NORMALMODE 0x00000000 #define KGSL_FLAGS_SAFEMODE 0x00000001 @@ -237,19 +171,14 @@ enum kgsl_user_mem_type { #define KGSL_FLAGS_SOFT_RESET 0x00000100 #define KGSL_FLAGS_PER_CONTEXT_TIMESTAMPS 0x00000200 -/* Server Side Sync Timeout in milliseconds */ #define KGSL_SYNCOBJ_SERVER_TIMEOUT 2000 -/* UBWC Modes */ #define KGSL_UBWC_NONE 0 #define KGSL_UBWC_1_0 1 #define KGSL_UBWC_2_0 2 #define KGSL_UBWC_3_0 3 #define KGSL_UBWC_4_0 4 -/* - * Reset status values for context - */ enum kgsl_ctx_reset_stat { KGSL_CTX_STAT_NO_ERROR = 0x00000000, KGSL_CTX_STAT_GUILTY_CONTEXT_RESET_EXT = 0x00000001, @@ -263,38 +192,13 @@ enum kgsl_ctx_reset_stat { struct kgsl_devinfo { unsigned int device_id; - /* - * chip revision id - * coreid:8 majorrev:8 minorrev:8 patch:8 - */ unsigned int chip_id; unsigned int mmu_enabled; unsigned long gmem_gpubaseaddr; - /* - * This field contains the adreno revision - * number 200, 205, 220, etc... - */ unsigned int gpu_id; __kernel_size_t gmem_sizebytes; }; -/* - * struct kgsl_devmemstore - this structure defines the region of memory - * that can be mmap()ed from this driver. The timestamp fields are volatile - * because they are written by the GPU - * @soptimestamp: Start of pipeline timestamp written by GPU before the - * commands in concern are processed - * @sbz: Unused, kept for 8 byte alignment - * @eoptimestamp: End of pipeline timestamp written by GPU after the - * commands in concern are processed - * @sbz2: Unused, kept for 8 byte alignment - * @preempted: Indicates if the context was preempted - * @sbz3: Unused, kept for 8 byte alignment - * @ref_wait_ts: Timestamp on which to generate interrupt, unused now. - * @sbz4: Unused, kept for 8 byte alignment - * @current_context: The current context the GPU is working on - * @sbz5: Unused, kept for 8 byte alignment - */ struct kgsl_devmemstore { volatile unsigned int soptimestamp; unsigned int sbz; @@ -312,14 +216,12 @@ struct kgsl_devmemstore { ((ctxt_id)*sizeof(struct kgsl_devmemstore) + \ offsetof(struct kgsl_devmemstore, field)) -/* timestamp id*/ enum kgsl_timestamp_type { - KGSL_TIMESTAMP_CONSUMED = 0x00000001, /* start-of-pipeline timestamp */ - KGSL_TIMESTAMP_RETIRED = 0x00000002, /* end-of-pipeline timestamp*/ + KGSL_TIMESTAMP_CONSUMED = 0x00000001, + KGSL_TIMESTAMP_RETIRED = 0x00000002, KGSL_TIMESTAMP_QUEUED = 0x00000003, }; -/* property types - used with kgsl_device_getproperty */ #define KGSL_PROP_DEVICE_INFO 0x1 #define KGSL_PROP_DEVICE_SHADOW 0x2 #define KGSL_PROP_DEVICE_POWER 0x3 @@ -352,30 +254,13 @@ enum kgsl_timestamp_type { #define KGSL_PROP_VK_DEVICE_ID 0x2A #define KGSL_PROP_IS_LPAC_ENABLED 0x2B -/* - * kgsl_capabilities_properties returns a list of supported properties. - * If the user passes 0 for 'count' the kernel will set it to the number of - * supported properties. The list is expected to be 'count * sizeof(__u32)' - * bytes long. The kernel will return the actual number of entries copied into - * list via 'count'. - */ struct kgsl_capabilities_properties { __u64 list; __u32 count; }; -/* - * KGSL_QUERY_CAPS_PROPERTIES returns a list of the valid properties in the - * kernel. The subtype data should be struct kgsl_capabilities_properties - */ #define KGSL_QUERY_CAPS_PROPERTIES 1 -/* - * kgsl_capabilities allows the user to query kernel capabilities. The 'data' - * type should be set appropriately for the querytype (see above). Pass 0 to - * 'size' and the kernel will set it to the expected size of 'data' that is - * appropriate for querytype (in bytes). - */ struct kgsl_capabilities { __u64 data; __u64 size; @@ -385,7 +270,7 @@ struct kgsl_capabilities { struct kgsl_shadowprop { unsigned long gpuaddr; __kernel_size_t size; - unsigned int flags; /* contains KGSL_FLAGS_ values */ + unsigned int flags; }; struct kgsl_qdss_stm_prop { @@ -437,10 +322,8 @@ struct kgsl_gpu_model { char gpu_model[32]; }; -/* Context property sub types */ #define KGSL_CONTEXT_PROP_FAULTS 1 -/* Performance counter groups */ #define KGSL_PERFCOUNTER_GROUP_CP 0x0 #define KGSL_PERFCOUNTER_GROUP_RBBM 0x1 @@ -502,7 +385,6 @@ struct kgsl_gpu_model { #define KGSL_PERFCOUNTER_NOT_USED 0xFFFFFFFF #define KGSL_PERFCOUNTER_BROKEN 0xFFFFFFFE -/* structure holds list of ibs */ struct kgsl_ibdesc { unsigned long gpuaddr; unsigned long __pad; @@ -510,21 +392,6 @@ struct kgsl_ibdesc { unsigned int ctrl; }; -/** - * struct kgsl_cmdbatch_profiling_buffer - * @wall_clock_s: Ringbuffer submission time (seconds). - * If KGSL_CMDBATCH_PROFILING_KTIME is set, time is provided - * in kernel clocks, otherwise wall clock time is used. - * @wall_clock_ns: Ringbuffer submission time (nanoseconds). - * If KGSL_CMDBATCH_PROFILING_KTIME is set time is provided - * in kernel clocks, otherwise wall clock time is used. - * @gpu_ticks_queued: GPU ticks at ringbuffer submission - * @gpu_ticks_submitted: GPU ticks when starting cmdbatch execution - * @gpu_ticks_retired: GPU ticks when finishing cmdbatch execution - * - * This structure defines the profiling buffer used to measure cmdbatch - * execution time - */ struct kgsl_cmdbatch_profiling_buffer { __u64 wall_clock_s; __u64 wall_clock_ns; @@ -533,20 +400,8 @@ struct kgsl_cmdbatch_profiling_buffer { __u64 gpu_ticks_retired; }; -/* ioctls */ #define KGSL_IOC_TYPE 0x09 -/* - * get misc info about the GPU - * type should be a value from enum kgsl_property_type - * value points to a structure that varies based on type - * sizebytes is sizeof() that structure - * for KGSL_PROP_DEVICE_INFO, use struct kgsl_devinfo - * this structure contaings hardware versioning info. - * for KGSL_PROP_DEVICE_SHADOW, use struct kgsl_shadowprop - * this is used to find mmap() offset and sizes for mapping - * struct kgsl_memstore into userspace. - */ struct kgsl_device_getproperty { unsigned int type; void __user *value; @@ -556,12 +411,7 @@ struct kgsl_device_getproperty { #define IOCTL_KGSL_DEVICE_GETPROPERTY \ _IOWR(KGSL_IOC_TYPE, 0x2, struct kgsl_device_getproperty) -/* IOCTL_KGSL_DEVICE_READ (0x3) - removed 03/2012 - */ -/* block until the GPU has executed past a given timestamp - * timeout is in milliseconds. - */ struct kgsl_device_waittimestamp { unsigned int timestamp; unsigned int timeout; @@ -579,35 +429,20 @@ struct kgsl_device_waittimestamp_ctxtid { #define IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID \ _IOW(KGSL_IOC_TYPE, 0x7, struct kgsl_device_waittimestamp_ctxtid) -/* DEPRECATED: issue indirect commands to the GPU. - * drawctxt_id must have been created with IOCTL_KGSL_DRAWCTXT_CREATE - * ibaddr and sizedwords must specify a subset of a buffer created - * with IOCTL_KGSL_SHAREDMEM_FROM_PMEM - * flags may be a mask of KGSL_CONTEXT_ values - * timestamp is a returned counter value which can be passed to - * other ioctls to determine when the commands have been executed by - * the GPU. - * - * This function is deprecated - consider using IOCTL_KGSL_SUBMIT_COMMANDS - * instead - */ struct kgsl_ringbuffer_issueibcmds { unsigned int drawctxt_id; unsigned long ibdesc_addr; unsigned int numibs; - unsigned int timestamp; /*output param */ + unsigned int timestamp; unsigned int flags; }; #define IOCTL_KGSL_RINGBUFFER_ISSUEIBCMDS \ _IOWR(KGSL_IOC_TYPE, 0x10, struct kgsl_ringbuffer_issueibcmds) -/* read the most recently executed timestamp value - * type should be a value from enum kgsl_timestamp_type - */ struct kgsl_cmdstream_readtimestamp { unsigned int type; - unsigned int timestamp; /*output param */ + unsigned int timestamp; }; #define IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_OLD \ @@ -616,11 +451,6 @@ struct kgsl_cmdstream_readtimestamp { #define IOCTL_KGSL_CMDSTREAM_READTIMESTAMP \ _IOWR(KGSL_IOC_TYPE, 0x11, struct kgsl_cmdstream_readtimestamp) -/* free memory when the GPU reaches a given timestamp. - * gpuaddr specify a memory region created by a - * IOCTL_KGSL_SHAREDMEM_FROM_PMEM call - * type should be a value from enum kgsl_timestamp_type - */ struct kgsl_cmdstream_freememontimestamp { unsigned long gpuaddr; unsigned int type; @@ -630,28 +460,18 @@ struct kgsl_cmdstream_freememontimestamp { #define IOCTL_KGSL_CMDSTREAM_FREEMEMONTIMESTAMP \ _IOW(KGSL_IOC_TYPE, 0x12, struct kgsl_cmdstream_freememontimestamp) -/* - * Previous versions of this header had incorrectly defined - * IOCTL_KGSL_CMDSTREAM_FREEMEMONTIMESTAMP as a read-only ioctl instead - * of a write only ioctl. To ensure binary compatibility, the following - * #define will be used to intercept the incorrect ioctl - */ #define IOCTL_KGSL_CMDSTREAM_FREEMEMONTIMESTAMP_OLD \ _IOR(KGSL_IOC_TYPE, 0x12, struct kgsl_cmdstream_freememontimestamp) -/* create a draw context, which is used to preserve GPU state. - * The flags field may contain a mask KGSL_CONTEXT_* values - */ struct kgsl_drawctxt_create { unsigned int flags; - unsigned int drawctxt_id; /*output param */ + unsigned int drawctxt_id; }; #define IOCTL_KGSL_DRAWCTXT_CREATE \ _IOWR(KGSL_IOC_TYPE, 0x13, struct kgsl_drawctxt_create) -/* destroy a draw context */ struct kgsl_drawctxt_destroy { unsigned int drawctxt_id; }; @@ -659,16 +479,12 @@ struct kgsl_drawctxt_destroy { #define IOCTL_KGSL_DRAWCTXT_DESTROY \ _IOW(KGSL_IOC_TYPE, 0x14, struct kgsl_drawctxt_destroy) -/* - * add a block of pmem, fb, ashmem or user allocated address - * into the GPU address space - */ struct kgsl_map_user_mem { int fd; - unsigned long gpuaddr; /*output param */ + unsigned long gpuaddr; __kernel_size_t len; __kernel_size_t offset; - unsigned long hostptr; /*input param */ + unsigned long hostptr; enum kgsl_user_mem_type memtype; unsigned int flags; }; @@ -679,7 +495,7 @@ struct kgsl_map_user_mem { struct kgsl_cmdstream_readtimestamp_ctxtid { unsigned int context_id; unsigned int type; - unsigned int timestamp; /*output param */ + unsigned int timestamp; }; #define IOCTL_KGSL_CMDSTREAM_READTIMESTAMP_CTXTID \ @@ -696,10 +512,9 @@ struct kgsl_cmdstream_freememontimestamp_ctxtid { _IOW(KGSL_IOC_TYPE, 0x17, \ struct kgsl_cmdstream_freememontimestamp_ctxtid) -/* add a block of pmem or fb into the GPU address space */ struct kgsl_sharedmem_from_pmem { int pmem_fd; - unsigned long gpuaddr; /*output param */ + unsigned long gpuaddr; unsigned int len; unsigned int offset; }; @@ -707,7 +522,6 @@ struct kgsl_sharedmem_from_pmem { #define IOCTL_KGSL_SHAREDMEM_FROM_PMEM \ _IOWR(KGSL_IOC_TYPE, 0x20, struct kgsl_sharedmem_from_pmem) -/* remove memory from the GPU's address space */ struct kgsl_sharedmem_free { unsigned long gpuaddr; }; @@ -757,15 +571,10 @@ struct kgsl_bind_gmem_shadow { #define IOCTL_KGSL_DRAWCTXT_BIND_GMEM_SHADOW \ _IOW(KGSL_IOC_TYPE, 0x22, struct kgsl_bind_gmem_shadow) -/* add a block of memory into the GPU address space */ -/* - * IOCTL_KGSL_SHAREDMEM_FROM_VMALLOC deprecated 09/2012 - * use IOCTL_KGSL_GPUMEM_ALLOC instead - */ struct kgsl_sharedmem_from_vmalloc { - unsigned long gpuaddr; /*output param */ + unsigned long gpuaddr; unsigned int hostptr; unsigned int flags; }; @@ -773,13 +582,6 @@ struct kgsl_sharedmem_from_vmalloc { #define IOCTL_KGSL_SHAREDMEM_FROM_VMALLOC \ _IOWR(KGSL_IOC_TYPE, 0x23, struct kgsl_sharedmem_from_vmalloc) -/* - * This is being deprecated in favor of IOCTL_KGSL_GPUMEM_CACHE_SYNC which - * supports both directions (flush and invalidate). This code will still - * work, but by definition it will do a flush of the cache which might not be - * what you want to have happen on a buffer following a GPU operation. It is - * safer to go with IOCTL_KGSL_GPUMEM_CACHE_SYNC - */ #define IOCTL_KGSL_SHAREDMEM_FLUSH_CACHE \ _IOW(KGSL_IOC_TYPE, 0x24, struct kgsl_sharedmem_free) @@ -795,13 +597,12 @@ struct kgsl_drawctxt_set_bin_base_offset { enum kgsl_cmdwindow_type { KGSL_CMDWINDOW_MIN = 0x00000000, KGSL_CMDWINDOW_2D = 0x00000000, - KGSL_CMDWINDOW_3D = 0x00000001, /* legacy */ + KGSL_CMDWINDOW_3D = 0x00000001, KGSL_CMDWINDOW_MMU = 0x00000002, KGSL_CMDWINDOW_ARBITER = 0x000000FF, KGSL_CMDWINDOW_MAX = 0x000000FF, }; -/* write to the command window */ struct kgsl_cmdwindow_write { enum kgsl_cmdwindow_type target; unsigned int addr; @@ -812,7 +613,7 @@ struct kgsl_cmdwindow_write { _IOW(KGSL_IOC_TYPE, 0x2e, struct kgsl_cmdwindow_write) struct kgsl_gpumem_alloc { - unsigned long gpuaddr; /* output param */ + unsigned long gpuaddr; __kernel_size_t size; unsigned int flags; }; @@ -823,49 +624,38 @@ struct kgsl_gpumem_alloc { struct kgsl_cff_syncmem { unsigned long gpuaddr; __kernel_size_t len; - unsigned int __pad[2]; /* For future binary compatibility */ + unsigned int __pad[2]; }; #define IOCTL_KGSL_CFF_SYNCMEM \ _IOW(KGSL_IOC_TYPE, 0x30, struct kgsl_cff_syncmem) -/* - * A timestamp event allows the user space to register an action following an - * expired timestamp. Note IOCTL_KGSL_TIMESTAMP_EVENT has been redefined to - * _IOWR to support fences which need to return a fd for the priv parameter. - */ struct kgsl_timestamp_event { - int type; /* Type of event (see list below) */ - unsigned int timestamp; /* Timestamp to trigger event on */ - unsigned int context_id; /* Context for the timestamp */ - void __user *priv; /* Pointer to the event specific blob */ - __kernel_size_t len; /* Size of the event specific blob */ + int type; + unsigned int timestamp; + unsigned int context_id; + void __user *priv; + __kernel_size_t len; }; #define IOCTL_KGSL_TIMESTAMP_EVENT_OLD \ _IOW(KGSL_IOC_TYPE, 0x31, struct kgsl_timestamp_event) -/* A genlock timestamp event releases an existing lock on timestamp expire */ #define KGSL_TIMESTAMP_EVENT_GENLOCK 1 struct kgsl_timestamp_event_genlock { - int handle; /* Handle of the genlock lock to release */ + int handle; }; -/* A fence timestamp event releases an existing lock on timestamp expire */ #define KGSL_TIMESTAMP_EVENT_FENCE 2 struct kgsl_timestamp_event_fence { - int fence_fd; /* Fence to signal */ + int fence_fd; }; -/* - * Set a property within the kernel. Uses the same structure as - * IOCTL_KGSL_GETPROPERTY - */ #define IOCTL_KGSL_SETPROPERTY \ _IOW(KGSL_IOC_TYPE, 0x32, struct kgsl_device_getproperty) @@ -873,70 +663,26 @@ struct kgsl_timestamp_event_fence { #define IOCTL_KGSL_TIMESTAMP_EVENT \ _IOWR(KGSL_IOC_TYPE, 0x33, struct kgsl_timestamp_event) -/** - * struct kgsl_gpumem_alloc_id - argument to IOCTL_KGSL_GPUMEM_ALLOC_ID - * @id: returned id value for this allocation. - * @flags: mask of KGSL_MEM* values requested and actual flags on return. - * @size: requested size of the allocation and actual size on return. - * @mmapsize: returned size to pass to mmap() which may be larger than 'size' - * @gpuaddr: returned GPU address for the allocation - * - * Allocate memory for access by the GPU. The flags and size fields are echoed - * back by the kernel, so that the caller can know if the request was - * adjusted. - * - * Supported flags: - * KGSL_MEMFLAGS_GPUREADONLY: the GPU will be unable to write to the buffer - * KGSL_MEMTYPE*: usage hint for debugging aid - * KGSL_MEMALIGN*: alignment hint, may be ignored or adjusted by the kernel. - * KGSL_MEMFLAGS_USE_CPU_MAP: If set on call and return, the returned GPU - * address will be 0. Calling mmap() will set the GPU address. - */ struct kgsl_gpumem_alloc_id { unsigned int id; unsigned int flags; __kernel_size_t size; __kernel_size_t mmapsize; unsigned long gpuaddr; -/* private: reserved for future use*/ unsigned long __pad[2]; }; #define IOCTL_KGSL_GPUMEM_ALLOC_ID \ _IOWR(KGSL_IOC_TYPE, 0x34, struct kgsl_gpumem_alloc_id) -/** - * struct kgsl_gpumem_free_id - argument to IOCTL_KGSL_GPUMEM_FREE_ID - * @id: GPU allocation id to free - * - * Free an allocation by id, in case a GPU address has not been assigned or - * is unknown. Freeing an allocation by id with this ioctl or by GPU address - * with IOCTL_KGSL_SHAREDMEM_FREE are equivalent. - */ struct kgsl_gpumem_free_id { unsigned int id; -/* private: reserved for future use*/ unsigned int __pad; }; #define IOCTL_KGSL_GPUMEM_FREE_ID \ _IOWR(KGSL_IOC_TYPE, 0x35, struct kgsl_gpumem_free_id) -/** - * struct kgsl_gpumem_get_info - argument to IOCTL_KGSL_GPUMEM_GET_INFO - * @gpuaddr: GPU address to query. Also set on return. - * @id: GPU allocation id to query. Also set on return. - * @flags: returned mask of KGSL_MEM* values. - * @size: returned size of the allocation. - * @mmapsize: returned size to pass mmap(), which may be larger than 'size' - * @useraddr: returned address of the userspace mapping for this buffer - * - * This ioctl allows querying of all user visible attributes of an existing - * allocation, by either the GPU address or the id returned by a previous - * call to IOCTL_KGSL_GPUMEM_ALLOC_ID. Legacy allocation ioctls may not - * return all attributes so this ioctl can be used to look them up if needed. - * - */ struct kgsl_gpumem_get_info { unsigned long gpuaddr; unsigned int id; @@ -944,27 +690,12 @@ struct kgsl_gpumem_get_info { __kernel_size_t size; __kernel_size_t mmapsize; unsigned long useraddr; -/* private: reserved for future use*/ unsigned long __pad[4]; }; #define IOCTL_KGSL_GPUMEM_GET_INFO\ _IOWR(KGSL_IOC_TYPE, 0x36, struct kgsl_gpumem_get_info) -/** - * struct kgsl_gpumem_sync_cache - argument to IOCTL_KGSL_GPUMEM_SYNC_CACHE - * @gpuaddr: GPU address of the buffer to sync. - * @id: id of the buffer to sync. Either gpuaddr or id is sufficient. - * @op: a mask of KGSL_GPUMEM_CACHE_* values - * @offset: offset into the buffer - * @length: number of bytes starting from offset to perform - * the cache operation on - * - * Sync the L2 cache for memory headed to and from the GPU - this replaces - * KGSL_SHAREDMEM_FLUSH_CACHE since it can handle cache management for both - * directions - * - */ struct kgsl_gpumem_sync_cache { unsigned long gpuaddr; unsigned int id; @@ -982,101 +713,45 @@ struct kgsl_gpumem_sync_cache { #define KGSL_GPUMEM_CACHE_FLUSH \ (KGSL_GPUMEM_CACHE_CLEAN | KGSL_GPUMEM_CACHE_INV) -/* Flag to ensure backwards compatibility of kgsl_gpumem_sync_cache struct */ #define KGSL_GPUMEM_CACHE_RANGE (1 << 31U) #define IOCTL_KGSL_GPUMEM_SYNC_CACHE \ _IOW(KGSL_IOC_TYPE, 0x37, struct kgsl_gpumem_sync_cache) -/** - * struct kgsl_perfcounter_get - argument to IOCTL_KGSL_PERFCOUNTER_GET - * @groupid: Performance counter group ID - * @countable: Countable to select within the group - * @offset: Return offset of the reserved LO counter - * @offset_hi: Return offset of the reserved HI counter - * - * Get an available performance counter from a specified groupid. The offset - * of the performance counter will be returned after successfully assigning - * the countable to the counter for the specified group. An error will be - * returned and an offset of 0 if the groupid is invalid or there are no - * more counters left. After successfully getting a perfcounter, the user - * must call kgsl_perfcounter_put(groupid, contable) when finished with - * the perfcounter to clear up perfcounter resources. - * - */ struct kgsl_perfcounter_get { unsigned int groupid; unsigned int countable; unsigned int offset; unsigned int offset_hi; -/* private: reserved for future use */ - unsigned int __pad; /* For future binary compatibility */ + + unsigned int __pad; }; #define IOCTL_KGSL_PERFCOUNTER_GET \ _IOWR(KGSL_IOC_TYPE, 0x38, struct kgsl_perfcounter_get) -/** - * struct kgsl_perfcounter_put - argument to IOCTL_KGSL_PERFCOUNTER_PUT - * @groupid: Performance counter group ID - * @countable: Countable to release within the group - * - * Put an allocated performance counter to allow others to have access to the - * resource that was previously taken. This is only to be called after - * successfully getting a performance counter from kgsl_perfcounter_get(). - * - */ struct kgsl_perfcounter_put { unsigned int groupid; unsigned int countable; -/* private: reserved for future use */ - unsigned int __pad[2]; /* For future binary compatibility */ + + unsigned int __pad[2]; }; #define IOCTL_KGSL_PERFCOUNTER_PUT \ _IOW(KGSL_IOC_TYPE, 0x39, struct kgsl_perfcounter_put) -/** - * struct kgsl_perfcounter_query - argument to IOCTL_KGSL_PERFCOUNTER_QUERY - * @groupid: Performance counter group ID - * @countable: Return active countables array - * @size: Size of active countables array - * @max_counters: Return total number counters for the group ID - * - * Query the available performance counters given a groupid. The array - * *countables is used to return the current active countables in counters. - * The size of the array is passed in so the kernel will only write at most - * size or counter->size for the group id. The total number of available - * counters for the group ID is returned in max_counters. - * If the array or size passed in are invalid, then only the maximum number - * of counters will be returned, no data will be written to *countables. - * If the groupid is invalid an error code will be returned. - * - */ struct kgsl_perfcounter_query { unsigned int groupid; - /* Array to return the current countable for up to size counters */ unsigned int __user *countables; unsigned int count; unsigned int max_counters; -/* private: reserved for future use */ - unsigned int __pad[2]; /* For future binary compatibility */ + + unsigned int __pad[2]; }; #define IOCTL_KGSL_PERFCOUNTER_QUERY \ _IOWR(KGSL_IOC_TYPE, 0x3A, struct kgsl_perfcounter_query) -/** - * struct kgsl_perfcounter_query - argument to IOCTL_KGSL_PERFCOUNTER_QUERY - * @groupid: Performance counter group IDs - * @countable: Performance counter countable IDs - * @value: Return performance counter reads - * @size: Size of all arrays (groupid/countable pair and return value) - * - * Read in the current value of a performance counter given by the groupid - * and countable. - * - */ struct kgsl_perfcounter_read_group { unsigned int groupid; @@ -1087,44 +762,23 @@ struct kgsl_perfcounter_read_group { struct kgsl_perfcounter_read { struct kgsl_perfcounter_read_group __user *reads; unsigned int count; -/* private: reserved for future use */ - unsigned int __pad[2]; /* For future binary compatibility */ + + unsigned int __pad[2]; }; #define IOCTL_KGSL_PERFCOUNTER_READ \ _IOWR(KGSL_IOC_TYPE, 0x3B, struct kgsl_perfcounter_read) -/* - * struct kgsl_gpumem_sync_cache_bulk - argument to - * IOCTL_KGSL_GPUMEM_SYNC_CACHE_BULK - * @id_list: list of GPU buffer ids of the buffers to sync - * @count: number of GPU buffer ids in id_list - * @op: a mask of KGSL_GPUMEM_CACHE_* values - * - * Sync the cache for memory headed to and from the GPU. Certain - * optimizations can be made on the cache operation based on the total - * size of the working set of memory to be managed. - */ struct kgsl_gpumem_sync_cache_bulk { unsigned int __user *id_list; unsigned int count; unsigned int op; -/* private: reserved for future use */ - unsigned int __pad[2]; /* For future binary compatibility */ + + unsigned int __pad[2]; }; #define IOCTL_KGSL_GPUMEM_SYNC_CACHE_BULK \ _IOWR(KGSL_IOC_TYPE, 0x3C, struct kgsl_gpumem_sync_cache_bulk) -/* - * struct kgsl_cmd_syncpoint_timestamp - * @context_id: ID of a KGSL context - * @timestamp: GPU timestamp - * - * This structure defines a syncpoint comprising a context/timestamp pair. A - * list of these may be passed by IOCTL_KGSL_SUBMIT_COMMANDS to define - * dependencies that must be met before the command can be submitted to the - * hardware - */ struct kgsl_cmd_syncpoint_timestamp { unsigned int context_id; unsigned int timestamp; @@ -1134,60 +788,22 @@ struct kgsl_cmd_syncpoint_fence { int fd; }; -/* - * struct kgsl_cmd_syncpoint_timeline - * @timelines: Address of an array of &struct kgsl_timeline_val - * @count: Number of entries in @timelines - * @timelines_size: Size of each entry in @timelines - * - * Define a syncpoint for a number of timelines. This syncpoint will - * be satisfied when all of the specified timelines are signaled. - */ struct kgsl_cmd_syncpoint_timeline { __u64 timelines; __u32 count; __u32 timelines_size; }; -/** - * struct kgsl_cmd_syncpoint - Define a sync point for a command batch - * @type: type of sync point defined here - * @priv: Pointer to the type specific buffer - * @size: Size of the type specific buffer - * - * This structure contains pointers defining a specific command sync point. - * The pointer and size should point to a type appropriate structure. - */ struct kgsl_cmd_syncpoint { int type; void __user *priv; __kernel_size_t size; }; -/* Flag to indicate that the cmdlist may contain memlists */ #define KGSL_IBDESC_MEMLIST 0x1 -/* Flag to point out the cmdbatch profiling buffer in the memlist */ #define KGSL_IBDESC_PROFILING_BUFFER 0x2 -/** - * struct kgsl_submit_commands - Argument to IOCTL_KGSL_SUBMIT_COMMANDS - * @context_id: KGSL context ID that owns the commands - * @flags: - * @cmdlist: User pointer to a list of kgsl_ibdesc structures - * @numcmds: Number of commands listed in cmdlist - * @synclist: User pointer to a list of kgsl_cmd_syncpoint structures - * @numsyncs: Number of sync points listed in synclist - * @timestamp: On entry the a user defined timestamp, on exist the timestamp - * assigned to the command batch - * - * This structure specifies a command to send to the GPU hardware. This is - * similar to kgsl_issueibcmds expect that it doesn't support the legacy way to - * submit IB lists and it adds sync points to block the IB until the - * dependencies are satisified. This entry point is the new and preferred way - * to submit commands to the GPU. The memory list can be used to specify all - * memory that is referrenced in the current set of commands. - */ struct kgsl_submit_commands { unsigned int context_id; @@ -1197,20 +813,12 @@ struct kgsl_submit_commands { struct kgsl_cmd_syncpoint __user *synclist; unsigned int numsyncs; unsigned int timestamp; -/* private: reserved for future use */ unsigned int __pad[4]; }; #define IOCTL_KGSL_SUBMIT_COMMANDS \ _IOWR(KGSL_IOC_TYPE, 0x3D, struct kgsl_submit_commands) -/** - * struct kgsl_device_constraint - device constraint argument - * @context_id: KGSL context ID - * @type: type of constraint i.e pwrlevel/none - * @data: constraint data - * @size: size of the constraint data - */ struct kgsl_device_constraint { unsigned int type; unsigned int context_id; @@ -1218,103 +826,55 @@ struct kgsl_device_constraint { __kernel_size_t size; }; -/* Constraint Type*/ #define KGSL_CONSTRAINT_NONE 0 #define KGSL_CONSTRAINT_PWRLEVEL 1 -/* L3 constraint Type */ #define KGSL_CONSTRAINT_L3_NONE 2 #define KGSL_CONSTRAINT_L3_PWRLEVEL 3 -/* PWRLEVEL constraint level*/ -/* set to min frequency */ #define KGSL_CONSTRAINT_PWR_MIN 0 -/* set to max frequency */ #define KGSL_CONSTRAINT_PWR_MAX 1 struct kgsl_device_constraint_pwrlevel { unsigned int level; }; -/** - * struct kgsl_syncsource_create - Argument to IOCTL_KGSL_SYNCSOURCE_CREATE - * @id: returned id for the syncsource that was created. - * - * This ioctl creates a userspace sync timeline. - */ struct kgsl_syncsource_create { unsigned int id; -/* private: reserved for future use */ unsigned int __pad[3]; }; #define IOCTL_KGSL_SYNCSOURCE_CREATE \ _IOWR(KGSL_IOC_TYPE, 0x40, struct kgsl_syncsource_create) -/** - * struct kgsl_syncsource_destroy - Argument to IOCTL_KGSL_SYNCSOURCE_DESTROY - * @id: syncsource id to destroy - * - * This ioctl creates a userspace sync timeline. - */ struct kgsl_syncsource_destroy { unsigned int id; -/* private: reserved for future use */ unsigned int __pad[3]; }; #define IOCTL_KGSL_SYNCSOURCE_DESTROY \ _IOWR(KGSL_IOC_TYPE, 0x41, struct kgsl_syncsource_destroy) -/** - * struct kgsl_syncsource_create_fence - Argument to - * IOCTL_KGSL_SYNCSOURCE_CREATE_FENCE - * @id: syncsource id - * @fence_fd: returned sync_fence fd - * - * Create a fence that may be signaled by userspace by calling - * IOCTL_KGSL_SYNCSOURCE_SIGNAL_FENCE. There are no order dependencies between - * these fences. - */ struct kgsl_syncsource_create_fence { unsigned int id; int fence_fd; -/* private: reserved for future use */ unsigned int __pad[4]; }; -/** - * struct kgsl_syncsource_signal_fence - Argument to - * IOCTL_KGSL_SYNCSOURCE_SIGNAL_FENCE - * @id: syncsource id - * @fence_fd: sync_fence fd to signal - * - * Signal a fence that was created by a IOCTL_KGSL_SYNCSOURCE_CREATE_FENCE - * call using the same syncsource id. This allows a fence to be shared - * to other processes but only signaled by the process owning the fd - * used to create the fence. - */ #define IOCTL_KGSL_SYNCSOURCE_CREATE_FENCE \ _IOWR(KGSL_IOC_TYPE, 0x42, struct kgsl_syncsource_create_fence) struct kgsl_syncsource_signal_fence { unsigned int id; int fence_fd; -/* private: reserved for future use */ unsigned int __pad[4]; }; #define IOCTL_KGSL_SYNCSOURCE_SIGNAL_FENCE \ _IOWR(KGSL_IOC_TYPE, 0x43, struct kgsl_syncsource_signal_fence) -/** - * struct kgsl_cff_sync_gpuobj - Argument to IOCTL_KGSL_CFF_SYNC_GPUOBJ - * @offset: Offset into the GPU object to sync - * @length: Number of bytes to sync - * @id: ID of the GPU object to sync - */ struct kgsl_cff_sync_gpuobj { __u64 offset; __u64 length; @@ -1324,16 +884,6 @@ struct kgsl_cff_sync_gpuobj { #define IOCTL_KGSL_CFF_SYNC_GPUOBJ \ _IOW(KGSL_IOC_TYPE, 0x44, struct kgsl_cff_sync_gpuobj) -/** - * struct kgsl_gpuobj_alloc - Argument to IOCTL_KGSL_GPUOBJ_ALLOC - * @size: Size in bytes of the object to allocate - * @flags: mask of KGSL_MEMFLAG_* bits - * @va_len: Size in bytes of the virtual region to allocate - * @mmapsize: Returns the mmap() size of the object - * @id: Returns the GPU object ID of the new object - * @metadata_len: Length of the metdata to copy from the user - * @metadata: Pointer to the user specified metadata to store for the object - */ struct kgsl_gpuobj_alloc { __u64 size; __u64 flags; @@ -1344,22 +894,11 @@ struct kgsl_gpuobj_alloc { __u64 metadata; }; -/* Let the user know that this header supports the gpuobj metadata */ #define KGSL_GPUOBJ_ALLOC_METADATA_MAX 64 #define IOCTL_KGSL_GPUOBJ_ALLOC \ _IOWR(KGSL_IOC_TYPE, 0x45, struct kgsl_gpuobj_alloc) -/** - * struct kgsl_gpuobj_free - Argument to IOCTL_KGLS_GPUOBJ_FREE - * @flags: Mask of: KGSL_GUPOBJ_FREE_ON_EVENT - * @priv: Pointer to the private object if KGSL_GPUOBJ_FREE_ON_EVENT is - * specified - * @id: ID of the GPU object to free - * @type: If KGSL_GPUOBJ_FREE_ON_EVENT is specified, the type of asynchronous - * event to free on - * @len: Length of the data passed in priv - */ struct kgsl_gpuobj_free { __u64 flags; __u64 __user priv; @@ -1373,21 +912,11 @@ struct kgsl_gpuobj_free { #define KGSL_GPU_EVENT_TIMESTAMP 1 #define KGSL_GPU_EVENT_FENCE 2 -/** - * struct kgsl_gpu_event_timestamp - Specifies a timestamp event to free a GPU - * object on - * @context_id: ID of the timestamp event to wait for - * @timestamp: Timestamp of the timestamp event to wait for - */ struct kgsl_gpu_event_timestamp { unsigned int context_id; unsigned int timestamp; }; -/** - * struct kgsl_gpu_event_fence - Specifies a fence ID to to free a GPU object on - * @fd: File descriptor for the fence - */ struct kgsl_gpu_event_fence { int fd; }; @@ -1395,15 +924,6 @@ struct kgsl_gpu_event_fence { #define IOCTL_KGSL_GPUOBJ_FREE \ _IOW(KGSL_IOC_TYPE, 0x46, struct kgsl_gpuobj_free) -/** - * struct kgsl_gpuobj_info - argument to IOCTL_KGSL_GPUOBJ_INFO - * @gpuaddr: GPU address of the object - * @flags: Current flags for the object - * @size: Size of the object - * @va_len: VA size of the object - * @va_addr: Virtual address of the object (if it is mapped) - * id - GPU object ID of the object to query - */ struct kgsl_gpuobj_info { __u64 gpuaddr; __u64 flags; @@ -1417,14 +937,6 @@ struct kgsl_gpuobj_info { _IOWR(KGSL_IOC_TYPE, 0x47, struct kgsl_gpuobj_info) -/** - * struct kgsl_gpuobj_import - argument to IOCTL_KGSL_GPUOBJ_IMPORT - * @priv: Pointer to the private data for the import type - * @priv_len: Length of the private data - * @flags: Mask of KGSL_MEMFLAG_ flags - * @type: Type of the import (KGSL_USER_MEM_TYPE_*) - * @id: Returns the ID of the new GPU object - */ struct kgsl_gpuobj_import { __u64 __user priv; __u64 priv_len; @@ -1433,18 +945,10 @@ struct kgsl_gpuobj_import { unsigned int id; }; -/** - * struct kgsl_gpuobj_import_dma_buf - import a dmabuf object - * @fd: File descriptor for the dma-buf object - */ struct kgsl_gpuobj_import_dma_buf { int fd; }; -/** - * struct kgsl_gpuobj_import_useraddr - import an object based on a useraddr - * @virtaddr: Virtual address of the object to import - */ struct kgsl_gpuobj_import_useraddr { __u64 virtaddr; }; @@ -1452,13 +956,6 @@ struct kgsl_gpuobj_import_useraddr { #define IOCTL_KGSL_GPUOBJ_IMPORT \ _IOWR(KGSL_IOC_TYPE, 0x48, struct kgsl_gpuobj_import) -/** - * struct kgsl_gpuobj_sync_obj - Individual GPU object to sync - * @offset: Offset within the GPU object to sync - * @length: Number of bytes to sync - * @id: ID of the GPU object to sync - * @op: Cache operation to execute - */ struct kgsl_gpuobj_sync_obj { __u64 offset; @@ -1467,12 +964,6 @@ struct kgsl_gpuobj_sync_obj { unsigned int op; }; -/** - * struct kgsl_gpuobj_sync - Argument for IOCTL_KGSL_GPUOBJ_SYNC - * @objs: Pointer to an array of kgsl_gpuobj_sync_obj structs - * @obj_len: Size of each item in the array - * @count: Number of items in the array - */ struct kgsl_gpuobj_sync { __u64 __user objs; @@ -1483,14 +974,6 @@ struct kgsl_gpuobj_sync { #define IOCTL_KGSL_GPUOBJ_SYNC \ _IOW(KGSL_IOC_TYPE, 0x49, struct kgsl_gpuobj_sync) -/** - * struct kgsl_command_object - GPU command object - * @offset: GPU address offset of the object - * @gpuaddr: GPU address of the object - * @size: Size of the object - * @flags: Current flags for the object - * @id - GPU command object ID - */ struct kgsl_command_object { __u64 offset; __u64 gpuaddr; @@ -1499,33 +982,12 @@ struct kgsl_command_object { unsigned int id; }; -/** - * struct kgsl_command_syncpoint - GPU syncpoint object - * @priv: Pointer to the type specific buffer - * @size: Size of the type specific buffer - * @type: type of sync point defined here - */ struct kgsl_command_syncpoint { __u64 __user priv; __u64 size; unsigned int type; }; -/** - * struct kgsl_command_object - Argument for IOCTL_KGSL_GPU_COMMAND - * @flags: Current flags for the object - * @cmdlist: List of kgsl_command_objects for submission - * @cmd_size: Size of kgsl_command_objects structure - * @numcmds: Number of kgsl_command_objects in command list - * @objlist: List of kgsl_command_objects for tracking - * @obj_size: Size of kgsl_command_objects structure - * @numobjs: Number of kgsl_command_objects in object list - * @synclist: List of kgsl_command_syncpoints - * @sync_size: Size of kgsl_command_syncpoint structure - * @numsyncs: Number of kgsl_command_syncpoints in syncpoint list - * @context_id: Context ID submittin ghte kgsl_gpu_command - * @timestamp: Timestamp for the submitted commands - */ struct kgsl_gpu_command { __u64 flags; __u64 __user cmdlist; @@ -1544,23 +1006,6 @@ struct kgsl_gpu_command { #define IOCTL_KGSL_GPU_COMMAND \ _IOWR(KGSL_IOC_TYPE, 0x4A, struct kgsl_gpu_command) -/** - * struct kgsl_preemption_counters_query - argument to - * IOCTL_KGSL_PREEMPTIONCOUNTER_QUERY - * @counters: Return preemption counters array - * @size_user: Size allocated by userspace - * @size_priority_level: Size of preemption counters for each - * priority level - * @max_priority_level: Return max number of priority levels - * - * Query the available preemption counters. The array counters - * is used to return preemption counters. The size of the array - * is passed in so the kernel will only write at most size_user - * or max available preemption counters. The total number of - * preemption counters is returned in max_priority_level. If the - * array or size passed in are invalid, then an error is - * returned back. - */ struct kgsl_preemption_counters_query { __u64 __user counters; unsigned int size_user; @@ -1571,16 +1016,6 @@ struct kgsl_preemption_counters_query { #define IOCTL_KGSL_PREEMPTIONCOUNTER_QUERY \ _IOWR(KGSL_IOC_TYPE, 0x4B, struct kgsl_preemption_counters_query) -/** - * struct kgsl_gpuobj_set_info - argument for IOCTL_KGSL_GPUOBJ_SET_INFO - * @flags: Flags to indicate which parameters to change - * @metadata: If KGSL_GPUOBJ_SET_INFO_METADATA is set, a pointer to the new - * metadata - * @id: GPU memory object ID to change - * @metadata_len: If KGSL_GPUOBJ_SET_INFO_METADATA is set, the length of the - * new metadata string - * @type: If KGSL_GPUOBJ_SET_INFO_TYPE is set, the new type of the memory object - */ #define KGSL_GPUOBJ_SET_INFO_METADATA (1 << 0) #define KGSL_GPUOBJ_SET_INFO_TYPE (1 << 1) @@ -1596,13 +1031,6 @@ struct kgsl_gpuobj_set_info { #define IOCTL_KGSL_GPUOBJ_SET_INFO \ _IOW(KGSL_IOC_TYPE, 0x4C, struct kgsl_gpuobj_set_info) -/** - * struct kgsl_sparse_phys_alloc - Argument for IOCTL_KGSL_SPARSE_PHYS_ALLOC - * @size: Size in bytes to back - * @pagesize: Pagesize alignment required - * @flags: Flags for this allocation - * @id: Returned ID for this allocation - */ struct kgsl_sparse_phys_alloc { __u64 size; __u64 pagesize; @@ -1613,10 +1041,6 @@ struct kgsl_sparse_phys_alloc { #define IOCTL_KGSL_SPARSE_PHYS_ALLOC \ _IOWR(KGSL_IOC_TYPE, 0x50, struct kgsl_sparse_phys_alloc) -/** - * struct kgsl_sparse_phys_free - Argument for IOCTL_KGSL_SPARSE_PHYS_FREE - * @id: ID to free - */ struct kgsl_sparse_phys_free { unsigned int id; }; @@ -1624,14 +1048,6 @@ struct kgsl_sparse_phys_free { #define IOCTL_KGSL_SPARSE_PHYS_FREE \ _IOW(KGSL_IOC_TYPE, 0x51, struct kgsl_sparse_phys_free) -/** - * struct kgsl_sparse_virt_alloc - Argument for IOCTL_KGSL_SPARSE_VIRT_ALLOC - * @size: Size in bytes to reserve - * @pagesize: Pagesize alignment required - * @flags: Flags for this allocation - * @id: Returned ID for this allocation - * @gpuaddr: Returned GPU address for this allocation - */ struct kgsl_sparse_virt_alloc { __u64 size; __u64 pagesize; @@ -1643,10 +1059,6 @@ struct kgsl_sparse_virt_alloc { #define IOCTL_KGSL_SPARSE_VIRT_ALLOC \ _IOWR(KGSL_IOC_TYPE, 0x52, struct kgsl_sparse_virt_alloc) -/** - * struct kgsl_sparse_virt_free - Argument for IOCTL_KGSL_SPARSE_VIRT_FREE - * @id: ID to free - */ struct kgsl_sparse_virt_free { unsigned int id; }; @@ -1654,14 +1066,6 @@ struct kgsl_sparse_virt_free { #define IOCTL_KGSL_SPARSE_VIRT_FREE \ _IOW(KGSL_IOC_TYPE, 0x53, struct kgsl_sparse_virt_free) -/** - * struct kgsl_sparse_binding_object - Argument for kgsl_sparse_bind - * @virtoffset: Offset into the virtual ID - * @physoffset: Offset into the physical ID (bind only) - * @size: Size in bytes to reserve - * @flags: Flags for this kgsl_sparse_binding_object - * @id: Physical ID to bind (bind only) - */ struct kgsl_sparse_binding_object { __u64 virtoffset; __u64 physoffset; @@ -1670,14 +1074,6 @@ struct kgsl_sparse_binding_object { unsigned int id; }; -/** - * struct kgsl_sparse_bind - Argument for IOCTL_KGSL_SPARSE_BIND - * @list: List of kgsl_sparse_bind_objects to bind/unbind - * @id: Virtual ID to bind/unbind - * @size: Size of kgsl_sparse_bind_object - * @count: Number of elements in list - * - */ struct kgsl_sparse_bind { __u64 __user list; unsigned int id; @@ -1688,20 +1084,6 @@ struct kgsl_sparse_bind { #define IOCTL_KGSL_SPARSE_BIND \ _IOW(KGSL_IOC_TYPE, 0x54, struct kgsl_sparse_bind) -/** - * struct kgsl_gpu_sparse_command - Argument for - * IOCTL_KGSL_GPU_SPARSE_COMMAND - * @flags: Current flags for the object - * @sparselist: List of kgsl_sparse_binding_object to bind/unbind - * @synclist: List of kgsl_command_syncpoints - * @sparsesize: Size of kgsl_sparse_binding_object - * @numsparse: Number of elements in list - * @sync_size: Size of kgsl_command_syncpoint structure - * @numsyncs: Number of kgsl_command_syncpoints in syncpoint list - * @context_id: Context ID submitting the kgsl_gpu_command - * @timestamp: Timestamp for the submitted commands - * @id: Virtual ID to bind/unbind - */ struct kgsl_gpu_sparse_command { __u64 flags; __u64 __user sparselist; @@ -1721,26 +1103,6 @@ struct kgsl_gpu_sparse_command { #define KGSL_GPUMEM_RANGE_OP_BIND 1 #define KGSL_GPUMEM_RANGE_OP_UNBIND 2 -/** - * struct kgsl_gpumem_bind_range - specifies a bind operation for a virtual - * buffer object - * @child_offset: Offset to the start of memory within the child buffer object - * (not used for KGSL_GPUMEM_RANGE_OP_UNBIND operations) - * @target_offset: GPU address offset within the target VBO - * @length: Amount of memory to map/unmap (in bytes) - * @child_id: The GPU buffer ID for the child object to map/unmap in the VBO - * @op: One of KGSL_GPUMEM_RANGE_OP_BIND or KGSL_GPUMEM_RANGE_OP_UNBIND - * - * This defines a specific bind operation to a virtual buffer object specified - * in &struct kgsl_gpumem_bind_ranges. When @op is KGSL_GPUMEM_RANGE_OP_BIND the - * physical memory starting at @child_offset in the memory object identified by - * @child_id will be mapped into the target virtual buffer object starting at - * @offset for @length bytes. - * - * When @op is KGSL_GPUMEM_RANGE_OP_UNBIND any entries in the target virtual - * buffer object between @offset and @length that belong to @child_id will be - * removed. - */ struct kgsl_gpumem_bind_range { __u64 child_offset; __u64 target_offset; @@ -1752,32 +1114,6 @@ struct kgsl_gpumem_bind_range { #define KGSL_GPUMEM_BIND_ASYNC (1UL << 0) #define KGSL_GPUMEM_BIND_FENCE_OUT (1UL << 1) -/** - * struct kgsl_gpumem_bind_ranges - Argument to IOCTL_KGSL_GPUMEM_BIND_RANGES to - * either map or unmap a child buffer object into a virtual buffer object. - * @ranges: User memory pointer to an array of range operations of type &struct - * kgsl_gpumem_bind_range - * @ranges_nents: Number of entries in @ranges - * @ranges_size: Size of each entry in @ranges in bytes - * @id: GPU buffer object identifier for the target virtual buffer object - * @flags: Bitmap of KGSL_GPUMEM_BIND_ASYNC and KGSL_GPUMEM_BIND_FENCE_OUT - * @fence_id: If KGSL_GPUMEM_BIND_FENCE_OUT is set in @flags contains the - * identifier for the sync fence that will be signaled after the operation - * completes - * - * Describes a number of range operations to perform on a virtual buffer object - * identified by @id. Ranges should be a __u64 representation of an array of - * &struct kgsl_gpumem_bind_range entries. @ranges_nents will contain the number - * of entries in the array, and @ranges_size will contain the size of each entry - * in the array. If KGSL_GPUMEM_BIND_ASYNC is set the operation will be - * performed asynchronously and the operation will immediately return to the - * user. Otherwise the calling context will block until the operation has - * completed. - * - * If KGSL_GPUMEM_BIND_ASYNC and KGSL_GPUMEM_BIND_FENCE_OUT are both set a sync - * fence will be created and returned in @fence_id. The fence will be signaled - * when the bind operation has completed. - */ struct kgsl_gpumem_bind_ranges { __u64 ranges; __u32 ranges_nents; @@ -1785,7 +1121,6 @@ struct kgsl_gpumem_bind_ranges { __u32 id; __u32 flags; int fence_id; - /* private: 64 bit compatibility */ __u32 padding; }; @@ -1794,81 +1129,23 @@ struct kgsl_gpumem_bind_ranges { #define KGSL_GPU_AUX_COMMAND_BIND (1 << 0) #define KGSL_GPU_AUX_COMMAND_TIMELINE (1 << 1) -/* Reuse the same flag that GPU COMMAND uses */ #define KGSL_GPU_AUX_COMMAND_SYNC KGSL_CMDBATCH_SYNC -/** - * struct kgsl_gpu_aux_command_bind - Descriptor for a GPU AUX bind command - * @rangeslist: Pointer to a list of &struct kgsl_gpumem_bind_range items - * @numranges Number of entries in @rangeslist - * @rangesize: Size of each entry in @rangeslist - * @target: The GPU memory ID for the target virtual buffer object - * - * Describe a GPU AUX command to bind ranges in a virtual buffer object. - * @rangeslist points to a &struct kgsl_gpumem_bind_ranges which is the same - * struct that is used by IOCTl_KGSL_GPUMEM_BIND_RANGES. @numrages is the size - * of the array in @rangeslist and @rangesize is the size of each entity in - * @rangeslist. @target points to the GPU ID for the target VBO object. - */ struct kgsl_gpu_aux_command_bind { __u64 rangeslist; __u64 numranges; __u64 rangesize; __u32 target; -/* private: Padding for 64 bit compatibility */ __u32 padding; }; -/** - * struct kgsl_aux_command_generic - Container for an AUX command - * @priv: Pointer to the type specific buffer - * @size: Size of the type specific buffer - * @type: type of sync point defined here - * - * Describes a generic container for GPU aux commands. @priv is a user pointer - * to the command struct matching @type of size @size. - */ struct kgsl_gpu_aux_command_generic { __u64 priv; __u64 size; __u32 type; -/* private: Padding for 64 bit compatibility */ __u32 padding; }; -/** - * struct kgsl_gpu_aux_command - Argument for IOCTL_KGSL_GPU_AUX_COMMAND - * @flags: flags for the object - * @cmdlist: List of &struct kgsl_gpu_aux_command_generic objects - * @cmd_size: Size of each entry in @cmdlist - * @numcmds: Number of entries in @cmdlist - * @synclist: List of &struct kgsl_command_syncpoint objects - * @syncsize: Size of each entry in @synclist - * @numsyncs: Number of entries in @synclist - * @context_id: ID of the context submtting the aux command - * @timestamp: Timestamp for the command submission - * - * Describe a GPU auxiliary command. Auxiliary commands are tasks that are not - * performed on hardware but can be queued like normal GPU commands. Like GPU - * commands AUX commands are assigned a timestamp and processed in order in the - * queue. They can also have standard sync objects attached. The only - * difference is that AUX commands usually perform some sort of administrative - * task in the CPU and are retired in the dispatcher. - * - * For bind operations flags must have one of the KGSL_GPU_AUX_COMMAND_* flags - * set. If sync objects are attached KGSL_GPU_AUX_COMMAND_SYNC must be set. - * @cmdlist points to an array of &struct kgsl_gpu_aux_command_generic structs - * which in turn will have a pointer to a specific command type. - * @numcmds is the number of commands in the list and @cmdsize is the size - * of each entity in @cmdlist. - * - * If KGSL_GPU_AUX_COMMAND_SYNC is specified @synclist will point to an array of - * &struct kgsl_command_syncpoint items in the same fashion as a GPU hardware - * command. @numsyncs and @syncsize describe the list. - * - * @context_id is the context that is submitting the command and @timestamp - * contains the timestamp for the operation. - */ struct kgsl_gpu_aux_command { __u64 flags; __u64 cmdlist; @@ -1884,59 +1161,24 @@ struct kgsl_gpu_aux_command { #define IOCTL_KGSL_GPU_AUX_COMMAND \ _IOWR(KGSL_IOC_TYPE, 0x57, struct kgsl_gpu_aux_command) -/** - * struct kgsl_timeline_create - Argument for IOCTL_KGSL_TIMELINE_CREATE - * @seqno: Initial sequence number for the timeline - * @id: Timeline identifier [out] - * - * Create a new semaphore timeline and return the identifier in @id. - * The identifier is global for the device and can be used to - * identify the timeline in all subsequent commands. - */ struct kgsl_timeline_create { __u64 seqno; __u32 id; -/* private: padding for 64 bit compatibility */ __u32 padding; }; #define IOCTL_KGSL_TIMELINE_CREATE \ _IOWR(KGSL_IOC_TYPE, 0x58, struct kgsl_timeline_create) -/** - * struct kgsl_timeline_val - A container to store a timeline/sequence number - * pair. - * @seqno: Sequence number to signal/query - * @timeline: The timeline identifier to signal/query - * - * A container to store a timeline/seqno pair used by the query and signal - * ioctls. - */ struct kgsl_timeline_val { __u64 seqno; __u32 timeline; -/* private: padding for 64 bit compatibility */ __u32 padding; }; #define KGSL_TIMELINE_WAIT_ALL 1 #define KGSL_TIMELINE_WAIT_ANY 2 -/** - * struct kgsl_timeline_wait - Argument for IOCTL_KGSL_TIMELINE_WAIT - * @tv_sec: Number of seconds to wait for the signal - * @tv_nsec: Number of nanoseconds to wait for the signal - * @timelines: Address of an array of &struct kgsl_timeline_val entries - * @count: Number of entries in @timeline - * @timelines_size: Size of each entry in @timelines - * @flags: One of KGSL_TIMELINE_WAIT_ALL or KGSL_TIMELINE_WAIT_ANY - * - * Wait for the timelines listed in @timelines to be signaled. If @flags is - * equal to KGSL_TIMELINE_WAIT_ALL then wait for all timelines or if - * KGSL_TIMELINE_WAIT_ANY is specified then wait for any of the timelines to - * signal. @tv_sec and @tv_nsec indicates the number of seconds and nanoseconds - * that the process should be blocked waiting for the signal. - */ struct kgsl_timeline_wait { __s64 tv_sec; __s64 tv_nsec; @@ -1944,7 +1186,6 @@ struct kgsl_timeline_wait { __u32 count; __u32 timelines_size; __u32 flags; -/* private: padding for 64 bit compatibility */ __u32 padding; }; @@ -1954,14 +1195,6 @@ struct kgsl_timeline_wait { #define IOCTL_KGSL_TIMELINE_QUERY \ _IOWR(KGSL_IOC_TYPE, 0x5A, struct kgsl_timeline_val) -/** - * struct kgsl_timeline_signal - argument for IOCTL_KGSL_TIMELINE_SIGNAL - * @timelines: Address of an array of &struct kgsl_timeline_val entries - * @count: Number of entries in @timelines - * @timelines_size: Size of each entry in @timelines - * - * Signal an array of timelines of type @struct kgsl_timeline_val. - */ struct kgsl_timeline_signal { __u64 timelines; __u32 count; @@ -1971,15 +1204,6 @@ struct kgsl_timeline_signal { #define IOCTL_KGSL_TIMELINE_SIGNAL \ _IOW(KGSL_IOC_TYPE, 0x5B, struct kgsl_timeline_signal) -/** - * struct kgsl_timeline_fence_get - argument for IOCTL_KGSL_TIMELINE_FENCE_GET - * @seqno: Sequence number for the fence - * @timeline: Timeline to create the fence on - * @handle: Contains the fence fd for a successful operation [out] - * - * Create a sync file descriptor for the seqnum on the timeline and return it in - * @handle. Can be polled and queried just like any other sync file descriptor - */ struct kgsl_timeline_fence_get { __u64 seqno; __u32 timeline; @@ -1988,34 +1212,18 @@ struct kgsl_timeline_fence_get { #define IOCTL_KGSL_TIMELINE_FENCE_GET \ _IOWR(KGSL_IOC_TYPE, 0x5C, struct kgsl_timeline_fence_get) -/** - * IOCTL_KGSL_TIMELINE_DESTROY takes a u32 identifier for the timeline to - * destroy - */ #define IOCTL_KGSL_TIMELINE_DESTROY _IOW(KGSL_IOC_TYPE, 0x5D, __u32) -/** - * struct kgsl_gpu_aux_command_timeline - An aux command for timeline signals - * @timelines: An array of &struct kgsl_timeline_val elements - * @count: The number of entries in @timelines - * @timelines_size: The size of each element in @timelines - * - * An aux command for timeline signals that can be pointed to by - * &struct kgsl_aux_command_generic when the type is - * KGSL_GPU_AUX_COMMAND_TIMELINE. - */ struct kgsl_gpu_aux_command_timeline { __u64 timelines; __u32 count; __u32 timelines_size; }; -/* Macros for fault type used in kgsl_fault structure */ #define KGSL_FAULT_TYPE_NO_FAULT 0 #define KGSL_FAULT_TYPE_PAGEFAULT 1 #define KGSL_FAULT_TYPE_MAX 2 -/* Macros to be used in kgsl_pagefault_report structure */ #define KGSL_PAGEFAULT_TYPE_NONE 0 #define KGSL_PAGEFAULT_TYPE_READ (1 << 0) #define KGSL_PAGEFAULT_TYPE_WRITE (1 << 1) @@ -2024,87 +1232,32 @@ struct kgsl_gpu_aux_command_timeline { #define KGSL_PAGEFAULT_TYPE_EXTERNAL (1 << 4) #define KGSL_PAGEFAULT_TYPE_TRANSACTION_STALLED (1 << 5) -/** - * struct kgsl_pagefault_report - Descriptor for each page fault - * @fault_addr: page fault address - * @fault_type: type of page fault - * - * Contains information about supported GPU page fault. - * Supported fault type: KGSL_PAGEFAULT_TYPE_* - */ struct kgsl_pagefault_report { __u64 fault_addr; - /* private: reserved for future use */ __u64 reserved[2]; __u32 fault_type; - /* private: padding for 64 bit compatibility */ __u32 __pad; }; -/** - * struct kgsl_fault - Descriptor for each GPU fault type - * @fault: User memory pointer to list of specific fault type - * @type: Type of gpu fault - * @count: Number of entries in @fault - * @size: Size of each entry in @fault in bytes - * - * Contains information about each GPU fault type. If user passes 0 for all the fields, KGSL - * will return the @count and @type of fault. Based on this, user can allocate a buffer for - * specific fault type, fill the @fault and specify the structure size of type specific fault - * in @size. User can walk through @fault list to parse the fault type specific information. - * - * Supported type: KGSL_FAULT_TYPE_* - */ struct kgsl_fault { __u64 fault; __u32 type; __u32 count; __u32 size; - /* private: padding for 64 bit compatibility */ __u32 padding; }; -/** - * struct kgsl_fault_report - Container for list of GPU faults - * @faultlist: User memory pointer to list of fault descriptor &struct kgsl_fault - * @faultnents: Number of entries in @faultlist. Each entry corresponds to a fault type i.e. - * KGSL_FAULT_TYPE_* - * @faultsize: Size of each entry in @faultlist in bytes - * @context_id: ID of a KGSL context - * - * Returns a list of GPU faults for a context identified by @context_id. If the user specifies - * @context_id only, then KGSL will set the @faultnents to the number of fault types it has - * for that context. - * - * User is expected to allocate an array of @struct kgsl_fault with @faultnents number of entries - * and fill the @faultlist field. On calling @IOCTL_KGSL_GET_FAULT_REPORT, KGSL will return the - * type and count for each fault. Based on this, user needs to update the @kgsl_fault structure. - * Then, it should call the @IOCTL_KGSL_GET_FAULT_REPORT again for kernel to fill the fault - * information. - */ struct kgsl_fault_report { __u64 faultlist; __u32 faultnents; __u32 faultsize; __u32 context_id; - /* private: padding for 64 bit compatibility */ __u32 padding; }; #define IOCTL_KGSL_GET_FAULT_REPORT \ _IOWR(KGSL_IOC_TYPE, 0x5E, struct kgsl_fault_report) -/** - * struct kgsl_recurring_object - Argument for IOCTL_KGSL_RECURRING_COMMAND - * @flags: Current flags for the object - * @cmdlist: List of kgsl_command_objects for submission - * @cmd_size: Size of kgsl_command_objects structure - * @numcmds: Number of kgsl_command_objects in command list - * @objlist: List of kgsl_command_objects for tracking - * @obj_size: Size of kgsl_command_objects structure - * @numobjs: Number of kgsl_command_objects in object list - * @context_id: Context ID submitting the kgsl_recurring_command - */ struct kgsl_recurring_command { __u64 flags; __u64 __user cmdlist; @@ -2114,11 +1267,10 @@ struct kgsl_recurring_command { __u32 objsize; __u32 numobjs; __u32 context_id; - /* private: Padding for 64 bit compatibility */ __u32 padding; }; #define IOCTL_KGSL_RECURRING_COMMAND \ _IOWR(KGSL_IOC_TYPE, 0x5F, struct kgsl_recurring_command) -#endif /* _UAPI_MSM_KGSL_H */ \ No newline at end of file +#endif \ No newline at end of file diff --git a/app/src/main/cpp/adrenotools/src/hook/meson.build b/app/src/main/cpp/adrenotools/src/hook/meson.build index 0bb6eb390..4ea88a7dd 100644 --- a/app/src/main/cpp/adrenotools/src/hook/meson.build +++ b/app/src/main/cpp/adrenotools/src/hook/meson.build @@ -1,32 +1,12 @@ hook_impl_files = files(['hook_impl.cpp', 'hook_impl.h', 'hook_impl_params.h']) hook_impl_inc = include_directories('../../include') -# hook_impl_lib = shared_library('hook_impl', 'hook_impl.cpp', 'hook_impl.h', 'hook_impl_params.h') -# target_compile_options(['hook_impl', 'PRIVATE', '-Wall', '-Wextra']) -# target_link_libraries(['hook_impl', 'linkernsbypass', 'log']) -# target_include_directories(['hook_impl', 'PRIVATE', '../../include']) -# set_target_properties(['hook_impl', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden']) main_hook_files = files(['main_hook.c']) -# main_hook_lib = shared_library('main_hook', 'main_hook.c') -# target_compile_options(['main_hook', 'PRIVATE', '-Wall', '-Wextra']) -# target_link_options(['main_hook', 'PRIVATE', '-z', 'global']) -# target_link_libraries(['main_hook', 'hook_impl']) -# set_target_properties(['main_hook', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden']) file_redirect_hook_files = files(['file_redirect_hook.c']) -# file_redirect_hook_lib = shared_library('file_redirect_hook', 'file_redirect_hook.c') -# target_compile_options(['file_redirect_hook', 'PRIVATE', '-Wall', '-Wextra']) -# target_link_options(['file_redirect_hook', 'PRIVATE', '-z', 'global']) -# target_link_libraries(['file_redirect_hook', 'hook_impl']) -# set_target_properties(['file_redirect_hook', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden']) gsl_alloc_hook_files = files(['gsl_alloc_hook.c']) -# gsl_alloc_hook_lib = shared_library('gsl_alloc_hook', 'gsl_alloc_hook.c') -# target_compile_options(['gsl_alloc_hook', 'PRIVATE', '-Wall', '-Wextra']) -# target_link_options(['gsl_alloc_hook', 'PRIVATE', '-z', 'global']) -# target_link_libraries(['gsl_alloc_hook', 'hook_impl']) -# set_target_properties(['gsl_alloc_hook', 'PROPERTIES', 'CXX_VISIBILITY_PRESET', 'hidden']) hook_impl_lib = shared_library( 'hook_impl', diff --git a/app/src/main/cpp/adrenotools/tools/acc-shim/vk_acc_shim.cpp b/app/src/main/cpp/adrenotools/tools/acc-shim/vk_acc_shim.cpp index 2bccb0534..4c5b89ccc 100644 --- a/app/src/main/cpp/adrenotools/tools/acc-shim/vk_acc_shim.cpp +++ b/app/src/main/cpp/adrenotools/tools/acc-shim/vk_acc_shim.cpp @@ -5,7 +5,6 @@ thread_local int satisfy_driver_emutls; -// Check libllvm-glnext.so in older drivers for a full list of these, newer ones are slightly more awkward due to the split enum class OptionType : uint32_t { ArgsStringList = 1, QcArgsString = 4, @@ -21,7 +20,6 @@ struct StringListVal { struct Option { OptionType type; - // PAD union { bool boolVal; int intVal; @@ -49,7 +47,6 @@ int CompileHook(void *state, OptionsSet *opts) { Option replacementOpts[ReplacementOptsNum]; uint32_t patchedOptNum{}; - // Insert your desired args here - results will be written to stderr const char *argv[] = {"-help", "-help-hidden", "", NULL }; StringListVal listVal{ .len = 3, @@ -83,7 +80,6 @@ int CompileHook(void *state, OptionsSet *opts) { struct LlvmInterface { uint32_t unk0[3]; - // PAD void *makeContext; void *delContext; decltype(CompileHook) *compile; diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/QtiGrallocDefs.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/QtiGrallocDefs.h index e0ccbe1d2..8dcdaa5bf 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/QtiGrallocDefs.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/QtiGrallocDefs.h @@ -30,53 +30,31 @@ #ifndef __QTIGRALLOCDEFS_H__ #define __QTIGRALLOCDEFS_H__ -// From gralloc_priv.h -/* Gralloc usage bits indicating the type of allocation that should be used */ -/* Refer to BufferUsage in hardware/interfaces/graphics/common//types.hal */ -/* The bits below are in officially defined vendor space - * i.e bits 28-31 and 48-63*/ -/* Non linear, Universal Bandwidth Compression */ #define GRALLOC_USAGE_PRIVATE_ALLOC_UBWC (UINT32_C(1) << 28) -/* Set this for allocating uncached memory (using O_DSYNC), - * cannot be used with noncontiguous heaps */ #define GRALLOC_USAGE_PRIVATE_UNCACHED (UINT32_C(1) << 29) -/* This flag is used to indicate 10 bit format. - * When both GRALLOC_USAGE_PRIVATE_ALLOC_UBWC & GRALLOC_USAGE_PRIVATE_10BIT - * are set then it will indicate UBWC_TP10 format. - * When only GRALLOC_USAGE_PRIVATE_10BIT is set it will indicate linear P010 format. - */ #define GRALLOC_USAGE_PRIVATE_10BIT (UINT32_C(1) << 30) -/* This flag is used for SECURE display usecase */ #define GRALLOC_USAGE_PRIVATE_SECURE_DISPLAY (UINT32_C(1) << 31) -/* This flag is used to indicate video NV21 format */ #define GRALLOC_USAGE_PRIVATE_VIDEO_NV21_ENCODER 1ULL << 48 -/* unused legacy flags */ #define GRALLOC_USAGE_PRIVATE_MM_HEAP 0 #define GRALLOC_USAGE_PRIVATE_IOMMU_HEAP 0 -/* This flag is set for WFD usecase */ #define GRALLOC_USAGE_PRIVATE_WFD 1ULL << 51 -/* This flag is set for HEIF usecase */ #define GRALLOC_USAGE_PRIVATE_HEIF (UINT32_C(1) << 27) -/* TODO(user): Remove when clients stop referencing this flag */ #define GRALLOC_USAGE_PRIVATE_10BIT_TP 0 -/* This flag indicates PI format is being used */ #define GRALLOC_USAGE_PRIVATE_ALLOC_UBWC_PI 1ULL << 49 -/* This flag is set while CDSP accesses the buffer */ #define GRALLOC_USAGE_PRIVATE_CDSP 1ULL << 50 -// OEM specific HAL formats #define HAL_PIXEL_FORMAT_RGBA_5551 6 #define HAL_PIXEL_FORMAT_RGBA_4444 7 #define HAL_PIXEL_FORMAT_NV12_ENCODEABLE 0x102 @@ -98,7 +76,6 @@ #define HAL_PIXEL_FORMAT_RAW8 0x123 #define HAL_PIXEL_FORMAT_NV12_HEIF 0x116 -// 10 bit #define HAL_PIXEL_FORMAT_ARGB_2101010 0x117 #define HAL_PIXEL_FORMAT_RGBX_1010102 0x118 #define HAL_PIXEL_FORMAT_XRGB_2101010 0x119 @@ -106,7 +83,7 @@ #define HAL_PIXEL_FORMAT_ABGR_2101010 0x11B #define HAL_PIXEL_FORMAT_BGRX_1010102 0x11C #define HAL_PIXEL_FORMAT_XBGR_2101010 0x11D -#define HAL_PIXEL_FORMAT_YCbCr_420_P010 0x36 // HAL_PIXEL_FORMAT_YCBCR_P010 +#define HAL_PIXEL_FORMAT_YCbCr_420_P010 0x36 #define HAL_PIXEL_FORMAT_YCbCr_420_P010_UBWC 0x124 #define HAL_PIXEL_FORMAT_YCbCr_420_P010_VENUS 0x7FA30C0A @@ -115,7 +92,6 @@ #define HAL_PIXEL_FORMAT_INTERLACE 0x180 -// Camera utils format #define HAL_PIXEL_FORMAT_NV12_LINEAR_FLEX 0x125 #define HAL_PIXEL_FORMAT_NV12_UBWC_FLEX 0x126 #define HAL_PIXEL_FORMAT_MULTIPLANAR_FLEX 0x127 @@ -123,22 +99,16 @@ #define HAL_PIXEL_FORMAT_NV12_UBWC_FLEX_4_BATCH 0x129 #define HAL_PIXEL_FORMAT_NV12_UBWC_FLEX_8_BATCH 0x130 -// FSC Formats #define HAL_PIXEL_FORMAT_RGB888_UBWC_FSC 0x131 #define HAL_PIXEL_FORMAT_RGB101010_UBWC_FSC 0x132 -// v4l2_fourcc('Y', 'U', 'Y', 'L'). 24 bpp YUYV 4:2:2 10 bit per component #define HAL_PIXEL_FORMAT_YCbCr_422_I_10BIT 0x4C595559 -// v4l2_fourcc('Y', 'B', 'W', 'C'). 10 bit per component. This compressed -// format reduces the memory access bandwidth #define HAL_PIXEL_FORMAT_YCbCr_422_I_10BIT_COMPRESSED 0x43574259 -// UBWC aligned Venus format #define HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC 0x7FA30C06 #define HAL_PIXEL_FORMAT_YCbCr_420_TP10_UBWC 0x7FA30C09 -// Khronos ASTC formats #define HAL_PIXEL_FORMAT_COMPRESSED_RGBA_ASTC_4x4_KHR 0x93B0 #define HAL_PIXEL_FORMAT_COMPRESSED_RGBA_ASTC_5x4_KHR 0x93B1 #define HAL_PIXEL_FORMAT_COMPRESSED_RGBA_ASTC_5x5_KHR 0x93B2 @@ -170,4 +140,4 @@ enum { BUFFER_TYPE_UI = 0, BUFFER_TYPE_VIDEO }; -#endif //__QTIGRALLOCDEFS_H__ +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/common.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/common.h index 73e80cb1d..a6dd7366d 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/common.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/common.h @@ -26,7 +26,6 @@ using namespace android::hardware; using namespace android; -/* Simplified version of hidl::base::V1_0::IBase */ struct IBase : virtual public RefBase { virtual bool isRemote() const; diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.cpp b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.cpp index 30610e810..3a77d9274 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.cpp +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.cpp @@ -80,8 +80,6 @@ AdrenoMemInfo::AdrenoMemInfo() { ALOGE(" Failed to load libadreno_utils.so"); } - // Check if the overriding property debug.gralloc.gfx_ubwc_disable - // that disables UBWC allocations for the graphics stack is set char property[PROPERTY_VALUE_MAX]; property_get(DISABLE_UBWC_PROP, property, "0"); if (!(strncmp(property, "1", PROPERTY_VALUE_MAX)) || @@ -123,16 +121,13 @@ void AdrenoMemInfo::AlignUnCompressedRGB(int width, int height, int format, int break; } - int raster_mode = 0; // Adreno unknown raster mode. - int padding_threshold = 512; // Threshold for padding surfaces. - // the function below computes aligned width and aligned height - // based on linear or macro tile mode selected. + int raster_mode = 0; + int padding_threshold = 512; + + if (LINK_adreno_compute_fmt_aligned_width_and_height) { - // We call into adreno_utils only for RGB formats. So plane_id is 0 and - // num_samples is 1 always. We may have to add uitility function to - // find out these if there is a need to call this API for YUV formats. LINK_adreno_compute_fmt_aligned_width_and_height( - width, height, 0 /*plane_id*/, GetGpuPixelFormat(format), 1 /*num_samples*/, tile_enabled, + width, height, 0 , GetGpuPixelFormat(format), 1 , tile_enabled, raster_mode, padding_threshold, reinterpret_cast(aligned_w), reinterpret_cast(aligned_h)); } else if (LINK_adreno_compute_aligned_width_and_height) { @@ -140,7 +135,7 @@ void AdrenoMemInfo::AlignUnCompressedRGB(int width, int height, int format, int width, height, bpp, tile_enabled, raster_mode, padding_threshold, reinterpret_cast(aligned_w), reinterpret_cast(aligned_h)); } else if (LINK_adreno_compute_padding) { - int surface_tile_height = 1; // Linear surface + int surface_tile_height = 1; *aligned_w = UINT(LINK_adreno_compute_padding(width, bpp, surface_tile_height, raster_mode, padding_threshold)); ALOGW("%s: Warning!! Old GFX API is used to calculate stride", __FUNCTION__); @@ -157,9 +152,9 @@ void AdrenoMemInfo::AlignCompressedRGB(int width, int height, int format, unsign unsigned int* aligned_h) { if (LINK_adreno_compute_compressedfmt_aligned_width_and_height) { int bytesPerPixel = 0; - int raster_mode = 0; // Adreno unknown raster mode. - int padding_threshold = 512; // Threshold for padding - // surfaces. + int raster_mode = 0; + int padding_threshold = 512; + LINK_adreno_compute_compressedfmt_aligned_width_and_height( width, height, format, 0, raster_mode, padding_threshold, reinterpret_cast(aligned_w), @@ -341,4 +336,4 @@ bool AdrenoMemInfo::IsPISupportedByGPU(int format, uint64_t usage) { return false; } -} // namespace gralloc +} diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.h index 590d18ba6..f5281bc1c 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_adreno_info.h @@ -35,11 +35,10 @@ namespace gralloc { -// Adreno Pixel Formats typedef enum { ADRENO_PIXELFORMAT_UNKNOWN = 0, ADRENO_PIXELFORMAT_R16G16B16A16_FLOAT = 10, - ADRENO_PIXELFORMAT_R10G10B10A2_UNORM = 24, // Vertex, Normalized GL_UNSIGNED_INT_10_10_10_2_OES + ADRENO_PIXELFORMAT_R10G10B10A2_UNORM = 24, ADRENO_PIXELFORMAT_R8G8B8A8 = 28, ADRENO_PIXELFORMAT_R8G8B8A8_SRGB = 29, ADRENO_PIXELFORMAT_D32_FLOAT = 40, @@ -55,140 +54,87 @@ typedef enum { ADRENO_PIXELFORMAT_P010 = 104, ADRENO_PIXELFORMAT_YUY2 = 107, ADRENO_PIXELFORMAT_B4G4R4A4 = 115, - ADRENO_PIXELFORMAT_NV12_EXT = 506, // NV12 with non-std alignment and offsets - ADRENO_PIXELFORMAT_R8G8B8X8 = 507, // GL_RGB8 (Internal) - ADRENO_PIXELFORMAT_R8G8B8 = 508, // GL_RGB8 - ADRENO_PIXELFORMAT_A1B5G5R5 = 519, // GL_RGB5_A1 - ADRENO_PIXELFORMAT_R8G8B8X8_SRGB = 520, // GL_SRGB8 - ADRENO_PIXELFORMAT_R8G8B8_SRGB = 521, // GL_SRGB8 + ADRENO_PIXELFORMAT_NV12_EXT = 506, + ADRENO_PIXELFORMAT_R8G8B8X8 = 507, + ADRENO_PIXELFORMAT_R8G8B8 = 508, + ADRENO_PIXELFORMAT_A1B5G5R5 = 519, + ADRENO_PIXELFORMAT_R8G8B8X8_SRGB = 520, + ADRENO_PIXELFORMAT_R8G8B8_SRGB = 521, ADRENO_PIXELFORMAT_A2B10G10R10_UNORM = 532, - // Vertex, Normalized GL_UNSIGNED_INT_10_10_10_2_OES ADRENO_PIXELFORMAT_R10G10B10X2_UNORM = 537, ADRENO_PIXELFORMAT_D24_UNORM_X8_UINT = 548, ADRENO_PIXELFORMAT_D24_UNORM = 549, ADRENO_PIXELFORMAT_D32_FLOAT_X24S8_UINT = 551, ADRENO_PIXELFORMAT_S8_UINT = 552, - ADRENO_PIXELFORMAT_ASTC_4X4 = 568, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_5X4 = 569, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_5X5 = 570, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_6X5 = 571, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_6X6 = 572, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_8X5 = 573, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_8X6 = 574, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_8X8 = 575, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X5 = 576, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X6 = 577, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X8 = 578, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X10 = 579, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_12X10 = 580, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_12X12 = 581, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_4X4_SRGB = 582, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_5X4_SRGB = 583, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_5X5_SRGB = 584, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_6X5_SRGB = 585, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_6X6_SRGB = 586, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_8X5_SRGB = 587, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_8X6_SRGB = 588, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_8X8_SRGB = 589, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X5_SRGB = 590, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X6_SRGB = 591, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X8_SRGB = 592, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_10X10_SRGB = 593, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_12X10_SRGB = 594, // ASTC Compressed - ADRENO_PIXELFORMAT_ASTC_12X12_SRGB = 595, // ASTC Compressed - // Vertex, Normalized GL_UNSIGNED_INT_10_10_10_2_OES - ADRENO_PIXELFORMAT_R5G6B5 = 610, // RGBA version of B5G6R5 - ADRENO_PIXELFORMAT_R5G5B5A1 = 611, // RGBA version of B5G5R5A1 - ADRENO_PIXELFORMAT_R4G4B4A4 = 612, // RGBA version of B4G4R4A4 - ADRENO_PIXELFORMAT_UYVY = 614, // YUV 4:2:2 packed progressive (1 plane) + ADRENO_PIXELFORMAT_ASTC_4X4 = 568, + ADRENO_PIXELFORMAT_ASTC_5X4 = 569, + ADRENO_PIXELFORMAT_ASTC_5X5 = 570, + ADRENO_PIXELFORMAT_ASTC_6X5 = 571, + ADRENO_PIXELFORMAT_ASTC_6X6 = 572, + ADRENO_PIXELFORMAT_ASTC_8X5 = 573, + ADRENO_PIXELFORMAT_ASTC_8X6 = 574, + ADRENO_PIXELFORMAT_ASTC_8X8 = 575, + ADRENO_PIXELFORMAT_ASTC_10X5 = 576, + ADRENO_PIXELFORMAT_ASTC_10X6 = 577, + ADRENO_PIXELFORMAT_ASTC_10X8 = 578, + ADRENO_PIXELFORMAT_ASTC_10X10 = 579, + ADRENO_PIXELFORMAT_ASTC_12X10 = 580, + ADRENO_PIXELFORMAT_ASTC_12X12 = 581, + ADRENO_PIXELFORMAT_ASTC_4X4_SRGB = 582, + ADRENO_PIXELFORMAT_ASTC_5X4_SRGB = 583, + ADRENO_PIXELFORMAT_ASTC_5X5_SRGB = 584, + ADRENO_PIXELFORMAT_ASTC_6X5_SRGB = 585, + ADRENO_PIXELFORMAT_ASTC_6X6_SRGB = 586, + ADRENO_PIXELFORMAT_ASTC_8X5_SRGB = 587, + ADRENO_PIXELFORMAT_ASTC_8X6_SRGB = 588, + ADRENO_PIXELFORMAT_ASTC_8X8_SRGB = 589, + ADRENO_PIXELFORMAT_ASTC_10X5_SRGB = 590, + ADRENO_PIXELFORMAT_ASTC_10X6_SRGB = 591, + ADRENO_PIXELFORMAT_ASTC_10X8_SRGB = 592, + ADRENO_PIXELFORMAT_ASTC_10X10_SRGB = 593, + ADRENO_PIXELFORMAT_ASTC_12X10_SRGB = 594, + ADRENO_PIXELFORMAT_ASTC_12X12_SRGB = 595, + + ADRENO_PIXELFORMAT_R5G6B5 = 610, + ADRENO_PIXELFORMAT_R5G5B5A1 = 611, + ADRENO_PIXELFORMAT_R4G4B4A4 = 612, + ADRENO_PIXELFORMAT_UYVY = 614, ADRENO_PIXELFORMAT_NV21 = 619, - ADRENO_PIXELFORMAT_Y8U8V8A8 = 620, // YUV 4:4:4 packed (1 plane) - ADRENO_PIXELFORMAT_Y8 = 625, // Single 8-bit luma only channel YUV format - ADRENO_PIXELFORMAT_TP10 = 654, // YUV 4:2:0 planar 10 bits/comp (2 planes) + ADRENO_PIXELFORMAT_Y8U8V8A8 = 620, + ADRENO_PIXELFORMAT_Y8 = 625, + ADRENO_PIXELFORMAT_TP10 = 654, } ADRENOPIXELFORMAT; typedef enum { - SURFACE_TILE_MODE_DISABLE = 0x0, // used for linear surface - SURFACE_TILE_MODE_ENABLE = 0x1 // used for tiled surface + SURFACE_TILE_MODE_DISABLE = 0x0, + SURFACE_TILE_MODE_ENABLE = 0x1 } surface_tile_mode_t; class AdrenoMemInfo { public: - /* - * Function to compute aligned width and aligned height based on - * width, height, format and usage flags. - * - * @return aligned width, aligned height - */ void GetAlignedWidthAndHeight(int width, int height, int format, int usage, unsigned int* aligned_w, unsigned int* aligned_h, bool ubwc_enabled, bool tile_enabled); - /* - * Function to compute the adreno aligned width and aligned height - * based on the width and format. - * - * @return aligned width, aligned height - */ void AlignUnCompressedRGB(int width, int height, int format, int tileEnabled, unsigned int* aligned_w, unsigned int* aligned_h); - /* - * Function to compute the adreno aligned width and aligned height - * based on the width and format. - * - * @return aligned width, aligned height - */ void AlignCompressedRGB(int width, int height, int format, unsigned int* aligned_w, unsigned int* aligned_h); - /* - * Function to compute the pixel alignment requirement. - * - * @return alignment - */ uint32_t GetGpuPixelAlignment(); - /* - * Function to query whether GPU supports UBWC for given HAL format - * @return > 0 : supported - * 0 : not supported - */ bool IsUBWCSupportedByGPU(int format); - /* - * Function to check if GPU supports PI or not - */ bool IsPISupportedByGPU(int format, uint64_t usage); - /* - * Function to get the corresponding Adreno format for given HAL format - */ ADRENOPIXELFORMAT GetGpuPixelFormat(int hal_format); - /* - * Function to get graphics metadata blob size - * @return graphics metadata size - */ uint32_t AdrenoGetMetadataBlobSize(); - /* - * Function to populate the graphics metadata blob - * @return 1 : Successful - * 2 : Unsuccessful - */ int AdrenoInitMemoryLayout(void* metadata_blob, int width, int height, int depth, int format, int num_samples, int isUBWC, uint64_t usage, uint32_t num_planes); - /* - * Function to get buffer size for based on graphcis metadata - * @return buffer size - */ uint32_t AdrenoGetAlignedGpuBufferSize(void* metadata_blob); - /* - * Function to check if adreno size calculation APIs are avaliable - * @return true : Avaliable - * false : Unavaliable - */ bool AdrenoSizeAPIAvaliable(); static AdrenoMemInfo* GetInstance(); @@ -196,7 +142,6 @@ class AdrenoMemInfo { private: AdrenoMemInfo(); ~AdrenoMemInfo(); - // link(s)to adreno surface padding library. int (*LINK_adreno_compute_padding)(int width, int bpp, int surface_tile_height, int screen_tile_height, int padding_threshold) = NULL; void (*LINK_adreno_compute_aligned_width_and_height)(int width, int height, int bpp, @@ -229,6 +174,6 @@ class AdrenoMemInfo { static AdrenoMemInfo* s_instance; }; -} // namespace gralloc +} -#endif // __GR_ADRENO_INFO_H__ +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_priv_handle.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_priv_handle.h index 3efbea4b7..30a48e7ab 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_priv_handle.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_priv_handle.h @@ -57,29 +57,27 @@ struct private_handle_t { PRIV_FLAGS_CAMERA_READ = 0x00040000, PRIV_FLAGS_HW_COMPOSER = 0x00080000, PRIV_FLAGS_HW_TEXTURE = 0x00100000, - PRIV_FLAGS_ITU_R_601 = 0x00200000, // Unused from display - PRIV_FLAGS_ITU_R_601_FR = 0x00400000, // Unused from display - PRIV_FLAGS_ITU_R_709 = 0x00800000, // Unused from display + PRIV_FLAGS_ITU_R_601 = 0x00200000, + PRIV_FLAGS_ITU_R_601_FR = 0x00400000, + PRIV_FLAGS_ITU_R_709 = 0x00800000, PRIV_FLAGS_SECURE_DISPLAY = 0x01000000, PRIV_FLAGS_TILE_RENDERED = 0x02000000, PRIV_FLAGS_CPU_RENDERED = 0x04000000, PRIV_FLAGS_UBWC_ALIGNED = 0x08000000, PRIV_FLAGS_DISP_CONSUMER = 0x10000000, - PRIV_FLAGS_CLIENT_ALLOCATED = 0x20000000, // Ion buffer allocated outside of gralloc - PRIV_FLAGS_UBWC_ALIGNED_PI = 0x40000000, // PI format + PRIV_FLAGS_CLIENT_ALLOCATED = 0x20000000, + PRIV_FLAGS_UBWC_ALIGNED_PI = 0x40000000, }; - // file-descriptors dup'd over IPC int fd; int fd_metadata; - // values sent over IPC int magic; int flags; - int width; // holds width of the actual buffer allocated - int height; // holds height of the actual buffer allocated - int unaligned_width; // holds width client asked to allocate - int unaligned_height; // holds height client asked to allocate + int width; + int height; + int unaligned_width; + int unaligned_height; int format; int buffer_type; unsigned int layer_count; @@ -126,7 +124,6 @@ struct private_handle_t { numFds = kNumFds; } - // Legacy constructor used by some clients private_handle_t(int fd, unsigned int size, int usage, int buf_type, int format, int w, int h) : private_handle_t(fd, -1, PRIV_FLAGS_CLIENT_ALLOCATED, w, h, 0, 0, format, buf_type, size, static_cast(usage)) {} @@ -176,7 +173,6 @@ struct private_handle_t { unsigned int GetLayerCount() const { return layer_count; } int GetStride() const { - // In handle we currently store aligned width after allocation. return width; } @@ -187,4 +183,4 @@ struct private_handle_t { }; #pragma pack(pop) -#endif // __GR_PRIV_HANDLE_H__ +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.cpp b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.cpp index a83ef20a6..4dc3a1f4b 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.cpp +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.cpp @@ -162,7 +162,6 @@ bool CpuCanRead(uint64_t usage) { bool CpuCanWrite(uint64_t usage) { if (usage & BufferUsage::CPU_WRITE_MASK) { - // Application intends to use CPU for rendering return true; } @@ -177,7 +176,6 @@ uint32_t GetDataAlignment(int format, uint64_t usage) { if (usage & BufferUsage::PROTECTED) { if ((usage & BufferUsage::CAMERA_OUTPUT) || (usage & GRALLOC_USAGE_PRIVATE_SECURE_DISPLAY)) { - // The alignment here reflects qsee mmu V7L/V8L requirement align = SZ_2M; } else { align = SECURE_ALIGN; @@ -202,7 +200,6 @@ bool IsGPUFlagSupported(uint64_t usage) { return ret; } -// Returns the final buffer size meant to be allocated with ion unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int alignedh) { unsigned int size = 0; int format = info.format; @@ -223,7 +220,6 @@ unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int } else if (IsCompressedRGBFormat(format)) { size = alignedw * alignedh * ASTC_BLOCK_SIZE; } else { - // Below switch should be for only YUV/custom formats switch (format) { case HAL_PIXEL_FORMAT_RAW16: case HAL_PIXEL_FORMAT_Y16: @@ -237,15 +233,15 @@ unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int case HAL_PIXEL_FORMAT_Y8: size = alignedw * alignedh * 1; break; - // adreno formats - case HAL_PIXEL_FORMAT_YCrCb_420_SP_ADRENO: // NV21 + + case HAL_PIXEL_FORMAT_YCrCb_420_SP_ADRENO: size = ALIGN(alignedw * alignedh, SIZE_4K); size += (unsigned int)ALIGN(2 * ALIGN(width / 2, 32) * ALIGN(height / 2, 32), SIZE_4K); break; - case HAL_PIXEL_FORMAT_YCbCr_420_SP_TILED: // NV12 - // The chroma plane is subsampled, - // but the pitch in bytes is unchanged - // The GPU needs 4K alignment, but the video decoder needs 8K + case HAL_PIXEL_FORMAT_YCbCr_420_SP_TILED: + + + size = ALIGN(alignedw * alignedh, SIZE_8K); size += ALIGN(alignedw * (unsigned int)ALIGN(height / 2, 32), SIZE_8K); break; @@ -296,7 +292,6 @@ unsigned int GetSize(const BufferInfo& info, unsigned int alignedw, unsigned int return size; } -// Explicitly defined UBWC formats bool IsUBwcFormat(int format) { switch (format) { case HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC: @@ -309,7 +304,6 @@ bool IsUBwcFormat(int format) { } bool IsUBwcSupported(int format) { - // Existing HAL formats with UBWC support switch (format) { case HAL_PIXEL_FORMAT_BGR_565: case HAL_PIXEL_FORMAT_RGBA_8888: @@ -336,7 +330,6 @@ bool IsUBwcPISupported(int format, uint64_t usage) { return false; } - // As of now only two formats switch (format) { case HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC: case HAL_PIXEL_FORMAT_YCbCr_420_TP10_UBWC: { @@ -354,27 +347,21 @@ bool IsUBwcPISupported(int format, uint64_t usage) { } bool IsUBwcEnabled(int format, uint64_t usage) { - // Allow UBWC, if client is using an explicitly defined UBWC pixel format. if (IsUBwcFormat(format)) { return true; } - // Allow UBWC, if an OpenGL client sets UBWC usage flag and GPU plus MDP - // support the format. OR if a non-OpenGL client like Rotator, sets UBWC - // usage flag and MDP supports the format. if (((usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC) || (usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC_PI) || (usage & BufferUsage::COMPOSER_CLIENT_TARGET)) && IsUBwcSupported(format)) { bool enable = true; - // Query GPU for UBWC only if buffer is intended to be used by GPU. if ((usage & BufferUsage::GPU_TEXTURE) || (usage & BufferUsage::GPU_RENDER_TARGET)) { if (AdrenoMemInfo::GetInstance()) { enable = AdrenoMemInfo::GetInstance()->IsUBWCSupportedByGPU(format); } } - // Allow UBWC, only if CPU usage flags are not set if (enable && !(CpuCanAccess(usage))) { return true; } @@ -418,13 +405,10 @@ unsigned int GetRgbUBwcMetaBufferSize(int width, int height, uint32_t bpp) { return size; } - // Align meta buffer height to 16 blocks meta_height = ALIGN(((height + block_height - 1) / block_height), 16); - // Align meta buffer width to 64 blocks meta_width = ALIGN(((width + block_width - 1) / block_width), 64); - // Align meta buffer size to 4K size = (unsigned int)ALIGN((meta_width * meta_height), 4096); return size; @@ -500,12 +484,10 @@ bool UseUncached(int format, uint64_t usage) { return true; } - // CPU read rarely if ((usage & BufferUsage::CPU_READ_MASK) == static_cast(BufferUsage::CPU_READ_RARELY)) { return true; } - // CPU write rarely if ((usage & BufferUsage::CPU_WRITE_MASK) == static_cast(BufferUsage::CPU_WRITE_RARELY)) { return true; @@ -576,8 +558,6 @@ uint64_t GetHandleFlags(int format, uint64_t usage) { int GetImplDefinedFormat(uint64_t usage, int format) { int gr_format = format; - // If input format is HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED then based on - // the usage bits, gralloc assigns a format. if (format == HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED || format == HAL_PIXEL_FORMAT_YCbCr_420_888) { if ((usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC || usage & GRALLOC_USAGE_PRIVATE_ALLOC_UBWC_PI) && @@ -585,33 +565,30 @@ int GetImplDefinedFormat(uint64_t usage, int format) { gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS_UBWC; } else if (usage & BufferUsage::VIDEO_ENCODER) { if (usage & GRALLOC_USAGE_PRIVATE_VIDEO_NV21_ENCODER) { - gr_format = HAL_PIXEL_FORMAT_NV21_ENCODEABLE; // NV21 + gr_format = HAL_PIXEL_FORMAT_NV21_ENCODEABLE; } else if (usage & GRALLOC_USAGE_PRIVATE_HEIF) { gr_format = HAL_PIXEL_FORMAT_NV12_HEIF; } else if (format == HAL_PIXEL_FORMAT_YCbCr_420_888) { gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS; } else { - gr_format = HAL_PIXEL_FORMAT_NV12_ENCODEABLE; // NV12 + gr_format = HAL_PIXEL_FORMAT_NV12_ENCODEABLE; } } else if (usage & BufferUsage::CAMERA_INPUT) { if (usage & BufferUsage::CAMERA_OUTPUT) { - // Assumed ZSL if both producer and consumer camera flags set - gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; // NV21 + + gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; } else { - gr_format = HAL_PIXEL_FORMAT_YCrCb_420_SP; // NV21 + gr_format = HAL_PIXEL_FORMAT_YCrCb_420_SP; } } else if (usage & BufferUsage::CAMERA_OUTPUT) { if (format == HAL_PIXEL_FORMAT_YCbCr_420_888) { - gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; // NV21 + gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; } else { - gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS; // NV12 preview + gr_format = HAL_PIXEL_FORMAT_YCbCr_420_SP_VENUS; } } else if (usage & BufferUsage::COMPOSER_OVERLAY) { - // XXX: If we still haven't set a format, default to RGBA8888 gr_format = HAL_PIXEL_FORMAT_RGBA_8888; } else if (format == HAL_PIXEL_FORMAT_YCbCr_420_888) { - // If no other usage flags are detected, default the - // flexible YUV format to NV21_ZSL gr_format = HAL_PIXEL_FORMAT_NV21_ZSL; } } @@ -637,7 +614,7 @@ bool HasAlphaComponent(int32_t format) { } void GetRGBPlaneInfo(const BufferInfo& info, int32_t format, int32_t width, int32_t height, - int32_t /* flags */, int* plane_count, PlaneLayoutInfo* plane_info) { + int32_t , int* plane_count, PlaneLayoutInfo* plane_info) { uint64_t usage = info.usage; *plane_count = 1; uint32_t bpp = 0; @@ -659,4 +636,4 @@ void GetRGBPlaneInfo(const BufferInfo& info, int32_t format, int32_t width, int3 plane_info->scanlines = height; } -} // namespace gralloc +} diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.h index 13b21577c..4c757a214 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gr_utils.h @@ -42,7 +42,7 @@ #ifdef SLAVE_SIDE_CP #define SECURE_ALIGN SZ_1M -#else // MASTER_SIDE_CP +#else #define SECURE_ALIGN SZ_4K #endif @@ -68,59 +68,38 @@ inline Type1 ALIGN(Type1 x, Type2 align) { } enum PlaneComponent { - /* luma */ PLANE_COMPONENT_Y = 1 << 0, - /* chroma blue */ PLANE_COMPONENT_Cb = 1 << 1, - /* chroma red */ PLANE_COMPONENT_Cr = 1 << 2, - /* red */ PLANE_COMPONENT_R = 1 << 10, - /* green */ PLANE_COMPONENT_G = 1 << 11, - /* blue */ PLANE_COMPONENT_B = 1 << 12, - /* alpha */ PLANE_COMPONENT_A = 1 << 20, - /* raw data plane */ PLANE_COMPONENT_RAW = 1 << 30, - /* meta information plane */ PLANE_COMPONENT_META = 1 << 31, }; struct PlaneLayoutInfo { - /** Components represented the type of plane. */ PlaneComponent component; - /** horizontal subsampling. Must be a positive power of 2. */ uint32_t h_subsampling; - /** vertical subsampling. Must be a positive power of 2. */ uint32_t v_subsampling; - /** offset to the first byte of the top-left pixel of the plane - * and it is calculated from the start of the buffer. - * Add base of the handle with offset to get the first byte of the plane. - */ uint32_t offset; - /** step is the distance in bytes from one pixel value to the next. */ int32_t step; - /** stride of the plane in pixels */ int32_t stride; - /** stride of the plane in in bytes */ int32_t stride_bytes; - /** plane height or vertical stride */ int32_t scanlines; - /** size of the plane in bytes */ uint32_t size; }; @@ -151,6 +130,6 @@ int GetImplDefinedFormat(uint64_t usage, int format); int GetCustomFormatFlags(int format, uint64_t usage, int* custom_format, uint64_t* priv_flags); bool IsGPUFlagSupported(uint64_t usage); bool HasAlphaComponent(int32_t format); -} // namespace gralloc +} -#endif // __GR_UTILS_H__ +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gralloc_priv.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gralloc_priv.h index 8a333a6bd..0e4359e5e 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gralloc_priv.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/gralloc_priv.h @@ -26,7 +26,6 @@ #include "QtiGrallocDefs.h" #include "gr_priv_handle.h" -//#pragma message "Warning: gralloc_priv.h is deprecated" #define GRALLOC_PROP_PREFIX "vendor.gralloc." #define GRALLOC_PROP(prop_name) GRALLOC_PROP_PREFIX prop_name @@ -42,8 +41,6 @@ inline int roundUpToPageSize(int x) { return (x + (getpagesize() - 1)) & ~(getpagesize() - 1); } -/* Legacy gralloc1 definitions */ -/* Some clients may still be using the old flags */ #define GRALLOC1_PRODUCER_USAGE_PRIVATE_ADSP_HEAP GRALLOC_USAGE_PRIVATE_ADSP_HEAP #define GRALLOC1_PRODUCER_USAGE_PRIVATE_ALLOC_UBWC GRALLOC_USAGE_PRIVATE_ALLOC_UBWC #define GRALLOC1_PRODUCER_USAGE_PRIVATE_UNCACHED GRALLOC_USAGE_PRIVATE_UNCACHED @@ -54,7 +51,6 @@ inline int roundUpToPageSize(int x) { #define GRALLOC1_CONSUMER_USAGE_PRIVATE_10BIT_TP GRALLOC_USAGE_PRIVATE_10BIT_TP #define GRALLOC1_PRODUCER_USAGE_PRIVATE_VIDEO_NV21_ENCODER GRALLOC_USAGE_PRIVATE_VIDEO_NV21_ENCODER -// for PERFORM API : #define GRALLOC_MODULE_PERFORM_CREATE_HANDLE_FROM_BUFFER 1 #define GRALLOC_MODULE_PERFORM_GET_STRIDE 2 #define GRALLOC_MODULE_PERFORM_GET_CUSTOM_STRIDE_FROM_HANDLE 3 @@ -72,11 +68,9 @@ inline int roundUpToPageSize(int x) { #define GRALLOC1_MODULE_PERFORM_GET_INTERLACE_FLAG 15 #define GRALLOC_MODULE_PERFORM_GET_GRAPHICS_METADATA 16 -/* possible values for inverse gamma correction */ #define HAL_IGC_NOT_SPECIFIED 0 #define HAL_IGC_s_RGB 1 -/* Color Space: Values maps to ColorSpace_t in qdMetadata.h */ #define HAL_CSC_ITU_R_601 0 #define HAL_CSC_ITU_R_601_FR 1 #define HAL_CSC_ITU_R_709 2 @@ -84,20 +78,16 @@ inline int roundUpToPageSize(int x) { #define HAL_CSC_ITU_R_2020 4 #define HAL_CSC_ITU_R_2020_FR 5 -/* possible formats for 3D content*/ enum { HAL_NO_3D = 0x0, HAL_3D_SIDE_BY_SIDE_L_R = 0x1, HAL_3D_SIDE_BY_SIDE_R_L = 0x2, HAL_3D_TOP_BOTTOM = 0x4, - HAL_3D_IN_SIDE_BY_SIDE_L_R = 0x10000, // unused legacy format + HAL_3D_IN_SIDE_BY_SIDE_L_R = 0x10000, }; -/* Flag to determine interlaced content - * Value maps to Flags presents in types.hal of QtiMapperextensions - */ enum { LAYOUT_INTERLACED_FLAG = 1 << 0, }; -#endif // __GRALLOC_PRIV_H__ +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapper.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapper.h index ae3bdbba9..0325e8641 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapper.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapper.h @@ -60,43 +60,43 @@ struct IMapper : public IBase { int32_t height __attribute__((aligned(4))); }; - static sp getService(const std::string& /* serviceName */, const bool /* getStub */); + static sp getService(const std::string& , const bool ); }; -} // namespace V2_0 +} namespace V3_0 { struct IMapper : public IBase { - static sp getService(const std::string& /* serviceName */, const bool /* getStub */); + static sp getService(const std::string& , const bool ); }; -} // namespace V3_0 +} namespace V4_0 { struct IMapper : public IBase { - static sp getService(const std::string& /* serviceName */, const bool /* getStub */); + static sp getService(const std::string& , const bool ); }; -} // namespace V4_0 -} // namespace android::hardware::graphics::mapper +} +} namespace vendor::qti::hardware::display::mapper { namespace V4_0 { struct IQtiMapper : public IBase { - static Return> castFrom(const sp& /* parent */, - bool /* emitError */); + static Return> castFrom(const sp& , + bool ); }; -} // namespace V4_0 +} namespace V3_0 { struct IQtiMapper : public IBase { - static Return> castFrom(const sp& /* parent */, - bool /* emitError */); + static Return> castFrom(const sp& , + bool ); }; -} // namespace V3_0 +} namespace V2_0 { struct IQtiMapper : public IBase { @@ -175,9 +175,9 @@ struct IQtiMapper : public IBase { V1_0::IQtiMapperExtensions>& extensions)>; virtual Return getMapperExtensions(getMapperExtensions_cb _hidl_cb); - static Return> castFrom(const sp& /* parent */, - bool /* emitError */); + static Return> castFrom(const sp& , + bool ); }; -} // namespace V2_0 -} // namespace vendor::qti::hardware::display::mapper +} +} diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapperextensions.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapperextensions.h index 078cb90b2..1337627fb 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapperextensions.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/mapperextensions.h @@ -45,7 +45,7 @@ struct IQtiMapperExtensions : public IBase { IQtiMapperExtensions(){}; }; -} // namespace V1_0 +} namespace V1_1 { using Error = V1_0::Error; using PlaneLayout = V1_0::PlaneLayout; @@ -64,7 +64,6 @@ struct IQtiMapperExtensions : public IBase { std::terminate(); } - // Used, passthrough using getInterlacedFlag_cb = std::function; virtual ::android::hardware::Return getInterlacedFlag(void* buffer, @@ -79,7 +78,6 @@ struct IQtiMapperExtensions : public IBase { std::terminate(); } - // Used, passthrough using getRgbDataAddress_cb = std::function; virtual ::android::hardware::Return getRgbDataAddress(void* buffer, @@ -95,7 +93,6 @@ struct IQtiMapperExtensions : public IBase { std::terminate(); } - // Used, passhthrough using getColorSpace_cb = std::function; virtual ::android::hardware::Return getColorSpace(void* buffer, getColorSpace_cb _hidl_cb) { @@ -103,7 +100,6 @@ struct IQtiMapperExtensions : public IBase { std::terminate(); } - // Used, passthrough with hidl fixups using getYuvPlaneInfo_cb = std::function; virtual ::android::hardware::Return getFd(void* buffer, getFd_cb _hidl_cb); - // Used, cast private data with no passthrough using getWidth_cb = std::function; virtual ::android::hardware::Return getWidth(void* buffer, getWidth_cb _hidl_cb); - // Used, cast private data with no passthrough using getHeight_cb = std::function; virtual ::android::hardware::Return getHeight(void* buffer, getHeight_cb _hidl_cb); - // Used, cast private data with no passthrough using getOffset_cb = std::function; virtual ::android::hardware::Return getOffset(void* buffer, getOffset_cb _hidl_cb); - // Used, cast private data with no passthrough using getSize_cb = std::function; virtual ::android::hardware::Return getSize(void* buffer, getSize_cb _hidl_cb); @@ -179,12 +170,10 @@ struct IQtiMapperExtensions : public IBase { ::vendor::qti::hardware::display::mapperextensions::V1_0::Error error, uint64_t usage)>; virtual ::android::hardware::Return getUsageFlags(void* buffer, getUsageFlags_cb _hidl_cb); - // Used, cast private data with no passthrough using getFormat_cb = std::function; virtual ::android::hardware::Return getFormat(void* buffer, getFormat_cb _hidl_cb); - // Used, cast private data with no passthrough using getPrivateFlags_cb = std::function; virtual ::android::hardware::Return getPrivateFlags(void* buffer, @@ -198,7 +187,6 @@ struct IQtiMapperExtensions : public IBase { std::terminate(); } - // Used, PAIN using getFormatLayout_cb = std::function getSurfaceMetadata_V1(void* buffer, void* metadata) { @@ -217,8 +204,8 @@ struct IQtiMapperExtensions : public IBase { } static Return> castFrom( - android::sp const& /* parent */, bool /* emitError */); + android::sp const& , bool ); }; -} // namespace V1_1 -} // namespace vendor::qti::hardware::display::mapperextensions +} +} diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/common.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/common.h index 6f327baf3..64daacb5d 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/common.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/common.h @@ -12,7 +12,6 @@ namespace graphics { namespace common { namespace V1_0 { -// Forward declaration for forward reference support: enum class PixelFormat : int32_t; enum class BufferUsage : uint64_t; enum class Transform : int32_t; @@ -21,1402 +20,1072 @@ enum class ColorMode : int32_t; enum class ColorTransform : int32_t; enum class Hdr : int32_t; -/** - * Common enumeration and structure definitions for all graphics HALs. - * - * - * Pixel formats for graphics buffers. - */ enum class PixelFormat : int32_t { - /** - * 32-bit format that has 8-bit R, G, B, and A components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGBA_8888 = 1 /* 0x1 */, - /** - * 32-bit format that has 8-bit R, G, B, and unused components, in that - * order, from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGBX_8888 = 2 /* 0x2 */, - /** - * 24-bit format that has 8-bit R, G, and B components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGB_888 = 3 /* 0x3 */, - /** - * 16-bit packed format that has 5-bit R, 6-bit G, and 5-bit B components, - * in that order, from the most-sigfinicant bits to the least-significant - * bits. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGB_565 = 4 /* 0x4 */, - /** - * 32-bit format that has 8-bit B, G, R, and A components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - BGRA_8888 = 5 /* 0x5 */, - /** - * Legacy formats deprecated in favor of YCBCR_420_888. - */ - YCBCR_422_SP = 16 /* 0x10 */, - YCRCB_420_SP = 17 /* 0x11 */, - YCBCR_422_I = 20 /* 0x14 */, - /** - * 64-bit format that has 16-bit R, G, B, and A components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are signed floats, whose interpretation is defined - * by the dataspace. - */ - RGBA_FP16 = 22 /* 0x16 */, - /** - * RAW16 is a single-channel, 16-bit, little endian format, typically - * representing raw Bayer-pattern images from an image sensor, with minimal - * processing. - * - * The exact pixel layout of the data in the buffer is sensor-dependent, and - * needs to be queried from the camera device. - * - * Generally, not all 16 bits are used; more common values are 10 or 12 - * bits. If not all bits are used, the lower-order bits are filled first. - * All parameters to interpret the raw data (black and white points, - * color space, etc) must be queried from the camera device. - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - strides are specified in pixels, not in bytes - * - * size = stride * height * 2 - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW16 is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data, layout is as - * | defined above. - * Dataspace::DEPTH | Unprocessed implementation-dependent raw - * | depth measurements, opaque with 16 bit - * | samples. - * Other | Unsupported - */ - RAW16 = 32 /* 0x20 */, - /** - * BLOB is used to carry task-specific data which does not have a standard - * image structure. The details of the format are left to the two - * endpoints. - * - * A typical use case is for transporting JPEG-compressed images from the - * Camera HAL to the framework or to applications. - * - * Buffers of this format must have a height of 1, and width equal to their - * size in bytes. - * - * The mapping of the dataspace to buffer contents for BLOB is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::JFIF | An encoded JPEG image - * Dataspace::DEPTH | An android_depth_points buffer - * Dataspace::SENSOR | Sensor event data. - * Other | Unsupported - */ - BLOB = 33 /* 0x21 */, - /** - * A format indicating that the choice of format is entirely up to the - * allocator. - * - * The allocator should examine the usage bits passed in when allocating a - * buffer with this format, and it should derive the pixel format from - * those usage flags. This format must never be used with any of the - * BufferUsage::CPU_* usage flags. - * - * Even when the internally chosen format has an alpha component, the - * clients must assume the alpha vlaue to be 1.0. - * - * The interpretation of the component values is defined by the dataspace. - */ - IMPLEMENTATION_DEFINED = 34 /* 0x22 */, - /** - * This format allows platforms to use an efficient YCbCr/YCrCb 4:2:0 - * buffer layout, while still describing the general format in a - * layout-independent manner. While called YCbCr, it can be used to - * describe formats with either chromatic ordering, as well as - * whole planar or semiplanar layouts. - * - * This format must be accepted by the allocator when BufferUsage::CPU_* - * are set. - * - * Buffers with this format must be locked with IMapper::lockYCbCr. - * Locking with IMapper::lock must return an error. - * - * The interpretation of the component values is defined by the dataspace. - */ - YCBCR_420_888 = 35 /* 0x23 */, - /** - * RAW_OPAQUE is a format for unprocessed raw image buffers coming from an - * image sensor. The actual structure of buffers of this format is - * implementation-dependent. - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW_OPAQUE is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data. - * Other | Unsupported - */ - RAW_OPAQUE = 36 /* 0x24 */, - /** - * RAW10 is a single-channel, 10-bit per pixel, densely packed in each row, - * unprocessed format, usually representing raw Bayer-pattern images coming from - * an image sensor. - * - * In an image buffer with this format, starting from the first pixel of each - * row, each 4 consecutive pixels are packed into 5 bytes (40 bits). Each one - * of the first 4 bytes contains the top 8 bits of each pixel, The fifth byte - * contains the 2 least significant bits of the 4 pixels, the exact layout data - * for each 4 consecutive pixels is illustrated below (Pi[j] stands for the jth - * bit of the ith pixel): - * - * bit 7 bit 0 - * =====|=====|=====|=====|=====|=====|=====|=====| - * Byte 0: |P0[9]|P0[8]|P0[7]|P0[6]|P0[5]|P0[4]|P0[3]|P0[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 1: |P1[9]|P1[8]|P1[7]|P1[6]|P1[5]|P1[4]|P1[3]|P1[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 2: |P2[9]|P2[8]|P2[7]|P2[6]|P2[5]|P2[4]|P2[3]|P2[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 3: |P3[9]|P3[8]|P3[7]|P3[6]|P3[5]|P3[4]|P3[3]|P3[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 4: |P3[1]|P3[0]|P2[1]|P2[0]|P1[1]|P1[0]|P0[1]|P0[0]| - * =============================================== - * - * This format assumes - * - a width multiple of 4 pixels - * - an even height - * - a vertical stride equal to the height - * - strides are specified in bytes, not in pixels - * - * size = stride * height - * - * When stride is equal to width * (10 / 8), there will be no padding bytes at - * the end of each row, the entire image data is densely packed. When stride is - * larger than width * (10 / 8), padding bytes will be present at the end of each - * row (including the last row). - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW10 is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data. - * Other | Unsupported - */ - RAW10 = 37 /* 0x25 */, - /** - * RAW12 is a single-channel, 12-bit per pixel, densely packed in each row, - * unprocessed format, usually representing raw Bayer-pattern images coming from - * an image sensor. - * - * In an image buffer with this format, starting from the first pixel of each - * row, each two consecutive pixels are packed into 3 bytes (24 bits). The first - * and second byte contains the top 8 bits of first and second pixel. The third - * byte contains the 4 least significant bits of the two pixels, the exact layout - * data for each two consecutive pixels is illustrated below (Pi[j] stands for - * the jth bit of the ith pixel): - * - * bit 7 bit 0 - * ======|======|======|======|======|======|======|======| - * Byte 0: |P0[11]|P0[10]|P0[ 9]|P0[ 8]|P0[ 7]|P0[ 6]|P0[ 5]|P0[ 4]| - * |------|------|------|------|------|------|------|------| - * Byte 1: |P1[11]|P1[10]|P1[ 9]|P1[ 8]|P1[ 7]|P1[ 6]|P1[ 5]|P1[ 4]| - * |------|------|------|------|------|------|------|------| - * Byte 2: |P1[ 3]|P1[ 2]|P1[ 1]|P1[ 0]|P0[ 3]|P0[ 2]|P0[ 1]|P0[ 0]| - * ======================================================= - * - * This format assumes: - * - a width multiple of 4 pixels - * - an even height - * - a vertical stride equal to the height - * - strides are specified in bytes, not in pixels - * - * size = stride * height - * - * When stride is equal to width * (12 / 8), there will be no padding bytes at - * the end of each row, the entire image data is densely packed. When stride is - * larger than width * (12 / 8), padding bytes will be present at the end of - * each row (including the last row). - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW12 is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data. - * Other | Unsupported - */ - RAW12 = 38 /* 0x26 */, - /** - * 0x27 to 0x2A are reserved for flexible formats - * - * - * 32-bit packed format that has 2-bit A, 10-bit B, G, and R components, - * in that order, from the most-sigfinicant bits to the least-significant - * bits. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGBA_1010102 = 43 /* 0x2B */, - /** - * 0x100 - 0x1FF - * - * This range is reserved for vendor extensions. Formats in this range - * must support BufferUsage::GPU_TEXTURE. Clients must assume they do not - * have an alpha component. - * - * - * Y8 is a YUV planar format comprised of a WxH Y plane, with each pixel - * being represented by 8 bits. It is equivalent to just the Y plane from - * YV12. - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - * size = stride * height - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - Y8 = 538982489 /* 0x20203859 */, - /** - * Y16 is a YUV planar format comprised of a WxH Y plane, with each pixel - * being represented by 16 bits. It is just like Y8, but has double the - * bits per pixel (little endian). - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - strides are specified in pixels, not in bytes - * - * size = stride * height * 2 - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. When the dataspace is - * Dataspace::DEPTH, each pixel is a distance value measured by a depth - * camera, plus an associated confidence value. - */ - Y16 = 540422489 /* 0x20363159 */, - /** - * YV12 is a 4:2:0 YCrCb planar format comprised of a WxH Y plane followed - * by (W/2) x (H/2) Cr and Cb planes. - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - * y_size = stride * height - * c_stride = ALIGN(stride/2, 16) - * c_size = c_stride * height/2 - * size = y_size + c_size * 2 - * cr_offset = y_size - * cb_offset = y_size + c_size - * - * This range is reserved for vendor extensions. Formats in this range - * must support BufferUsage::GPU_TEXTURE. Clients must assume they do not - * have an alpha component. - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::GPU_TEXTURE - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - YV12 = 842094169 /* 0x32315659 */, + + + + + + + + RGBA_8888 = 1 , + + + + + + + + RGBX_8888 = 2 , + + + + + + + + RGB_888 = 3 , + + + + + + + + + RGB_565 = 4 , + + + + + + + + BGRA_8888 = 5 , + + + + YCBCR_422_SP = 16 , + YCRCB_420_SP = 17 , + YCBCR_422_I = 20 , + + + + + + + + RGBA_FP16 = 22 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RAW16 = 32 , + + + + + + + + + + + + + + + + + + + + + + BLOB = 33 , + + + + + + + + + + + + + + + IMPLEMENTATION_DEFINED = 34 , + + + + + + + + + + + + + + + + YCBCR_420_888 = 35 , + + + + + + + + + + + + + + + + + + + + + RAW_OPAQUE = 36 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RAW10 = 37 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RAW12 = 38 , + + + + + + + + + + + + RGBA_1010102 = 43 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Y8 = 538982489 , + + + + + + + + + + + + + + + + + + + + + + + + + + Y16 = 540422489 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + YV12 = 842094169 , }; -/** - * Buffer usage definitions. - */ enum class BufferUsage : uint64_t { - /** - * bit 0-3 is an enum - */ - CPU_READ_MASK = 15ull /* 0xfULL */, - /** - * buffer is never read by CPU - */ + + + + CPU_READ_MASK = 15ull , + + + CPU_READ_NEVER = 0ull, - /** - * buffer is rarely read by CPU - */ CPU_READ_RARELY = 2ull, - /** - * buffer is often read by CPU - */ CPU_READ_OFTEN = 3ull, - /** - * bit 4-7 is an enum - */ - CPU_WRITE_MASK = 240ull /* (0xfULL << 4) */, - /** - * buffer is never written by CPU - */ - CPU_WRITE_NEVER = 0ull /* (0 << 4) */, - /** - * buffer is rarely written by CPU - */ - CPU_WRITE_RARELY = 32ull /* (2 << 4) */, - /** - * buffer is often written by CPU - */ - CPU_WRITE_OFTEN = 48ull /* (3 << 4) */, - /** - * buffer is used as a GPU texture - */ - GPU_TEXTURE = 256ull /* (1ULL << 8) */, - /** - * buffer is used as a GPU render target - */ - GPU_RENDER_TARGET = 512ull /* (1ULL << 9) */, - /** - * bit 10 must be zero - * - * - * buffer is used as a composer HAL overlay layer - */ - COMPOSER_OVERLAY = 2048ull /* (1ULL << 11) */, - /** - * buffer is used as a composer HAL client target - */ - COMPOSER_CLIENT_TARGET = 4096ull /* (1ULL << 12) */, - /** - * bit 13 must be zero - * - * - * Buffer is allocated with hardware-level protection against copying the - * contents (or information derived from the contents) into unprotected - * memory. - */ - PROTECTED = 16384ull /* (1ULL << 14) */, - /** - * buffer is used as a hwcomposer HAL cursor layer - */ - COMPOSER_CURSOR = 32768ull /* (1ULL << 15) */, - /** - * buffer is used as a video encoder input - */ - VIDEO_ENCODER = 65536ull /* (1ULL << 16) */, - /** - * buffer is used as a camera HAL output - */ - CAMERA_OUTPUT = 131072ull /* (1ULL << 17) */, - /** - * buffer is used as a camera HAL input - */ - CAMERA_INPUT = 262144ull /* (1ULL << 18) */, - /** - * bit 19 must be zero - * - * - * buffer is used as a renderscript allocation - */ - RENDERSCRIPT = 1048576ull /* (1ULL << 20) */, - /** - * bit 21 must be zero - * - * - * buffer is used as a video decoder output - */ - VIDEO_DECODER = 4194304ull /* (1ULL << 22) */, - /** - * buffer is used as a sensor direct report output - */ - SENSOR_DIRECT_DATA = 8388608ull /* (1ULL << 23) */, - /** - * buffer is used as as an OpenGL shader storage or uniform - * buffer object - */ - GPU_DATA_BUFFER = 16777216ull /* (1ULL << 24) */, - /** - * bits 25-27 must be zero and are reserved for future versions - * - * - * bits 28-31 are reserved for vendor extensions - */ - VENDOR_MASK = 4026531840ull /* (0xfULL << 28) */, - /** - * bits 32-47 must be zero and are reserved for future versions - * - * - * bits 48-63 are reserved for vendor extensions - */ - VENDOR_MASK_HI = 18446462598732840960ull /* (0xffffULL << 48) */, + + + + CPU_WRITE_MASK = 240ull , + + + + CPU_WRITE_NEVER = 0ull , + + + + CPU_WRITE_RARELY = 32ull , + + + + CPU_WRITE_OFTEN = 48ull , + + + + GPU_TEXTURE = 256ull , + + + + GPU_RENDER_TARGET = 512ull , + + + + + + + COMPOSER_OVERLAY = 2048ull , + + + + COMPOSER_CLIENT_TARGET = 4096ull , + + + + + + + + + PROTECTED = 16384ull , + + + + COMPOSER_CURSOR = 32768ull , + + + + VIDEO_ENCODER = 65536ull , + + + + CAMERA_OUTPUT = 131072ull , + + + + CAMERA_INPUT = 262144ull , + + + + + + + RENDERSCRIPT = 1048576ull , + + + + + + + VIDEO_DECODER = 4194304ull , + + + + SENSOR_DIRECT_DATA = 8388608ull , + + + + + GPU_DATA_BUFFER = 16777216ull , + + + + + + + VENDOR_MASK = 4026531840ull , + + + + + + + VENDOR_MASK_HI = 18446462598732840960ull , }; -/** - * Transformation definitions - */ enum class Transform : int32_t { - /** - * Horizontal flip. FLIP_H/FLIP_V is applied before ROT_90. - */ - FLIP_H = 1 /* (1 << 0) */, - /** - * Vertical flip. FLIP_H/FLIP_V is applied before ROT_90. - */ - FLIP_V = 2 /* (1 << 1) */, - /** - * 90 degree clockwise rotation. FLIP_H/FLIP_V is applied before ROT_90. - */ - ROT_90 = 4 /* (1 << 2) */, - /** - * Commonly used combinations. - */ - ROT_180 = 3 /* (FLIP_H | FLIP_V) */, - ROT_270 = 7 /* ((FLIP_H | FLIP_V) | ROT_90) */, + + + + FLIP_H = 1 , + + + + FLIP_V = 2 , + + + + ROT_90 = 4 , + + + + ROT_180 = 3 , + ROT_270 = 7 , }; -/** - * Dataspace Definitions - * ====================== - * - * Dataspace is the definition of how pixel values should be interpreted. - * - * For many formats, this is the colorspace of the image data, which includes - * primaries (including white point) and the transfer characteristic function, - * which describes both gamma curve and numeric range (within the bit depth). - * - * Other dataspaces include depth measurement data from a depth camera. - * - * A dataspace is comprised of a number of fields. - * - * Version - * -------- - * The top 2 bits represent the revision of the field specification. This is - * currently always 0. - * - * - * bits 31-30 29 - 0 - * +-----+----------------------------------------------------+ - * fields | Rev | Revision specific fields | - * +-----+----------------------------------------------------+ - * - * Field layout for version = 0: - * ---------------------------- - * - * A dataspace is comprised of the following fields: - * Standard - * Transfer function - * Range - * - * bits 31-30 29-27 26 - 22 21 - 16 15 - 0 - * +-----+-----+--------+--------+----------------------------+ - * fields | 0 |Range|Transfer|Standard| Legacy and custom | - * +-----+-----+--------+--------+----------------------------+ - * VV RRR TTTTT SSSSSS LLLLLLLL LLLLLLLL - * - * If range, transfer and standard fields are all 0 (e.g. top 16 bits are - * all zeroes), the bottom 16 bits contain either a legacy dataspace value, - * or a custom value. - */ enum class Dataspace : int32_t { - /** - * Default-assumption data space, when not explicitly specified. - * - * It is safest to assume the buffer is an image with sRGB primaries and - * encoding ranges, but the consumer and/or the producer of the data may - * simply be using defaults. No automatic gamma transform should be - * expected, except for a possible display gamma transform when drawn to a - * screen. - */ - UNKNOWN = 0 /* 0x0 */, - /** - * Arbitrary dataspace with manually defined characteristics. Definition - * for colorspaces or other meaning must be communicated separately. - * - * This is used when specifying primaries, transfer characteristics, - * etc. separately. - * - * A typical use case is in video encoding parameters (e.g. for H.264), - * where a colorspace can have separately defined primaries, transfer - * characteristics, etc. - */ - ARBITRARY = 1 /* 0x1 */, - /** - * Color-description aspects - * - * The following aspects define various characteristics of the color - * specification. These represent bitfields, so that a data space value - * can specify each of them independently. - */ + + + + + + + + + + UNKNOWN = 0 , + + + + + + + + + + + + ARBITRARY = 1 , + + + + + + + STANDARD_SHIFT = 16, - /** - * Standard aspect - * - * Defines the chromaticity coordinates of the source primaries in terms of - * the CIE 1931 definition of x and y specified in ISO 11664-1. - */ - STANDARD_MASK = 4128768 /* (63 << STANDARD_SHIFT) */, - /** - * Chromacity coordinates are unknown or are determined by the application. - * Implementations shall use the following suggested standards: - * - * All YCbCr formats: BT709 if size is 720p or larger (since most video - * content is letterboxed this corresponds to width is - * 1280 or greater, or height is 720 or greater). - * BT601_625 if size is smaller than 720p or is JPEG. - * All RGB formats: BT709. - * - * For all other formats standard is undefined, and implementations should use - * an appropriate standard for the data represented. - */ - STANDARD_UNSPECIFIED = 0 /* (0 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.300 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.2126, KB = 0.0722 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT709 = 65536 /* (1 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - */ - STANDARD_BT601_625 = 131072 /* (2 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.222, KB = 0.071 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT601_625_UNADJUSTED = 196608 /* (3 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - */ - STANDARD_BT601_525 = 262144 /* (4 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.212, KB = 0.087 luminance interpretation - * for RGB conversion (as in SMPTE 240M). - */ - STANDARD_BT601_525_UNADJUSTED = 327680 /* (5 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.170 0.797 - * blue 0.131 0.046 - * red 0.708 0.292 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.2627, KB = 0.0593 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT2020 = 393216 /* (6 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.170 0.797 - * blue 0.131 0.046 - * red 0.708 0.292 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.2627, KB = 0.0593 luminance interpretation - * for RGB conversion using the linear domain. - */ - STANDARD_BT2020_CONSTANT_LUMINANCE = 458752 /* (7 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.21 0.71 - * blue 0.14 0.08 - * red 0.67 0.33 - * white (C) 0.310 0.316 - * - * Use the unadjusted KR = 0.30, KB = 0.11 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT470M = 524288 /* (8 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.243 0.692 - * blue 0.145 0.049 - * red 0.681 0.319 - * white (C) 0.310 0.316 - * - * Use the unadjusted KR = 0.254, KB = 0.068 luminance interpretation - * for RGB conversion. - */ - STANDARD_FILM = 589824 /* (9 << STANDARD_SHIFT) */, - /** - * SMPTE EG 432-1 and SMPTE RP 431-2. (DCI-P3) - * Primaries: x y - * green 0.265 0.690 - * blue 0.150 0.060 - * red 0.680 0.320 - * white (D65) 0.3127 0.3290 - */ - STANDARD_DCI_P3 = 655360 /* (10 << STANDARD_SHIFT) */, - /** - * Adobe RGB - * Primaries: x y - * green 0.210 0.710 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - */ - STANDARD_ADOBE_RGB = 720896 /* (11 << STANDARD_SHIFT) */, + + + + + + + STANDARD_MASK = 4128768 , + + + + + + + + + + + + + + STANDARD_UNSPECIFIED = 0 , + + + + + + + + + + + STANDARD_BT709 = 65536 , + + + + + + + + + + + + + STANDARD_BT601_625 = 131072 , + + + + + + + + + + + STANDARD_BT601_625_UNADJUSTED = 196608 , + + + + + + + + + + + + + STANDARD_BT601_525 = 262144 , + + + + + + + + + + + STANDARD_BT601_525_UNADJUSTED = 327680 , + + + + + + + + + + + STANDARD_BT2020 = 393216 , + + + + + + + + + + + STANDARD_BT2020_CONSTANT_LUMINANCE = 458752 , + + + + + + + + + + + STANDARD_BT470M = 524288 , + + + + + + + + + + + STANDARD_FILM = 589824 , + + + + + + + + + STANDARD_DCI_P3 = 655360 , + + + + + + + + + STANDARD_ADOBE_RGB = 720896 , TRANSFER_SHIFT = 22, - /** - * Transfer aspect - * - * Transfer characteristics are the opto-electronic transfer characteristic - * at the source as a function of linear optical intensity (luminance). - * - * For digital signals, E corresponds to the recorded value. Normally, the - * transfer function is applied in RGB space to each of the R, G and B - * components independently. This may result in color shift that can be - * minized by applying the transfer function in Lab space only for the L - * component. Implementation may apply the transfer function in RGB space - * for all pixel formats if desired. - */ - TRANSFER_MASK = 130023424 /* (31 << TRANSFER_SHIFT) */, - /** - * Transfer characteristics are unknown or are determined by the - * application. - * - * Implementations should use the following transfer functions: - * - * For YCbCr formats: use TRANSFER_SMPTE_170M - * For RGB formats: use TRANSFER_SRGB - * - * For all other formats transfer function is undefined, and implementations - * should use an appropriate standard for the data represented. - */ - TRANSFER_UNSPECIFIED = 0 /* (0 << TRANSFER_SHIFT) */, - /** - * Transfer characteristic curve: - * E = L - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_LINEAR = 4194304 /* (1 << TRANSFER_SHIFT) */, - /** - * Transfer characteristic curve: - * - * E = 1.055 * L^(1/2.4) - 0.055 for 0.0031308 <= L <= 1 - * = 12.92 * L for 0 <= L < 0.0031308 - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_SRGB = 8388608 /* (2 << TRANSFER_SHIFT) */, - /** - * BT.601 525, BT.601 625, BT.709, BT.2020 - * - * Transfer characteristic curve: - * E = 1.099 * L ^ 0.45 - 0.099 for 0.018 <= L <= 1 - * = 4.500 * L for 0 <= L < 0.018 - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_SMPTE_170M = 12582912 /* (3 << TRANSFER_SHIFT) */, - /** - * Assumed display gamma 2.2. - * - * Transfer characteristic curve: - * E = L ^ (1/2.2) - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_GAMMA2_2 = 16777216 /* (4 << TRANSFER_SHIFT) */, - /** - * display gamma 2.6. - * - * Transfer characteristic curve: - * E = L ^ (1/2.6) - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_GAMMA2_6 = 20971520 /* (5 << TRANSFER_SHIFT) */, - /** - * display gamma 2.8. - * - * Transfer characteristic curve: - * E = L ^ (1/2.8) - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_GAMMA2_8 = 25165824 /* (6 << TRANSFER_SHIFT) */, - /** - * SMPTE ST 2084 (Dolby Perceptual Quantizer) - * - * Transfer characteristic curve: - * E = ((c1 + c2 * L^n) / (1 + c3 * L^n)) ^ m - * c1 = c3 - c2 + 1 = 3424 / 4096 = 0.8359375 - * c2 = 32 * 2413 / 4096 = 18.8515625 - * c3 = 32 * 2392 / 4096 = 18.6875 - * m = 128 * 2523 / 4096 = 78.84375 - * n = 0.25 * 2610 / 4096 = 0.1593017578125 - * L - luminance of image 0 <= L <= 1 for HDR colorimetry. - * L = 1 corresponds to 10000 cd/m2 - * E - corresponding electrical signal - */ - TRANSFER_ST2084 = 29360128 /* (7 << TRANSFER_SHIFT) */, - /** - * ARIB STD-B67 Hybrid Log Gamma - * - * Transfer characteristic curve: - * E = r * L^0.5 for 0 <= L <= 1 - * = a * ln(L - b) + c for 1 < L - * a = 0.17883277 - * b = 0.28466892 - * c = 0.55991073 - * r = 0.5 - * L - luminance of image 0 <= L for HDR colorimetry. L = 1 corresponds - * to reference white level of 100 cd/m2 - * E - corresponding electrical signal - */ - TRANSFER_HLG = 33554432 /* (8 << TRANSFER_SHIFT) */, + + + + + + + + + + + + + + TRANSFER_MASK = 130023424 , + + + + + + + + + + + + + TRANSFER_UNSPECIFIED = 0 , + + + + + + + TRANSFER_LINEAR = 4194304 , + + + + + + + + + TRANSFER_SRGB = 8388608 , + + + + + + + + + + TRANSFER_SMPTE_170M = 12582912 , + + + + + + + + + TRANSFER_GAMMA2_2 = 16777216 , + + + + + + + + + TRANSFER_GAMMA2_6 = 20971520 , + + + + + + + + + TRANSFER_GAMMA2_8 = 25165824 , + + + + + + + + + + + + + + + TRANSFER_ST2084 = 29360128 , + + + + + + + + + + + + + + + TRANSFER_HLG = 33554432 , RANGE_SHIFT = 27, - /** - * Range aspect - * - * Defines the range of values corresponding to the unit range of 0-1. - * This is defined for YCbCr only, but can be expanded to RGB space. - */ - RANGE_MASK = 939524096 /* (7 << RANGE_SHIFT) */, - /** - * Range is unknown or are determined by the application. Implementations - * shall use the following suggested ranges: - * - * All YCbCr formats: limited range. - * All RGB or RGBA formats (including RAW and Bayer): full range. - * All Y formats: full range - * - * For all other formats range is undefined, and implementations should use - * an appropriate range for the data represented. - */ - RANGE_UNSPECIFIED = 0 /* (0 << RANGE_SHIFT) */, - /** - * Full range uses all values for Y, Cb and Cr from - * 0 to 2^b-1, where b is the bit depth of the color format. - */ - RANGE_FULL = 134217728 /* (1 << RANGE_SHIFT) */, - /** - * Limited range uses values 16/256*2^b to 235/256*2^b for Y, and - * 1/16*2^b to 15/16*2^b for Cb, Cr, R, G and B, where b is the bit depth of - * the color format. - * - * E.g. For 8-bit-depth formats: - * Luma (Y) samples should range from 16 to 235, inclusive - * Chroma (Cb, Cr) samples should range from 16 to 240, inclusive - * - * For 10-bit-depth formats: - * Luma (Y) samples should range from 64 to 940, inclusive - * Chroma (Cb, Cr) samples should range from 64 to 960, inclusive - */ - RANGE_LIMITED = 268435456 /* (2 << RANGE_SHIFT) */, - /** - * Extended range is used for scRGB. Intended for use with - * floating point pixel formats. [0.0 - 1.0] is the standard - * sRGB space. Values outside the range 0.0 - 1.0 can encode - * color outside the sRGB gamut. - * Used to blend / merge multiple dataspaces on a single display. - */ - RANGE_EXTENDED = 402653184 /* (3 << RANGE_SHIFT) */, - /** - * Legacy dataspaces - * - * - * sRGB linear encoding: - * - * The red, green, and blue components are stored in sRGB space, but - * are linear, not gamma-encoded. - * The RGB primaries and the white point are the same as BT.709. - * - * The values are encoded using the full range ([0,255] for 8-bit) for all - * components. - */ - SRGB_LINEAR = 512 /* 0x200 */, - V0_SRGB_LINEAR = 138477568 /* ((STANDARD_BT709 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * scRGB linear encoding: - * - * The red, green, and blue components are stored in extended sRGB space, - * but are linear, not gamma-encoded. - * The RGB primaries and the white point are the same as BT.709. - * - * The values are floating point. - * A pixel value of 1.0, 1.0, 1.0 corresponds to sRGB white (D65) at 80 nits. - * Values beyond the range [0.0 - 1.0] would correspond to other colors - * spaces and/or HDR content. - */ - V0_SCRGB_LINEAR = 406913024 /* ((STANDARD_BT709 | TRANSFER_LINEAR) | RANGE_EXTENDED) */, - /** - * sRGB gamma encoding: - * - * The red, green and blue components are stored in sRGB space, and - * converted to linear space when read, using the SRGB transfer function - * for each of the R, G and B components. When written, the inverse - * transformation is performed. - * - * The alpha component, if present, is always stored in linear space and - * is left unmodified when read or written. - * - * Use full range and BT.709 standard. - */ - SRGB = 513 /* 0x201 */, - V0_SRGB = 142671872 /* ((STANDARD_BT709 | TRANSFER_SRGB) | RANGE_FULL) */, - /** - * scRGB: - * - * The red, green, and blue components are stored in extended sRGB space, - * but are linear, not gamma-encoded. - * The RGB primaries and the white point are the same as BT.709. - * - * The values are floating point. - * A pixel value of 1.0, 1.0, 1.0 corresponds to sRGB white (D65) at 80 nits. - * Values beyond the range [0.0 - 1.0] would correspond to other colors - * spaces and/or HDR content. - */ - V0_SCRGB = 411107328 /* ((STANDARD_BT709 | TRANSFER_SRGB) | RANGE_EXTENDED) */, - /** - * YCbCr Colorspaces - * ----------------- - * - * Primaries are given using (x,y) coordinates in the CIE 1931 definition - * of x and y specified by ISO 11664-1. - * - * Transfer characteristics are the opto-electronic transfer characteristic - * at the source as a function of linear optical intensity (luminance). - * - * - * JPEG File Interchange Format (JFIF) - * - * Same model as BT.601-625, but all values (Y, Cb, Cr) range from 0 to 255 - * - * Use full range, BT.601 transfer and BT.601_625 standard. - */ - JFIF = 257 /* 0x101 */, - V0_JFIF = 146931712 /* ((STANDARD_BT601_625 | TRANSFER_SMPTE_170M) | RANGE_FULL) */, - /** - * ITU-R Recommendation 601 (BT.601) - 625-line - * - * Standard-definition television, 625 Lines (PAL) - * - * Use limited range, BT.601 transfer and BT.601_625 standard. - */ - BT601_625 = 258 /* 0x102 */, - V0_BT601_625 = 281149440 /* ((STANDARD_BT601_625 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * ITU-R Recommendation 601 (BT.601) - 525-line - * - * Standard-definition television, 525 Lines (NTSC) - * - * Use limited range, BT.601 transfer and BT.601_525 standard. - */ - BT601_525 = 259 /* 0x103 */, - V0_BT601_525 = 281280512 /* ((STANDARD_BT601_525 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * ITU-R Recommendation 709 (BT.709) - * - * High-definition television - * - * Use limited range, BT.709 transfer and BT.709 standard. - */ - BT709 = 260 /* 0x104 */, - V0_BT709 = 281083904 /* ((STANDARD_BT709 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * SMPTE EG 432-1 and SMPTE RP 431-2. - * - * Digital Cinema DCI-P3 - * - * Use full range, linear transfer and D65 DCI-P3 standard - */ - DCI_P3_LINEAR = 139067392 /* ((STANDARD_DCI_P3 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * SMPTE EG 432-1 and SMPTE RP 431-2. - * - * Digital Cinema DCI-P3 - * - * Use full range, gamma 2.6 transfer and D65 DCI-P3 standard - * Note: Application is responsible for gamma encoding the data as - * a 2.6 gamma encoding is not supported in HW. - */ - DCI_P3 = 155844608 /* ((STANDARD_DCI_P3 | TRANSFER_GAMMA2_6) | RANGE_FULL) */, - /** - * Display P3 - * - * Display P3 uses same primaries and white-point as DCI-P3 - * linear transfer function makes this the same as DCI_P3_LINEAR. - */ - DISPLAY_P3_LINEAR = 139067392 /* ((STANDARD_DCI_P3 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * Display P3 - * - * Use same primaries and white-point as DCI-P3 - * but sRGB transfer function. - */ - DISPLAY_P3 = 143261696 /* ((STANDARD_DCI_P3 | TRANSFER_SRGB) | RANGE_FULL) */, - /** - * Adobe RGB - * - * Use full range, gamma 2.2 transfer and Adobe RGB primaries - * Note: Application is responsible for gamma encoding the data as - * a 2.2 gamma encoding is not supported in HW. - */ - ADOBE_RGB = 151715840 /* ((STANDARD_ADOBE_RGB | TRANSFER_GAMMA2_2) | RANGE_FULL) */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use full range, linear transfer and BT2020 standard - */ - BT2020_LINEAR = 138805248 /* ((STANDARD_BT2020 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use full range, BT.709 transfer and BT2020 standard - */ - BT2020 = 147193856 /* ((STANDARD_BT2020 | TRANSFER_SMPTE_170M) | RANGE_FULL) */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use full range, SMPTE 2084 (PQ) transfer and BT2020 standard - */ - BT2020_PQ = 163971072 /* ((STANDARD_BT2020 | TRANSFER_ST2084) | RANGE_FULL) */, - /** - * Data spaces for non-color formats - * - * - * The buffer contains depth ranging measurements from a depth camera. - * This value is valid with formats: - * HAL_PIXEL_FORMAT_Y16: 16-bit samples, consisting of a depth measurement - * and an associated confidence value. The 3 MSBs of the sample make - * up the confidence value, and the low 13 LSBs of the sample make up - * the depth measurement. - * For the confidence section, 0 means 100% confidence, 1 means 0% - * confidence. The mapping to a linear float confidence value between - * 0.f and 1.f can be obtained with - * float confidence = (((depthSample >> 13) - 1) & 0x7) / 7.0f; - * The depth measurement can be extracted simply with - * uint16_t range = (depthSample & 0x1FFF); - * HAL_PIXEL_FORMAT_BLOB: A depth point cloud, as - * a variable-length float (x,y,z, confidence) coordinate point list. - * The point cloud will be represented with the android_depth_points - * structure. - */ - DEPTH = 4096 /* 0x1000 */, - /** - * The buffer contains sensor events from sensor direct report. - * This value is valid with formats: - * HAL_PIXEL_FORMAT_BLOB: an array of sensor event structure that forms - * a lock free queue. Format of sensor event structure is specified - * in Sensors HAL. - */ - SENSOR = 4097 /* 0x1001 */, + + + + + + + RANGE_MASK = 939524096 , + + + + + + + + + + + + RANGE_UNSPECIFIED = 0 , + + + + + RANGE_FULL = 134217728 , + + + + + + + + + + + + + + RANGE_LIMITED = 268435456 , + + + + + + + + RANGE_EXTENDED = 402653184 , + + + + + + + + + + + + + + SRGB_LINEAR = 512 , + V0_SRGB_LINEAR = 138477568 , + + + + + + + + + + + + + V0_SCRGB_LINEAR = 406913024 , + + + + + + + + + + + + + + SRGB = 513 , + V0_SRGB = 142671872 , + + + + + + + + + + + + + V0_SCRGB = 411107328 , + + + + + + + + + + + + + + + + + + JFIF = 257 , + V0_JFIF = 146931712 , + + + + + + + + BT601_625 = 258 , + V0_BT601_625 = 281149440 , + + + + + + + + BT601_525 = 259 , + V0_BT601_525 = 281280512 , + + + + + + + + BT709 = 260 , + V0_BT709 = 281083904 , + + + + + + + + DCI_P3_LINEAR = 139067392 , + + + + + + + + + + DCI_P3 = 155844608 , + + + + + + + DISPLAY_P3_LINEAR = 139067392 , + + + + + + + DISPLAY_P3 = 143261696 , + + + + + + + + ADOBE_RGB = 151715840 , + + + + + + + + BT2020_LINEAR = 138805248 , + + + + + + + + BT2020 = 147193856 , + + + + + + + + BT2020_PQ = 163971072 , + + + + + + + + + + + + + + + + + + + + + + DEPTH = 4096 , + + + + + + + + SENSOR = 4097 , }; -/** - * Color modes that may be supported by a display. - * - * Definitions: - * Rendering intent generally defines the goal in mapping a source (input) - * color to a destination device color for a given color mode. - * - * It is important to keep in mind three cases where mapping may be applied: - * 1. The source gamut is much smaller than the destination (display) gamut - * 2. The source gamut is much larger than the destination gamut (this will - * ordinarily be handled using colorimetric rendering, below) - * 3. The source and destination gamuts are roughly equal, although not - * completely overlapping - * Also, a common requirement for mappings is that skin tones should be - * preserved, or at least remain natural in appearance. - * - * Colorimetric Rendering Intent (All cases): - * Colorimetric indicates that colors should be preserved. In the case - * that the source gamut lies wholly within the destination gamut or is - * about the same (#1, #3), this will simply mean that no manipulations - * (no saturation boost, for example) are applied. In the case where some - * source colors lie outside the destination gamut (#2, #3), those will - * need to be mapped to colors that are within the destination gamut, - * while the already in-gamut colors remain unchanged. - * - * Non-colorimetric transforms can take many forms. There are no hard - * rules and it's left to the implementation to define. - * Two common intents are described below. - * - * Stretched-Gamut Enhancement Intent (Source < Destination): - * When the destination gamut is much larger than the source gamut (#1), the - * source primaries may be redefined to reflect the full extent of the - * destination space, or to reflect an intermediate gamut. - * Skin-tone preservation would likely be applied. An example might be sRGB - * input displayed on a DCI-P3 capable device, with skin-tone preservation. - * - * Within-Gamut Enhancement Intent (Source >= Destination): - * When the device (destination) gamut is not larger than the source gamut - * (#2 or #3), but the appearance of a larger gamut is desired, techniques - * such as saturation boost may be applied to the source colors. Skin-tone - * preservation may be applied. There is no unique method for within-gamut - * enhancement; it would be defined within a flexible color mode. - * - */ enum class ColorMode : int32_t { - /** - * DEFAULT is the "native" gamut of the display. - * White Point: Vendor/OEM defined - * Panel Gamma: Vendor/OEM defined (typically 2.2) - * Rendering Intent: Vendor/OEM defined (typically 'enhanced') - */ NATIVE = 0, - /** - * STANDARD_BT601_625 corresponds with display - * settings that implement the ITU-R Recommendation BT.601 - * or Rec 601. Using 625 line version - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_625 = 1, - /** - * Primaries: - * x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.222, KB = 0.071 luminance interpretation - * for RGB conversion. - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_625_UNADJUSTED = 2, - /** - * Primaries: - * x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_525 = 3, - /** - * Primaries: - * x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.212, KB = 0.087 luminance interpretation - * for RGB conversion (as in SMPTE 240M). - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_525_UNADJUSTED = 4, - /** - * REC709 corresponds with display settings that implement - * the ITU-R Recommendation BT.709 / Rec. 709 for high-definition television. - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.300 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * HDTV REC709 Inverse Gamma Correction (IGC): V represents normalized - * (with [0 to 1] range) value of R, G, or B. - * - * if Vnonlinear < 0.081 - * Vlinear = Vnonlinear / 4.5 - * else - * Vlinear = ((Vnonlinear + 0.099) / 1.099) ^ (1/0.45) - * - * HDTV REC709 Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.5 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear) ^ 0.45 – 0.099 - */ STANDARD_BT709 = 5, - /** - * DCI_P3 corresponds with display settings that implement - * SMPTE EG 432-1 and SMPTE RP 431-2 - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.265 0.690 - * blue 0.150 0.060 - * red 0.680 0.320 - * white (D65) 0.3127 0.3290 - * - * Gamma: 2.6 - */ DCI_P3 = 6, - /** - * SRGB corresponds with display settings that implement - * the sRGB color space. Uses the same primaries as ITU-R Recommendation - * BT.709 - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.300 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * PC/Internet (sRGB) Inverse Gamma Correction (IGC): - * - * if Vnonlinear ≤ 0.03928 - * Vlinear = Vnonlinear / 12.92 - * else - * Vlinear = ((Vnonlinear + 0.055)/1.055) ^ 2.4 - * - * PC/Internet (sRGB) Gamma Correction (GC): - * - * if Vlinear ≤ 0.0031308 - * Vnonlinear = 12.92 * Vlinear - * else - * Vnonlinear = 1.055 * (Vlinear)^(1/2.4) – 0.055 - */ SRGB = 7, - /** - * ADOBE_RGB corresponds with the RGB color space developed - * by Adobe Systems, Inc. in 1998. - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.210 0.710 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Gamma: 2.2 - */ ADOBE_RGB = 8, - /** - * DISPLAY_P3 is a color space that uses the DCI_P3 primaries, - * the D65 white point and the SRGB transfer functions. - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.265 0.690 - * blue 0.150 0.060 - * red 0.680 0.320 - * white (D65) 0.3127 0.3290 - * - * PC/Internet (sRGB) Gamma Correction (GC): - * - * if Vlinear ≤ 0.0030186 - * Vnonlinear = 12.92 * Vlinear - * else - * Vnonlinear = 1.055 * (Vlinear)^(1/2.4) – 0.055 - * - * Note: In most cases sRGB transfer function will be fine. - */ DISPLAY_P3 = 9, }; -/** - * Color transforms that may be applied by hardware composer to the whole - * display. - */ enum class ColorTransform : int32_t { - /** - * Applies no transform to the output color - */ IDENTITY = 0, - /** - * Applies an arbitrary transform defined by a 4x4 affine matrix - */ ARBITRARY_MATRIX = 1, - /** - * Applies a transform that inverts the value or luminance of the color, but - * does not modify hue or saturation - */ VALUE_INVERSE = 2, - /** - * Applies a transform that maps all colors to shades of gray - */ GRAYSCALE = 3, - /** - * Applies a transform which corrects for protanopic color blindness - */ CORRECT_PROTANOPIA = 4, - /** - * Applies a transform which corrects for deuteranopic color blindness - */ CORRECT_DEUTERANOPIA = 5, - /** - * Applies a transform which corrects for tritanopic color blindness - */ CORRECT_TRITANOPIA = 6, }; -/** - * Supported HDR formats. Must be kept in sync with equivalents in Display.java. - */ enum class Hdr : int32_t { - /** - * Device supports Dolby Vision HDR - */ DOLBY_VISION = 1, - /** - * Device supports HDR10 - */ HDR10 = 2, - /** - * Device supports hybrid log-gamma HDR - */ HLG = 3, }; -// -// type declarations for package -// template static inline std::string toString(int32_t o); @@ -1691,9 +1360,6 @@ constexpr int32_t& operator&=(int32_t& v, return v; } -// -// type header definitions for package -// template <> inline std::string toString<::android::hardware::graphics::common::V1_0::PixelFormat>(int32_t o) { @@ -3183,15 +2849,12 @@ static inline std::string toString(::android::hardware::graphics::common::V1_0:: return os; } -} // namespace V1_0 -} // namespace common -} // namespace graphics -} // namespace hardware -} // namespace android +} +} +} +} +} -// -// global type declarations for package -// namespace android { namespace hardware { @@ -3220,9 +2883,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::PixelFormat, 2 ::android::hardware::graphics::common::V1_0::PixelFormat::Y16, ::android::hardware::graphics::common::V1_0::PixelFormat::YV12, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -3254,9 +2917,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::BufferUsage, 2 ::android::hardware::graphics::common::V1_0::BufferUsage::VENDOR_MASK, ::android::hardware::graphics::common::V1_0::BufferUsage::VENDOR_MASK_HI, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -3270,9 +2933,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::Transform, 5> ::android::hardware::graphics::common::V1_0::Transform::ROT_180, ::android::hardware::graphics::common::V1_0::Transform::ROT_270, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -3338,9 +3001,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::Dataspace, 57> ::android::hardware::graphics::common::V1_0::Dataspace::DEPTH, ::android::hardware::graphics::common::V1_0::Dataspace::SENSOR, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -3359,9 +3022,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::ColorMode, 10> ::android::hardware::graphics::common::V1_0::ColorMode::ADOBE_RGB, ::android::hardware::graphics::common::V1_0::ColorMode::DISPLAY_P3, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -3377,9 +3040,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::ColorTransform ::android::hardware::graphics::common::V1_0::ColorTransform::CORRECT_DEUTERANOPIA, ::android::hardware::graphics::common::V1_0::ColorTransform::CORRECT_TRITANOPIA, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -3391,11 +3054,11 @@ constexpr std::array<::android::hardware::graphics::common::V1_0::Hdr, 3> ::android::hardware::graphics::common::V1_0::Hdr::HDR10, ::android::hardware::graphics::common::V1_0::Hdr::HLG, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} -#endif // HIDL_GENERATED_ANDROID_HARDWARE_GRAPHICS_COMMON_V1_0_TYPES_H +#endif #ifndef HIDL_GENERATED_ANDROID_HARDWARE_GRAPHICS_COMMON_V1_1_TYPES_H #define HIDL_GENERATED_ANDROID_HARDWARE_GRAPHICS_COMMON_V1_1_TYPES_H @@ -3411,1449 +3074,1158 @@ namespace graphics { namespace common { namespace V1_1 { -// Forward declaration for forward reference support: enum class PixelFormat : int32_t; enum class BufferUsage : uint64_t; enum class Dataspace : int32_t; enum class ColorMode : int32_t; enum class RenderIntent : int32_t; -/** - * Pixel formats for graphics buffers. - */ enum class PixelFormat : int32_t { - /** - * 32-bit format that has 8-bit R, G, B, and A components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGBA_8888 = 1 /* 0x1 */, - /** - * 32-bit format that has 8-bit R, G, B, and unused components, in that - * order, from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGBX_8888 = 2 /* 0x2 */, - /** - * 24-bit format that has 8-bit R, G, and B components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGB_888 = 3 /* 0x3 */, - /** - * 16-bit packed format that has 5-bit R, 6-bit G, and 5-bit B components, - * in that order, from the most-sigfinicant bits to the least-significant - * bits. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGB_565 = 4 /* 0x4 */, - /** - * 32-bit format that has 8-bit B, G, R, and A components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - BGRA_8888 = 5 /* 0x5 */, - /** - * Legacy formats deprecated in favor of YCBCR_420_888. - */ - YCBCR_422_SP = 16 /* 0x10 */, - YCRCB_420_SP = 17 /* 0x11 */, - YCBCR_422_I = 20 /* 0x14 */, - /** - * 64-bit format that has 16-bit R, G, B, and A components, in that order, - * from the lowest memory address to the highest memory address. - * - * The component values are signed floats, whose interpretation is defined - * by the dataspace. - */ - RGBA_FP16 = 22 /* 0x16 */, - /** - * RAW16 is a single-channel, 16-bit, little endian format, typically - * representing raw Bayer-pattern images from an image sensor, with minimal - * processing. - * - * The exact pixel layout of the data in the buffer is sensor-dependent, and - * needs to be queried from the camera device. - * - * Generally, not all 16 bits are used; more common values are 10 or 12 - * bits. If not all bits are used, the lower-order bits are filled first. - * All parameters to interpret the raw data (black and white points, - * color space, etc) must be queried from the camera device. - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - strides are specified in pixels, not in bytes - * - * size = stride * height * 2 - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW16 is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data, layout is as - * | defined above. - * Dataspace::DEPTH | Unprocessed implementation-dependent raw - * | depth measurements, opaque with 16 bit - * | samples. - * Other | Unsupported - */ - RAW16 = 32 /* 0x20 */, - /** - * BLOB is used to carry task-specific data which does not have a standard - * image structure. The details of the format are left to the two - * endpoints. - * - * A typical use case is for transporting JPEG-compressed images from the - * Camera HAL to the framework or to applications. - * - * Buffers of this format must have a height of 1, and width equal to their - * size in bytes. - * - * The mapping of the dataspace to buffer contents for BLOB is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::JFIF | An encoded JPEG image - * Dataspace::DEPTH | An android_depth_points buffer - * Dataspace::SENSOR | Sensor event data. - * Other | Unsupported - */ - BLOB = 33 /* 0x21 */, - /** - * A format indicating that the choice of format is entirely up to the - * allocator. - * - * The allocator should examine the usage bits passed in when allocating a - * buffer with this format, and it should derive the pixel format from - * those usage flags. This format must never be used with any of the - * BufferUsage::CPU_* usage flags. - * - * Even when the internally chosen format has an alpha component, the - * clients must assume the alpha vlaue to be 1.0. - * - * The interpretation of the component values is defined by the dataspace. - */ - IMPLEMENTATION_DEFINED = 34 /* 0x22 */, - /** - * This format allows platforms to use an efficient YCbCr/YCrCb 4:2:0 - * buffer layout, while still describing the general format in a - * layout-independent manner. While called YCbCr, it can be used to - * describe formats with either chromatic ordering, as well as - * whole planar or semiplanar layouts. - * - * This format must be accepted by the allocator when BufferUsage::CPU_* - * are set. - * - * Buffers with this format must be locked with IMapper::lockYCbCr. - * Locking with IMapper::lock must return an error. - * - * The interpretation of the component values is defined by the dataspace. - */ - YCBCR_420_888 = 35 /* 0x23 */, - /** - * RAW_OPAQUE is a format for unprocessed raw image buffers coming from an - * image sensor. The actual structure of buffers of this format is - * implementation-dependent. - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW_OPAQUE is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data. - * Other | Unsupported - */ - RAW_OPAQUE = 36 /* 0x24 */, - /** - * RAW10 is a single-channel, 10-bit per pixel, densely packed in each row, - * unprocessed format, usually representing raw Bayer-pattern images coming from - * an image sensor. - * - * In an image buffer with this format, starting from the first pixel of each - * row, each 4 consecutive pixels are packed into 5 bytes (40 bits). Each one - * of the first 4 bytes contains the top 8 bits of each pixel, The fifth byte - * contains the 2 least significant bits of the 4 pixels, the exact layout data - * for each 4 consecutive pixels is illustrated below (Pi[j] stands for the jth - * bit of the ith pixel): - * - * bit 7 bit 0 - * =====|=====|=====|=====|=====|=====|=====|=====| - * Byte 0: |P0[9]|P0[8]|P0[7]|P0[6]|P0[5]|P0[4]|P0[3]|P0[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 1: |P1[9]|P1[8]|P1[7]|P1[6]|P1[5]|P1[4]|P1[3]|P1[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 2: |P2[9]|P2[8]|P2[7]|P2[6]|P2[5]|P2[4]|P2[3]|P2[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 3: |P3[9]|P3[8]|P3[7]|P3[6]|P3[5]|P3[4]|P3[3]|P3[2]| - * |-----|-----|-----|-----|-----|-----|-----|-----| - * Byte 4: |P3[1]|P3[0]|P2[1]|P2[0]|P1[1]|P1[0]|P0[1]|P0[0]| - * =============================================== - * - * This format assumes - * - a width multiple of 4 pixels - * - an even height - * - a vertical stride equal to the height - * - strides are specified in bytes, not in pixels - * - * size = stride * height - * - * When stride is equal to width * (10 / 8), there will be no padding bytes at - * the end of each row, the entire image data is densely packed. When stride is - * larger than width * (10 / 8), padding bytes will be present at the end of each - * row (including the last row). - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW10 is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data. - * Other | Unsupported - */ - RAW10 = 37 /* 0x25 */, - /** - * RAW12 is a single-channel, 12-bit per pixel, densely packed in each row, - * unprocessed format, usually representing raw Bayer-pattern images coming from - * an image sensor. - * - * In an image buffer with this format, starting from the first pixel of each - * row, each two consecutive pixels are packed into 3 bytes (24 bits). The first - * and second byte contains the top 8 bits of first and second pixel. The third - * byte contains the 4 least significant bits of the two pixels, the exact layout - * data for each two consecutive pixels is illustrated below (Pi[j] stands for - * the jth bit of the ith pixel): - * - * bit 7 bit 0 - * ======|======|======|======|======|======|======|======| - * Byte 0: |P0[11]|P0[10]|P0[ 9]|P0[ 8]|P0[ 7]|P0[ 6]|P0[ 5]|P0[ 4]| - * |------|------|------|------|------|------|------|------| - * Byte 1: |P1[11]|P1[10]|P1[ 9]|P1[ 8]|P1[ 7]|P1[ 6]|P1[ 5]|P1[ 4]| - * |------|------|------|------|------|------|------|------| - * Byte 2: |P1[ 3]|P1[ 2]|P1[ 1]|P1[ 0]|P0[ 3]|P0[ 2]|P0[ 1]|P0[ 0]| - * ======================================================= - * - * This format assumes: - * - a width multiple of 4 pixels - * - an even height - * - a vertical stride equal to the height - * - strides are specified in bytes, not in pixels - * - * size = stride * height - * - * When stride is equal to width * (12 / 8), there will be no padding bytes at - * the end of each row, the entire image data is densely packed. When stride is - * larger than width * (12 / 8), padding bytes will be present at the end of - * each row (including the last row). - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::RENDERSCRIPT - * - * The mapping of the dataspace to buffer contents for RAW12 is as - * follows: - * - * Dataspace value | Buffer contents - * -------------------------------+----------------------------------------- - * Dataspace::ARBITRARY | Raw image sensor data. - * Other | Unsupported - */ - RAW12 = 38 /* 0x26 */, - /** - * 0x27 to 0x2A are reserved for flexible formats - * - * - * 32-bit packed format that has 2-bit A, 10-bit B, G, and R components, - * in that order, from the most-sigfinicant bits to the least-significant - * bits. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - RGBA_1010102 = 43 /* 0x2B */, - /** - * 0x100 - 0x1FF - * - * This range is reserved for vendor extensions. Formats in this range - * must support BufferUsage::GPU_TEXTURE. Clients must assume they do not - * have an alpha component. - * - * - * Y8 is a YUV planar format comprised of a WxH Y plane, with each pixel - * being represented by 8 bits. It is equivalent to just the Y plane from - * YV12. - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - * size = stride * height - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - Y8 = 538982489 /* 0x20203859 */, - /** - * Y16 is a YUV planar format comprised of a WxH Y plane, with each pixel - * being represented by 16 bits. It is just like Y8, but has double the - * bits per pixel (little endian). - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - strides are specified in pixels, not in bytes - * - * size = stride * height * 2 - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. When the dataspace is - * Dataspace::DEPTH, each pixel is a distance value measured by a depth - * camera, plus an associated confidence value. - */ - Y16 = 540422489 /* 0x20363159 */, - /** - * YV12 is a 4:2:0 YCrCb planar format comprised of a WxH Y plane followed - * by (W/2) x (H/2) Cr and Cb planes. - * - * This format assumes - * - an even width - * - an even height - * - a horizontal stride multiple of 16 pixels - * - a vertical stride equal to the height - * - * y_size = stride * height - * c_stride = ALIGN(stride/2, 16) - * c_size = c_stride * height/2 - * size = y_size + c_size * 2 - * cr_offset = y_size - * cb_offset = y_size + c_size - * - * This range is reserved for vendor extensions. Formats in this range - * must support BufferUsage::GPU_TEXTURE. Clients must assume they do not - * have an alpha component. - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::CAMERA_* - * - BufferUsage::CPU_* - * - BufferUsage::GPU_TEXTURE - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - YV12 = 842094169 /* 0x32315659 */, - /** - * 16-bit format that has a single 16-bit depth component. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - DEPTH_16 = 48 /* 0x30 */, - /** - * 32-bit format that has a single 24-bit depth component and, optionally, - * 8 bits that are unused. - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - */ - DEPTH_24 = 49 /* 0x31 */, - /** - * 32-bit format that has a 24-bit depth component and an 8-bit stencil - * component packed into 32-bits. - * - * The depth component values are unsigned normalized to the range [0, 1], - * whose interpretation is defined by the dataspace. The stencil values are - * unsigned integers, whose interpretation is defined by the dataspace. - */ - DEPTH_24_STENCIL_8 = 50 /* 0x32 */, - /** - * 32-bit format that has a single 32-bit depth component. - * - * The component values are signed floats, whose interpretation is defined - * by the dataspace. - */ - DEPTH_32F = 51 /* 0x33 */, - /** - * Two-component format that has a 32-bit depth component, an 8-bit stencil - * component, and optionally 24-bits unused. - * - * The depth component values are signed floats, whose interpretation is - * defined by the dataspace. The stencil bits are unsigned integers, whose - * interpretation is defined by the dataspace. - */ - DEPTH_32F_STENCIL_8 = 52 /* 0x34 */, - /** - * 8-bit format that has a single 8-bit stencil component. - * - * The component values are unsigned integers, whose interpretation is - * defined by the dataspace. - */ - STENCIL_8 = 53 /* 0x35 */, - /** - * P010 is a 4:2:0 YCbCr semiplanar format comprised of a WxH Y plane - * followed immediately by a Wx(H/2) CbCr plane. Each sample is - * represented by a 16-bit little-endian value, with the lower 6 bits set - * to zero. - * - * This format assumes - * - an even height - * - a vertical stride equal to the height - * - * stride_in_bytes = stride * 2 - * y_size = stride_in_bytes * height - * cbcr_size = stride_in_bytes * (height / 2) - * cb_offset = y_size - * cr_offset = cb_offset + 2 - * - * This format must be accepted by the allocator when used with the - * following usage flags: - * - * - BufferUsage::VIDEO_* - * - BufferUsage::CPU_* - * - BufferUsage::GPU_TEXTURE - * - * The component values are unsigned normalized to the range [0, 1], whose - * interpretation is defined by the dataspace. - * - * This format is appropriate for 10bit video content. - * - * Buffers with this format must be locked with IMapper::lockYCbCr - * or with IMapper::lock. - */ - YCBCR_P010 = 54 /* 0x36 */, + + + + + + + + RGBA_8888 = 1 , + + + + + + + + RGBX_8888 = 2 , + + + + + + + + RGB_888 = 3 , + + + + + + + + + RGB_565 = 4 , + + + + + + + + BGRA_8888 = 5 , + + + + YCBCR_422_SP = 16 , + YCRCB_420_SP = 17 , + YCBCR_422_I = 20 , + + + + + + + + RGBA_FP16 = 22 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RAW16 = 32 , + + + + + + + + + + + + + + + + + + + + + + BLOB = 33 , + + + + + + + + + + + + + + + IMPLEMENTATION_DEFINED = 34 , + + + + + + + + + + + + + + + + YCBCR_420_888 = 35 , + + + + + + + + + + + + + + + + + + + + + RAW_OPAQUE = 36 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RAW10 = 37 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RAW12 = 38 , + + + + + + + + + + + + RGBA_1010102 = 43 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Y8 = 538982489 , + + + + + + + + + + + + + + + + + + + + + + + + + + Y16 = 540422489 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + YV12 = 842094169 , + + + + + + + DEPTH_16 = 48 , + + + + + + + + DEPTH_24 = 49 , + + + + + + + + + DEPTH_24_STENCIL_8 = 50 , + + + + + + + DEPTH_32F = 51 , + + + + + + + + + DEPTH_32F_STENCIL_8 = 52 , + + + + + + + STENCIL_8 = 53 , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + YCBCR_P010 = 54 , }; -/** - * Buffer usage definitions. - */ enum class BufferUsage : uint64_t { - /** - * bit 0-3 is an enum - */ - CPU_READ_MASK = 15ull /* 0xfULL */, - /** - * buffer is never read by CPU - */ + + + + CPU_READ_MASK = 15ull , + + + CPU_READ_NEVER = 0ull, - /** - * buffer is rarely read by CPU - */ CPU_READ_RARELY = 2ull, - /** - * buffer is often read by CPU - */ CPU_READ_OFTEN = 3ull, - /** - * bit 4-7 is an enum - */ - CPU_WRITE_MASK = 240ull /* (0xfULL << 4) */, - /** - * buffer is never written by CPU - */ - CPU_WRITE_NEVER = 0ull /* (0 << 4) */, - /** - * buffer is rarely written by CPU - */ - CPU_WRITE_RARELY = 32ull /* (2 << 4) */, - /** - * buffer is often written by CPU - */ - CPU_WRITE_OFTEN = 48ull /* (3 << 4) */, - /** - * buffer is used as a GPU texture - */ - GPU_TEXTURE = 256ull /* (1ULL << 8) */, - /** - * buffer is used as a GPU render target - */ - GPU_RENDER_TARGET = 512ull /* (1ULL << 9) */, - /** - * bit 10 must be zero - * - * - * buffer is used as a composer HAL overlay layer - */ - COMPOSER_OVERLAY = 2048ull /* (1ULL << 11) */, - /** - * buffer is used as a composer HAL client target - */ - COMPOSER_CLIENT_TARGET = 4096ull /* (1ULL << 12) */, - /** - * bit 13 must be zero - * - * - * Buffer is allocated with hardware-level protection against copying the - * contents (or information derived from the contents) into unprotected - * memory. - */ - PROTECTED = 16384ull /* (1ULL << 14) */, - /** - * buffer is used as a hwcomposer HAL cursor layer - */ - COMPOSER_CURSOR = 32768ull /* (1ULL << 15) */, - /** - * buffer is used as a video encoder input - */ - VIDEO_ENCODER = 65536ull /* (1ULL << 16) */, - /** - * buffer is used as a camera HAL output - */ - CAMERA_OUTPUT = 131072ull /* (1ULL << 17) */, - /** - * buffer is used as a camera HAL input - */ - CAMERA_INPUT = 262144ull /* (1ULL << 18) */, - /** - * bit 19 must be zero - * - * - * buffer is used as a renderscript allocation - */ - RENDERSCRIPT = 1048576ull /* (1ULL << 20) */, - /** - * bit 21 must be zero - * - * - * buffer is used as a video decoder output - */ - VIDEO_DECODER = 4194304ull /* (1ULL << 22) */, - /** - * buffer is used as a sensor direct report output - */ - SENSOR_DIRECT_DATA = 8388608ull /* (1ULL << 23) */, - /** - * buffer is used as as an OpenGL shader storage or uniform - * buffer object - */ - GPU_DATA_BUFFER = 16777216ull /* (1ULL << 24) */, - /** - * bits 25-27 must be zero and are reserved for future versions - * - * - * bits 28-31 are reserved for vendor extensions - */ - VENDOR_MASK = 4026531840ull /* (0xfULL << 28) */, - /** - * bits 32-47 must be zero and are reserved for future versions - * - * - * bits 48-63 are reserved for vendor extensions - */ - VENDOR_MASK_HI = 18446462598732840960ull /* (0xffffULL << 48) */, - /** - * buffer is used as a cube map texture - */ - GPU_CUBE_MAP = 33554432ull /* (1ULL << 25) */, - /** - * buffer contains a complete mipmap hierarchy - */ - GPU_MIPMAP_COMPLETE = 67108864ull /* (1ULL << 26) */, + + + + CPU_WRITE_MASK = 240ull , + + + + CPU_WRITE_NEVER = 0ull , + + + + CPU_WRITE_RARELY = 32ull , + + + + CPU_WRITE_OFTEN = 48ull , + + + + GPU_TEXTURE = 256ull , + + + + GPU_RENDER_TARGET = 512ull , + + + + + + + COMPOSER_OVERLAY = 2048ull , + + + + COMPOSER_CLIENT_TARGET = 4096ull , + + + + + + + + + PROTECTED = 16384ull , + + + + COMPOSER_CURSOR = 32768ull , + + + + VIDEO_ENCODER = 65536ull , + + + + CAMERA_OUTPUT = 131072ull , + + + + CAMERA_INPUT = 262144ull , + + + + + + + RENDERSCRIPT = 1048576ull , + + + + + + + VIDEO_DECODER = 4194304ull , + + + + SENSOR_DIRECT_DATA = 8388608ull , + + + + + GPU_DATA_BUFFER = 16777216ull , + + + + + + + VENDOR_MASK = 4026531840ull , + + + + + + + VENDOR_MASK_HI = 18446462598732840960ull , + + + + GPU_CUBE_MAP = 33554432ull , + + + + GPU_MIPMAP_COMPLETE = 67108864ull , }; enum class Dataspace : int32_t { - /** - * Default-assumption data space, when not explicitly specified. - * - * It is safest to assume the buffer is an image with sRGB primaries and - * encoding ranges, but the consumer and/or the producer of the data may - * simply be using defaults. No automatic gamma transform should be - * expected, except for a possible display gamma transform when drawn to a - * screen. - */ - UNKNOWN = 0 /* 0x0 */, - /** - * Arbitrary dataspace with manually defined characteristics. Definition - * for colorspaces or other meaning must be communicated separately. - * - * This is used when specifying primaries, transfer characteristics, - * etc. separately. - * - * A typical use case is in video encoding parameters (e.g. for H.264), - * where a colorspace can have separately defined primaries, transfer - * characteristics, etc. - */ - ARBITRARY = 1 /* 0x1 */, - /** - * Color-description aspects - * - * The following aspects define various characteristics of the color - * specification. These represent bitfields, so that a data space value - * can specify each of them independently. - */ + + + + + + + + + + UNKNOWN = 0 , + + + + + + + + + + + + ARBITRARY = 1 , + + + + + + + STANDARD_SHIFT = 16, - /** - * Standard aspect - * - * Defines the chromaticity coordinates of the source primaries in terms of - * the CIE 1931 definition of x and y specified in ISO 11664-1. - */ - STANDARD_MASK = 4128768 /* (63 << STANDARD_SHIFT) */, - /** - * Chromacity coordinates are unknown or are determined by the application. - * Implementations shall use the following suggested standards: - * - * All YCbCr formats: BT709 if size is 720p or larger (since most video - * content is letterboxed this corresponds to width is - * 1280 or greater, or height is 720 or greater). - * BT601_625 if size is smaller than 720p or is JPEG. - * All RGB formats: BT709. - * - * For all other formats standard is undefined, and implementations should use - * an appropriate standard for the data represented. - */ - STANDARD_UNSPECIFIED = 0 /* (0 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.300 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.2126, KB = 0.0722 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT709 = 65536 /* (1 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - */ - STANDARD_BT601_625 = 131072 /* (2 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.222, KB = 0.071 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT601_625_UNADJUSTED = 196608 /* (3 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - */ - STANDARD_BT601_525 = 262144 /* (4 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.212, KB = 0.087 luminance interpretation - * for RGB conversion (as in SMPTE 240M). - */ - STANDARD_BT601_525_UNADJUSTED = 327680 /* (5 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.170 0.797 - * blue 0.131 0.046 - * red 0.708 0.292 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.2627, KB = 0.0593 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT2020 = 393216 /* (6 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.170 0.797 - * blue 0.131 0.046 - * red 0.708 0.292 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.2627, KB = 0.0593 luminance interpretation - * for RGB conversion using the linear domain. - */ - STANDARD_BT2020_CONSTANT_LUMINANCE = 458752 /* (7 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.21 0.71 - * blue 0.14 0.08 - * red 0.67 0.33 - * white (C) 0.310 0.316 - * - * Use the unadjusted KR = 0.30, KB = 0.11 luminance interpretation - * for RGB conversion. - */ - STANDARD_BT470M = 524288 /* (8 << STANDARD_SHIFT) */, - /** - * Primaries: x y - * green 0.243 0.692 - * blue 0.145 0.049 - * red 0.681 0.319 - * white (C) 0.310 0.316 - * - * Use the unadjusted KR = 0.254, KB = 0.068 luminance interpretation - * for RGB conversion. - */ - STANDARD_FILM = 589824 /* (9 << STANDARD_SHIFT) */, - /** - * SMPTE EG 432-1 and SMPTE RP 431-2. (DCI-P3) - * Primaries: x y - * green 0.265 0.690 - * blue 0.150 0.060 - * red 0.680 0.320 - * white (D65) 0.3127 0.3290 - */ - STANDARD_DCI_P3 = 655360 /* (10 << STANDARD_SHIFT) */, - /** - * Adobe RGB - * Primaries: x y - * green 0.210 0.710 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - */ - STANDARD_ADOBE_RGB = 720896 /* (11 << STANDARD_SHIFT) */, + + + + + + + STANDARD_MASK = 4128768 , + + + + + + + + + + + + + + STANDARD_UNSPECIFIED = 0 , + + + + + + + + + + + STANDARD_BT709 = 65536 , + + + + + + + + + + + + + STANDARD_BT601_625 = 131072 , + + + + + + + + + + + STANDARD_BT601_625_UNADJUSTED = 196608 , + + + + + + + + + + + + + STANDARD_BT601_525 = 262144 , + + + + + + + + + + + STANDARD_BT601_525_UNADJUSTED = 327680 , + + + + + + + + + + + STANDARD_BT2020 = 393216 , + + + + + + + + + + + STANDARD_BT2020_CONSTANT_LUMINANCE = 458752 , + + + + + + + + + + + STANDARD_BT470M = 524288 , + + + + + + + + + + + STANDARD_FILM = 589824 , + + + + + + + + + STANDARD_DCI_P3 = 655360 , + + + + + + + + + STANDARD_ADOBE_RGB = 720896 , TRANSFER_SHIFT = 22, - /** - * Transfer aspect - * - * Transfer characteristics are the opto-electronic transfer characteristic - * at the source as a function of linear optical intensity (luminance). - * - * For digital signals, E corresponds to the recorded value. Normally, the - * transfer function is applied in RGB space to each of the R, G and B - * components independently. This may result in color shift that can be - * minized by applying the transfer function in Lab space only for the L - * component. Implementation may apply the transfer function in RGB space - * for all pixel formats if desired. - */ - TRANSFER_MASK = 130023424 /* (31 << TRANSFER_SHIFT) */, - /** - * Transfer characteristics are unknown or are determined by the - * application. - * - * Implementations should use the following transfer functions: - * - * For YCbCr formats: use TRANSFER_SMPTE_170M - * For RGB formats: use TRANSFER_SRGB - * - * For all other formats transfer function is undefined, and implementations - * should use an appropriate standard for the data represented. - */ - TRANSFER_UNSPECIFIED = 0 /* (0 << TRANSFER_SHIFT) */, - /** - * Transfer characteristic curve: - * E = L - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_LINEAR = 4194304 /* (1 << TRANSFER_SHIFT) */, - /** - * Transfer characteristic curve: - * - * E = 1.055 * L^(1/2.4) - 0.055 for 0.0031308 <= L <= 1 - * = 12.92 * L for 0 <= L < 0.0031308 - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_SRGB = 8388608 /* (2 << TRANSFER_SHIFT) */, - /** - * BT.601 525, BT.601 625, BT.709, BT.2020 - * - * Transfer characteristic curve: - * E = 1.099 * L ^ 0.45 - 0.099 for 0.018 <= L <= 1 - * = 4.500 * L for 0 <= L < 0.018 - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_SMPTE_170M = 12582912 /* (3 << TRANSFER_SHIFT) */, - /** - * Assumed display gamma 2.2. - * - * Transfer characteristic curve: - * E = L ^ (1/2.2) - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_GAMMA2_2 = 16777216 /* (4 << TRANSFER_SHIFT) */, - /** - * display gamma 2.6. - * - * Transfer characteristic curve: - * E = L ^ (1/2.6) - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_GAMMA2_6 = 20971520 /* (5 << TRANSFER_SHIFT) */, - /** - * display gamma 2.8. - * - * Transfer characteristic curve: - * E = L ^ (1/2.8) - * L - luminance of image 0 <= L <= 1 for conventional colorimetry - * E - corresponding electrical signal - */ - TRANSFER_GAMMA2_8 = 25165824 /* (6 << TRANSFER_SHIFT) */, - /** - * SMPTE ST 2084 (Dolby Perceptual Quantizer) - * - * Transfer characteristic curve: - * E = ((c1 + c2 * L^n) / (1 + c3 * L^n)) ^ m - * c1 = c3 - c2 + 1 = 3424 / 4096 = 0.8359375 - * c2 = 32 * 2413 / 4096 = 18.8515625 - * c3 = 32 * 2392 / 4096 = 18.6875 - * m = 128 * 2523 / 4096 = 78.84375 - * n = 0.25 * 2610 / 4096 = 0.1593017578125 - * L - luminance of image 0 <= L <= 1 for HDR colorimetry. - * L = 1 corresponds to 10000 cd/m2 - * E - corresponding electrical signal - */ - TRANSFER_ST2084 = 29360128 /* (7 << TRANSFER_SHIFT) */, - /** - * ARIB STD-B67 Hybrid Log Gamma - * - * Transfer characteristic curve: - * E = r * L^0.5 for 0 <= L <= 1 - * = a * ln(L - b) + c for 1 < L - * a = 0.17883277 - * b = 0.28466892 - * c = 0.55991073 - * r = 0.5 - * L - luminance of image 0 <= L for HDR colorimetry. L = 1 corresponds - * to reference white level of 100 cd/m2 - * E - corresponding electrical signal - */ - TRANSFER_HLG = 33554432 /* (8 << TRANSFER_SHIFT) */, + + + + + + + + + + + + + + TRANSFER_MASK = 130023424 , + + + + + + + + + + + + + TRANSFER_UNSPECIFIED = 0 , + + + + + + + TRANSFER_LINEAR = 4194304 , + + + + + + + + + TRANSFER_SRGB = 8388608 , + + + + + + + + + + TRANSFER_SMPTE_170M = 12582912 , + + + + + + + + + TRANSFER_GAMMA2_2 = 16777216 , + + + + + + + + + TRANSFER_GAMMA2_6 = 20971520 , + + + + + + + + + TRANSFER_GAMMA2_8 = 25165824 , + + + + + + + + + + + + + + + TRANSFER_ST2084 = 29360128 , + + + + + + + + + + + + + + + TRANSFER_HLG = 33554432 , RANGE_SHIFT = 27, - /** - * Range aspect - * - * Defines the range of values corresponding to the unit range of 0-1. - * This is defined for YCbCr only, but can be expanded to RGB space. - */ - RANGE_MASK = 939524096 /* (7 << RANGE_SHIFT) */, - /** - * Range is unknown or are determined by the application. Implementations - * shall use the following suggested ranges: - * - * All YCbCr formats: limited range. - * All RGB or RGBA formats (including RAW and Bayer): full range. - * All Y formats: full range - * - * For all other formats range is undefined, and implementations should use - * an appropriate range for the data represented. - */ - RANGE_UNSPECIFIED = 0 /* (0 << RANGE_SHIFT) */, - /** - * Full range uses all values for Y, Cb and Cr from - * 0 to 2^b-1, where b is the bit depth of the color format. - */ - RANGE_FULL = 134217728 /* (1 << RANGE_SHIFT) */, - /** - * Limited range uses values 16/256*2^b to 235/256*2^b for Y, and - * 1/16*2^b to 15/16*2^b for Cb, Cr, R, G and B, where b is the bit depth of - * the color format. - * - * E.g. For 8-bit-depth formats: - * Luma (Y) samples should range from 16 to 235, inclusive - * Chroma (Cb, Cr) samples should range from 16 to 240, inclusive - * - * For 10-bit-depth formats: - * Luma (Y) samples should range from 64 to 940, inclusive - * Chroma (Cb, Cr) samples should range from 64 to 960, inclusive - */ - RANGE_LIMITED = 268435456 /* (2 << RANGE_SHIFT) */, - /** - * Extended range is used for scRGB. Intended for use with - * floating point pixel formats. [0.0 - 1.0] is the standard - * sRGB space. Values outside the range 0.0 - 1.0 can encode - * color outside the sRGB gamut. - * Used to blend / merge multiple dataspaces on a single display. - */ - RANGE_EXTENDED = 402653184 /* (3 << RANGE_SHIFT) */, - /** - * Legacy dataspaces - * - * - * sRGB linear encoding: - * - * The red, green, and blue components are stored in sRGB space, but - * are linear, not gamma-encoded. - * The RGB primaries and the white point are the same as BT.709. - * - * The values are encoded using the full range ([0,255] for 8-bit) for all - * components. - */ - SRGB_LINEAR = 512 /* 0x200 */, - V0_SRGB_LINEAR = 138477568 /* ((STANDARD_BT709 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * scRGB linear encoding: - * - * The red, green, and blue components are stored in extended sRGB space, - * but are linear, not gamma-encoded. - * The RGB primaries and the white point are the same as BT.709. - * - * The values are floating point. - * A pixel value of 1.0, 1.0, 1.0 corresponds to sRGB white (D65) at 80 nits. - * Values beyond the range [0.0 - 1.0] would correspond to other colors - * spaces and/or HDR content. - */ - V0_SCRGB_LINEAR = 406913024 /* ((STANDARD_BT709 | TRANSFER_LINEAR) | RANGE_EXTENDED) */, - /** - * sRGB gamma encoding: - * - * The red, green and blue components are stored in sRGB space, and - * converted to linear space when read, using the SRGB transfer function - * for each of the R, G and B components. When written, the inverse - * transformation is performed. - * - * The alpha component, if present, is always stored in linear space and - * is left unmodified when read or written. - * - * Use full range and BT.709 standard. - */ - SRGB = 513 /* 0x201 */, - V0_SRGB = 142671872 /* ((STANDARD_BT709 | TRANSFER_SRGB) | RANGE_FULL) */, - /** - * scRGB: - * - * The red, green, and blue components are stored in extended sRGB space, - * but are linear, not gamma-encoded. - * The RGB primaries and the white point are the same as BT.709. - * - * The values are floating point. - * A pixel value of 1.0, 1.0, 1.0 corresponds to sRGB white (D65) at 80 nits. - * Values beyond the range [0.0 - 1.0] would correspond to other colors - * spaces and/or HDR content. - */ - V0_SCRGB = 411107328 /* ((STANDARD_BT709 | TRANSFER_SRGB) | RANGE_EXTENDED) */, - /** - * YCbCr Colorspaces - * ----------------- - * - * Primaries are given using (x,y) coordinates in the CIE 1931 definition - * of x and y specified by ISO 11664-1. - * - * Transfer characteristics are the opto-electronic transfer characteristic - * at the source as a function of linear optical intensity (luminance). - * - * - * JPEG File Interchange Format (JFIF) - * - * Same model as BT.601-625, but all values (Y, Cb, Cr) range from 0 to 255 - * - * Use full range, BT.601 transfer and BT.601_625 standard. - */ - JFIF = 257 /* 0x101 */, - V0_JFIF = 146931712 /* ((STANDARD_BT601_625 | TRANSFER_SMPTE_170M) | RANGE_FULL) */, - /** - * ITU-R Recommendation 601 (BT.601) - 625-line - * - * Standard-definition television, 625 Lines (PAL) - * - * Use limited range, BT.601 transfer and BT.601_625 standard. - */ - BT601_625 = 258 /* 0x102 */, - V0_BT601_625 = 281149440 /* ((STANDARD_BT601_625 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * ITU-R Recommendation 601 (BT.601) - 525-line - * - * Standard-definition television, 525 Lines (NTSC) - * - * Use limited range, BT.601 transfer and BT.601_525 standard. - */ - BT601_525 = 259 /* 0x103 */, - V0_BT601_525 = 281280512 /* ((STANDARD_BT601_525 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * ITU-R Recommendation 709 (BT.709) - * - * High-definition television - * - * Use limited range, BT.709 transfer and BT.709 standard. - */ - BT709 = 260 /* 0x104 */, - V0_BT709 = 281083904 /* ((STANDARD_BT709 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * SMPTE EG 432-1 and SMPTE RP 431-2. - * - * Digital Cinema DCI-P3 - * - * Use full range, linear transfer and D65 DCI-P3 standard - */ - DCI_P3_LINEAR = 139067392 /* ((STANDARD_DCI_P3 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * SMPTE EG 432-1 and SMPTE RP 431-2. - * - * Digital Cinema DCI-P3 - * - * Use full range, gamma 2.6 transfer and D65 DCI-P3 standard - * Note: Application is responsible for gamma encoding the data as - * a 2.6 gamma encoding is not supported in HW. - */ - DCI_P3 = 155844608 /* ((STANDARD_DCI_P3 | TRANSFER_GAMMA2_6) | RANGE_FULL) */, - /** - * Display P3 - * - * Display P3 uses same primaries and white-point as DCI-P3 - * linear transfer function makes this the same as DCI_P3_LINEAR. - */ - DISPLAY_P3_LINEAR = 139067392 /* ((STANDARD_DCI_P3 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * Display P3 - * - * Use same primaries and white-point as DCI-P3 - * but sRGB transfer function. - */ - DISPLAY_P3 = 143261696 /* ((STANDARD_DCI_P3 | TRANSFER_SRGB) | RANGE_FULL) */, - /** - * Adobe RGB - * - * Use full range, gamma 2.2 transfer and Adobe RGB primaries - * Note: Application is responsible for gamma encoding the data as - * a 2.2 gamma encoding is not supported in HW. - */ - ADOBE_RGB = 151715840 /* ((STANDARD_ADOBE_RGB | TRANSFER_GAMMA2_2) | RANGE_FULL) */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use full range, linear transfer and BT2020 standard - */ - BT2020_LINEAR = 138805248 /* ((STANDARD_BT2020 | TRANSFER_LINEAR) | RANGE_FULL) */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use full range, BT.709 transfer and BT2020 standard - */ - BT2020 = 147193856 /* ((STANDARD_BT2020 | TRANSFER_SMPTE_170M) | RANGE_FULL) */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use full range, SMPTE 2084 (PQ) transfer and BT2020 standard - */ - BT2020_PQ = 163971072 /* ((STANDARD_BT2020 | TRANSFER_ST2084) | RANGE_FULL) */, - /** - * Data spaces for non-color formats - * - * - * The buffer contains depth ranging measurements from a depth camera. - * This value is valid with formats: - * HAL_PIXEL_FORMAT_Y16: 16-bit samples, consisting of a depth measurement - * and an associated confidence value. The 3 MSBs of the sample make - * up the confidence value, and the low 13 LSBs of the sample make up - * the depth measurement. - * For the confidence section, 0 means 100% confidence, 1 means 0% - * confidence. The mapping to a linear float confidence value between - * 0.f and 1.f can be obtained with - * float confidence = (((depthSample >> 13) - 1) & 0x7) / 7.0f; - * The depth measurement can be extracted simply with - * uint16_t range = (depthSample & 0x1FFF); - * HAL_PIXEL_FORMAT_BLOB: A depth point cloud, as - * a variable-length float (x,y,z, confidence) coordinate point list. - * The point cloud will be represented with the android_depth_points - * structure. - */ - DEPTH = 4096 /* 0x1000 */, - /** - * The buffer contains sensor events from sensor direct report. - * This value is valid with formats: - * HAL_PIXEL_FORMAT_BLOB: an array of sensor event structure that forms - * a lock free queue. Format of sensor event structure is specified - * in Sensors HAL. - */ - SENSOR = 4097 /* 0x1001 */, - /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television - * - * Use limited range, BT.709 transfer and BT2020 standard - */ - BT2020_ITU = 281411584 /* ((STANDARD_BT2020 | TRANSFER_SMPTE_170M) | RANGE_LIMITED) */, - /** - * ITU-R Recommendation 2100 (BT.2100) - * - * High dynamic range television - * - * Use limited/full range, PQ/HLG transfer, and BT2020 standard - * limited range is the preferred / normative definition for BT.2100 - */ - BT2020_ITU_PQ = 298188800 /* ((STANDARD_BT2020 | TRANSFER_ST2084) | RANGE_LIMITED) */, - BT2020_ITU_HLG = 302383104 /* ((STANDARD_BT2020 | TRANSFER_HLG) | RANGE_LIMITED) */, - BT2020_HLG = 168165376 /* ((STANDARD_BT2020 | TRANSFER_HLG) | RANGE_FULL) */, + + + + + + + RANGE_MASK = 939524096 , + + + + + + + + + + + + RANGE_UNSPECIFIED = 0 , + + + + + RANGE_FULL = 134217728 , + + + + + + + + + + + + + + RANGE_LIMITED = 268435456 , + + + + + + + + RANGE_EXTENDED = 402653184 , + + + + + + + + + + + + + + SRGB_LINEAR = 512 , + V0_SRGB_LINEAR = 138477568 , + + + + + + + + + + + + + V0_SCRGB_LINEAR = 406913024 , + + + + + + + + + + + + + + SRGB = 513 , + V0_SRGB = 142671872 , + + + + + + + + + + + + + V0_SCRGB = 411107328 , + + + + + + + + + + + + + + + + + + JFIF = 257 , + V0_JFIF = 146931712 , + + + + + + + + BT601_625 = 258 , + V0_BT601_625 = 281149440 , + + + + + + + + BT601_525 = 259 , + V0_BT601_525 = 281280512 , + + + + + + + + BT709 = 260 , + V0_BT709 = 281083904 , + + + + + + + + DCI_P3_LINEAR = 139067392 , + + + + + + + + + + DCI_P3 = 155844608 , + + + + + + + DISPLAY_P3_LINEAR = 139067392 , + + + + + + + DISPLAY_P3 = 143261696 , + + + + + + + + ADOBE_RGB = 151715840 , + + + + + + + + BT2020_LINEAR = 138805248 , + + + + + + + + BT2020 = 147193856 , + + + + + + + + BT2020_PQ = 163971072 , + + + + + + + + + + + + + + + + + + + + + + DEPTH = 4096 , + + + + + + + + SENSOR = 4097 , + + + + + + + + BT2020_ITU = 281411584 , + + + + + + + + + BT2020_ITU_PQ = 298188800 , + BT2020_ITU_HLG = 302383104 , + BT2020_HLG = 168165376 , }; enum class ColorMode : int32_t { - /** - * DEFAULT is the "native" gamut of the display. - * White Point: Vendor/OEM defined - * Panel Gamma: Vendor/OEM defined (typically 2.2) - * Rendering Intent: Vendor/OEM defined (typically 'enhanced') - */ NATIVE = 0, - /** - * STANDARD_BT601_625 corresponds with display - * settings that implement the ITU-R Recommendation BT.601 - * or Rec 601. Using 625 line version - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_625 = 1, - /** - * Primaries: - * x y - * green 0.290 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.222, KB = 0.071 luminance interpretation - * for RGB conversion. - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_625_UNADJUSTED = 2, - /** - * Primaries: - * x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * KR = 0.299, KB = 0.114. This adjusts the luminance interpretation - * for RGB conversion from the one purely determined by the primaries - * to minimize the color shift into RGB space that uses BT.709 - * primaries. - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_525 = 3, - /** - * Primaries: - * x y - * green 0.310 0.595 - * blue 0.155 0.070 - * red 0.630 0.340 - * white (D65) 0.3127 0.3290 - * - * Use the unadjusted KR = 0.212, KB = 0.087 luminance interpretation - * for RGB conversion (as in SMPTE 240M). - * - * Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.500 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear)^(0.45) – 0.099 - */ STANDARD_BT601_525_UNADJUSTED = 4, - /** - * REC709 corresponds with display settings that implement - * the ITU-R Recommendation BT.709 / Rec. 709 for high-definition television. - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.300 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * HDTV REC709 Inverse Gamma Correction (IGC): V represents normalized - * (with [0 to 1] range) value of R, G, or B. - * - * if Vnonlinear < 0.081 - * Vlinear = Vnonlinear / 4.5 - * else - * Vlinear = ((Vnonlinear + 0.099) / 1.099) ^ (1/0.45) - * - * HDTV REC709 Gamma Correction (GC): - * - * if Vlinear < 0.018 - * Vnonlinear = 4.5 * Vlinear - * else - * Vnonlinear = 1.099 * (Vlinear) ^ 0.45 – 0.099 - */ STANDARD_BT709 = 5, - /** - * DCI_P3 corresponds with display settings that implement - * SMPTE EG 432-1 and SMPTE RP 431-2 - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.265 0.690 - * blue 0.150 0.060 - * red 0.680 0.320 - * white (D65) 0.3127 0.3290 - * - * Gamma: 2.6 - */ DCI_P3 = 6, - /** - * SRGB corresponds with display settings that implement - * the sRGB color space. Uses the same primaries as ITU-R Recommendation - * BT.709 - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.300 0.600 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * PC/Internet (sRGB) Inverse Gamma Correction (IGC): - * - * if Vnonlinear ≤ 0.03928 - * Vlinear = Vnonlinear / 12.92 - * else - * Vlinear = ((Vnonlinear + 0.055)/1.055) ^ 2.4 - * - * PC/Internet (sRGB) Gamma Correction (GC): - * - * if Vlinear ≤ 0.0031308 - * Vnonlinear = 12.92 * Vlinear - * else - * Vnonlinear = 1.055 * (Vlinear)^(1/2.4) – 0.055 - */ SRGB = 7, - /** - * ADOBE_RGB corresponds with the RGB color space developed - * by Adobe Systems, Inc. in 1998. - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.210 0.710 - * blue 0.150 0.060 - * red 0.640 0.330 - * white (D65) 0.3127 0.3290 - * - * Gamma: 2.2 - */ ADOBE_RGB = 8, - /** - * DISPLAY_P3 is a color space that uses the DCI_P3 primaries, - * the D65 white point and the SRGB transfer functions. - * Rendering Intent: Colorimetric - * Primaries: - * x y - * green 0.265 0.690 - * blue 0.150 0.060 - * red 0.680 0.320 - * white (D65) 0.3127 0.3290 - * - * PC/Internet (sRGB) Gamma Correction (GC): - * - * if Vlinear ≤ 0.0030186 - * Vnonlinear = 12.92 * Vlinear - * else - * Vnonlinear = 1.055 * (Vlinear)^(1/2.4) – 0.055 - * - * Note: In most cases sRGB transfer function will be fine. - */ DISPLAY_P3 = 9, - /** - * BT2020 corresponds with display settings that implement the ITU-R - * Recommendation BT.2020 / Rec. 2020 for UHDTV. - * - * Primaries: - * x y - * green 0.170 0.797 - * blue 0.131 0.046 - * red 0.708 0.292 - * white (D65) 0.3127 0.3290 - * - * Inverse Gamma Correction (IGC): V represents normalized (with [0 to 1] - * range) value of R, G, or B. - * - * if Vnonlinear < b * 4.5 - * Vlinear = Vnonlinear / 4.5 - * else - * Vlinear = ((Vnonlinear + (a - 1)) / a) ^ (1/0.45) - * - * Gamma Correction (GC): - * - * if Vlinear < b - * Vnonlinear = 4.5 * Vlinear - * else - * Vnonlinear = a * Vlinear ^ 0.45 - (a - 1) - * - * where - * - * a = 1.09929682680944, b = 0.018053968510807 - * - * For practical purposes, these a/b values can be used instead - * - * a = 1.099, b = 0.018 for 10-bit display systems - * a = 1.0993, b = 0.0181 for 12-bit display systems - */ BT2020 = 10, - /** - * BT2100_PQ and BT2100_HLG correspond with display settings that - * implement the ITU-R Recommendation BT.2100 / Rec. 2100 for HDR TV. - * - * Primaries: - * x y - * green 0.170 0.797 - * blue 0.131 0.046 - * red 0.708 0.292 - * white (D65) 0.3127 0.3290 - * - * For BT2100_PQ, the transfer function is Perceptual Quantizer (PQ). For - * BT2100_HLG, the transfer function is Hybrid Log-Gamma (HLG). - */ BT2100_PQ = 11, BT2100_HLG = 12, }; -/** - * RenderIntent defines the mapping from color mode colors to display colors. - * - * A render intent must not change how it maps colors when the color mode - * changes. That is to say that when a render intent maps color C to color C', - * the fact that color C can have different pixel values in different color - * modes should not affect the mapping. - * - * RenderIntent overrides the render intents defined for individual color - * modes. It is ignored when the color mode is ColorMode::NATIVE, because - * ColorMode::NATIVE colors are already display colors. - */ enum class RenderIntent : int32_t { - /** - * Colors in the display gamut are unchanged. Colors out of the display - * gamut are hard-clipped. - * - * This implies that the display must have been calibrated unless - * ColorMode::NATIVE is the only supported color mode. - */ COLORIMETRIC = 0, - /** - * Enhance colors that are in the display gamut. Colors out of the display - * gamut are hard-clipped. - * - * The enhancement typically picks the biggest standard color space (e.g. - * DCI-P3) that is narrower than the display gamut and stretches it to the - * display gamut. The stretching is recommended to preserve skin tones. - */ ENHANCE = 1, - /** - * Tone map high-dynamic-range colors to the display's dynamic range. The - * dynamic range of the colors are communicated separately. After tone - * mapping, the mapping to the display gamut is as defined in - * COLORIMETRIC. - */ TONE_MAP_COLORIMETRIC = 2, - /** - * Tone map high-dynamic-range colors to the display's dynamic range. The - * dynamic range of the colors are communicated separately. After tone - * mapping, the mapping to the display gamut is as defined in ENHANCE. - * - * The tone mapping step and the enhancing step must match - * TONE_MAP_COLORIMETRIC and ENHANCE respectively when they are also - * supported. - */ TONE_MAP_ENHANCE = 3, }; -// -// type declarations for package -// template static inline std::string toString(int32_t o); @@ -5050,9 +4422,6 @@ constexpr int32_t& operator&=(int32_t& v, return v; } -// -// type header definitions for package -// template <> inline std::string toString<::android::hardware::graphics::common::V1_1::PixelFormat>(int32_t o) { @@ -6546,15 +5915,12 @@ static inline std::string toString(::android::hardware::graphics::common::V1_1:: return os; } -} // namespace V1_1 -} // namespace common -} // namespace graphics -} // namespace hardware -} // namespace android +} +} +} +} +} -// -// global type declarations for package -// namespace android { namespace hardware { @@ -6590,9 +5956,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_1::PixelFormat, 2 ::android::hardware::graphics::common::V1_1::PixelFormat::STENCIL_8, ::android::hardware::graphics::common::V1_1::PixelFormat::YCBCR_P010, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -6626,9 +5992,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_1::BufferUsage, 2 ::android::hardware::graphics::common::V1_1::BufferUsage::GPU_CUBE_MAP, ::android::hardware::graphics::common::V1_1::BufferUsage::GPU_MIPMAP_COMPLETE, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -6698,9 +6064,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_1::Dataspace, 61> ::android::hardware::graphics::common::V1_1::Dataspace::BT2020_ITU_HLG, ::android::hardware::graphics::common::V1_1::Dataspace::BT2020_HLG, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -6722,9 +6088,9 @@ constexpr std::array<::android::hardware::graphics::common::V1_1::ColorMode, 13> ::android::hardware::graphics::common::V1_1::ColorMode::BT2100_PQ, ::android::hardware::graphics::common::V1_1::ColorMode::BT2100_HLG, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -6737,8 +6103,8 @@ constexpr std::array<::android::hardware::graphics::common::V1_1::RenderIntent, ::android::hardware::graphics::common::V1_1::RenderIntent::TONE_MAP_COLORIMETRIC, ::android::hardware::graphics::common::V1_1::RenderIntent::TONE_MAP_ENHANCE, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} -#endif // HIDL_GENERATED_ANDROID_HARDWARE_GRAPHICS_COMMON_V1_1_TYPES_H +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapper.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapper.h index ce126427d..f060f1c1e 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapper.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapper.h @@ -12,7 +12,6 @@ namespace graphics { namespace mapper { namespace V2_0 { -// Forward declaration for forward reference support: enum class Error : int32_t; struct YCbCrLayout; @@ -25,34 +24,8 @@ enum class Error : int32_t { UNSUPPORTED = 7, }; -/** - * A buffer descriptor is an implementation-defined opaque data returned by - * createDescriptor. It describes the properties of a buffer and is consumed - * by the allocator. - */ typedef ::android::hardware::hidl_vec BufferDescriptor; -/** - * Structure for describing YCbCr formats for consumption by applications. - * This is used with PixelFormat::YCBCR_*_888. - * - * Buffer chroma subsampling is defined in the format. - * e.g. PixelFormat::YCBCR_420_888 has subsampling 4:2:0. - * - * Buffers must have a 8 bit depth. - * - * y, cb, and cr point to the first byte of their respective planes. - * - * Stride describes the distance in bytes from the first value of one row of - * the image to the first value of the next row. It includes the width of the - * image plus padding. - * yStride is the stride of the luma plane. - * cStride is the stride of the chroma planes. - * - * chromaStep is the distance in bytes from one chroma pixel value to the - * next. This is 2 bytes for semiplanar (because chroma values are interleaved - * and each chroma value is one byte) and 1 for planar. - */ struct YCbCrLayout final { void* y; void* cb; @@ -62,9 +35,6 @@ struct YCbCrLayout final { uint32_t chromaStep; }; -// -// type declarations for package -// template static inline std::string toString(int32_t o); @@ -107,11 +77,7 @@ constexpr int32_t& operator&=(int32_t& v, static inline std::string toString( const ::android::hardware::graphics::mapper::V2_0::YCbCrLayout& o); -// operator== and operator!= are not generated for YCbCrLayout -// -// type header definitions for package -// template <> inline std::string toString<::android::hardware::graphics::mapper::V2_0::Error>(int32_t o) { @@ -218,17 +184,17 @@ static inline std::string toString( return os; } -// operator== and operator!= are not generated for YCbCrLayout -} // namespace V2_0 -} // namespace mapper -} // namespace graphics -} // namespace hardware -} // namespace android -// -// global type declarations for package -// +} +} +} +} +} + + + + namespace android { namespace hardware { @@ -243,8 +209,8 @@ constexpr std::array<::android::hardware::graphics::mapper::V2_0::Error, 6> ::android::hardware::graphics::mapper::V2_0::Error::NO_RESOURCES, ::android::hardware::graphics::mapper::V2_0::Error::UNSUPPORTED, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} -#endif // HIDL_GENERATED_ANDROID_HARDWARE_GRAPHICS_MAPPER_V2_0_TYPES_H +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapperextensions.h b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapperextensions.h index 79fddd18e..6890973e0 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapperextensions.h +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/ext/types/mapperextensions.h @@ -13,7 +13,6 @@ namespace display { namespace mapperextensions { namespace V1_0 { -// Forward declaration for forward reference support: enum class Error : int32_t; struct YCbCrLayout; enum class PlaneComponent : uint32_t; @@ -36,61 +35,22 @@ struct YCbCrLayout final { uint32_t chromaStep; }; -/** - * Plane Component type definitions. - */ enum class PlaneComponent : uint32_t { - PLANE_COMPONENT_Y = 1u /* (1 << 0) */, - PLANE_COMPONENT_Cb = 2u /* (1 << 1) */, - PLANE_COMPONENT_Cr = 4u /* (1 << 2) */, - PLANE_COMPONENT_R = 1024u /* (1 << 10) */, - PLANE_COMPONENT_G = 2048u /* (1 << 11) */, - PLANE_COMPONENT_B = 4096u /* (1 << 12) */, - PLANE_COMPONENT_A = 1048576u /* (1 << 20) */, - PLANE_COMPONENT_RAW = 1073741824u /* (1 << 30) */, - PLANE_COMPONENT_META = 2147483648u /* (1 << 31) */, + PLANE_COMPONENT_Y = 1u , + PLANE_COMPONENT_Cb = 2u , + PLANE_COMPONENT_Cr = 4u , + PLANE_COMPONENT_R = 1024u , + PLANE_COMPONENT_G = 2048u , + PLANE_COMPONENT_B = 4096u , + PLANE_COMPONENT_A = 1048576u , + PLANE_COMPONENT_RAW = 1073741824u , + PLANE_COMPONENT_META = 2147483648u , }; -/** - * Flags to send with getFormatLayout for extra information for e.g. interlaced, - * otherwise send 0. - */ enum class Flags : int32_t { - LAYOUT_INTERLACED_FLAG = 1 /* (1 << 0) */, + LAYOUT_INTERLACED_FLAG = 1 , }; -/** - * Structure for describing plane layout information for YUVA/RGBA/RAW formats - * YUV formats can be planar or semi-planar. - * - * This layout structure is used with any pixel format that can be - * represented by it, such as: - * - HAL_PIXEL_FORMAT_YCbCr_* - * - HAL_PIXEL_FORMAT_RGB*_* - * - even implementation defined formats that can be represented by - * the structures - * - * Each plane can be made up of one or more PlaneComponents depending on - * the format. - * - * Each plane can be subsampled either vertically or horizontally by - * a power-of-two factor. - * - * Pixel increment describes the distance in bytes from one pixel to the - * next pixel (to the right) on the same row for the component plane. - * This can be negative. - * - * Stride is provided in both pixels and bytes. - * Stride_bytes describes the distance in bytes from the first pixel of - * one row to the first pixel of the next row (below) for the component plane. - * This can be negative. - * - * Scanlines describes the height of the plane in terms of number of rows. - * - * Size of plane is stride_bytes * scanlines + any padding required for - * alignment as per format specification. - * - */ struct PlaneLayout final { ::android::hardware::hidl_bitfield< ::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneComponent> @@ -137,9 +97,6 @@ static_assert(sizeof(::vendor::qti::hardware::display::mapperextensions::V1_0::P static_assert(__alignof(::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneLayout) == 4, "wrong alignment"); -// -// type declarations for package -// template static inline std::string toString(int32_t o); @@ -185,7 +142,6 @@ constexpr int32_t& operator&=( static inline std::string toString( const ::vendor::qti::hardware::display::mapperextensions::V1_0::YCbCrLayout& o); -// operator== and operator!= are not generated for YCbCrLayout template static inline std::string toString(uint32_t o); @@ -284,9 +240,6 @@ static inline bool operator!=( const ::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneLayout& lhs, const ::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneLayout& rhs); -// -// type header definitions for package -// template <> inline std::string toString<::vendor::qti::hardware::display::mapperextensions::V1_0::Error>( @@ -379,7 +332,6 @@ static inline std::string toString( return os; } -// operator== and operator!= are not generated for YCbCrLayout template <> inline std::string @@ -642,16 +594,13 @@ static inline bool operator!=( return !(lhs == rhs); } -} // namespace V1_0 -} // namespace mapperextensions -} // namespace display -} // namespace hardware -} // namespace qti -} // namespace vendor +} +} +} +} +} +} -// -// global type declarations for package -// namespace android { namespace hardware { @@ -664,9 +613,9 @@ constexpr std::array<::vendor::qti::hardware::display::mapperextensions::V1_0::E ::vendor::qti::hardware::display::mapperextensions::V1_0::Error::NO_RESOURCES, ::vendor::qti::hardware::display::mapperextensions::V1_0::Error::UNSUPPORTED, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -688,9 +637,9 @@ constexpr std::array<::vendor::qti::hardware::display::mapperextensions::V1_0::P ::vendor::qti::hardware::display::mapperextensions::V1_0::PlaneComponent:: PLANE_COMPONENT_META, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} namespace android { namespace hardware { @@ -700,8 +649,8 @@ constexpr std::array<::vendor::qti::hardware::display::mapperextensions::V1_0::F hidl_enum_values<::vendor::qti::hardware::display::mapperextensions::V1_0::Flags> = { ::vendor::qti::hardware::display::mapperextensions::V1_0::Flags::LAYOUT_INTERLACED_FLAG, }; -} // namespace details -} // namespace hardware -} // namespace android +} +} +} -#endif // HIDL_GENERATED_VENDOR_QTI_HARDWARE_DISPLAY_MAPPEREXTENSIONS_V1_0_TYPES_H +#endif diff --git a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/shim.cpp b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/shim.cpp index 34dd68296..3c8a0134e 100644 --- a/app/src/main/cpp/adrenotools/tools/qtimapper-shim/shim.cpp +++ b/app/src/main/cpp/adrenotools/tools/qtimapper-shim/shim.cpp @@ -46,14 +46,13 @@ using namespace android::hardware; using namespace android; -// These two functions are used by the blob and aren't present on Q so weakly stub them extern "C" uint64_t __attribute__((weak)) atrace_get_enabled_tags() { return 0; } namespace android::hardware::details { void __attribute__((weak)) return_status::onValueRetrieval() const {} -} // namespace android::hardware::details +} bool IBase::isRemote() const { return false; @@ -146,7 +145,7 @@ sp IMapper::getService(const std::string& serviceName, const bool getSt return nullptr; } -} // namespace V2_0 +} namespace V3_0 { sp IMapper::getService(const std::string& serviceName, const bool getStub) { @@ -154,7 +153,7 @@ sp IMapper::getService(const std::string& serviceName, const bool getSt return nullptr; } -} // namespace V3_0 +} namespace V4_0 { sp IMapper::getService(const std::string& serviceName, const bool getStub) { @@ -162,8 +161,8 @@ sp IMapper::getService(const std::string& serviceName, const bool getSt return nullptr; } -} // namespace V4_0 -} // namespace android::hardware::graphics::mapper +} +} namespace vendor::qti::hardware::display { namespace mapper { namespace V4_0 { @@ -174,7 +173,7 @@ Return> IQtiMapper::castFrom(const sp> IQtiMapper::castFrom(const sp& parent, @@ -183,7 +182,7 @@ Return> IQtiMapper::castFrom(const sp> IQtiMapper::castFrom(const sp& parent, @@ -200,8 +199,8 @@ Return IQtiMapper::getMapperExtensions(getMapperExtensions_cb _hidl_cb) { return Void(); } -} // namespace V2_0 -} // namespace mapper +} +} namespace mapperextensions { namespace V1_1 { @@ -467,6 +466,6 @@ Return IQtiMapperExtensions::getFormatLayout(int32_t format, uint64_t usag return Void(); } -} // namespace V1_1 -} // namespace mapperextensions -} // namespace vendor::qti::hardware::display +} +} +} diff --git a/app/src/main/cpp/deviceinfo/vkinfo.c b/app/src/main/cpp/deviceinfo/vkinfo.c index a8d769818..81ccc6b0d 100644 --- a/app/src/main/cpp/deviceinfo/vkinfo.c +++ b/app/src/main/cpp/deviceinfo/vkinfo.c @@ -1,6 +1,3 @@ -// What the phone's own Vulkan driver says about its GPU, for the app's device info window: one -// instance on the system loader, the first physical device's properties, a few limits and the -// extensions DXVK and VKD3D-Proton lean on. Returned as "key=value" lines; nothing is kept open. #include #include #include @@ -62,12 +59,10 @@ Java_com_droiddeck_launcher_gpu_VulkanInfo_nativeQuery(JNIEnv *env, jclass clazz LINE("device=%s\n", p.deviceName); LINE("type=%s\n", type_name(p.deviceType)); LINE("api=%u.%u.%u\n", VK_API_VERSION_MAJOR(p.apiVersion), VK_API_VERSION_MINOR(p.apiVersion), VK_API_VERSION_PATCH(p.apiVersion)); - // Qualcomm packs its driver version as 10.10.12 bits, the same layout as VK_MAKE_VERSION. LINE("driver_version=%u.%u.%u\n", p.driverVersion >> 22, (p.driverVersion >> 12) & 0x3ff, p.driverVersion & 0xfff); LINE("vendor_id=0x%04x\ndevice_id=0x%08x\n", p.vendorID, p.deviceID); LINE("max_image_2d=%u\n", p.limits.maxImageDimension2D); - // Driver name and build string (Vulkan 1.2 or VK_KHR_driver_properties). uint32_t extCount = 0; if (vkEnumerateDeviceExtensionProperties) vkEnumerateDeviceExtensionProperties(device, NULL, &extCount, NULL); static VkExtensionProperties exts[512]; diff --git a/app/src/main/cpp/fakeinput_steam.cpp b/app/src/main/cpp/fakeinput_steam.cpp index 984650c59..c70d460a9 100644 --- a/app/src/main/cpp/fakeinput_steam.cpp +++ b/app/src/main/cpp/fakeinput_steam.cpp @@ -1,12 +1,8 @@ /* - * The fake evdev layer. Taken from WinNative (maxjivi05, main 0cfc070b + feature/wayland-gamescope - * through f1f34cd7, 2026-09-14..19), GPL-3.0: the locking, the fork handlers, the shared device - * table, the Steam virtual-gamepad identity and the Z/RZ trigger placement are that author's. - * Added here: an opt-in Xbox 360 identity (FAKE_EVDEV_IDENTITY=xbox360), which is what gets SDL's - * built-in mapping applied with no configuration when the pad is read by the Steam client itself; - * and a stand-in for /dev/uinput (FAKE_EVDEV_UINPUT=1), so the virtual pads Steam Input makes for - * a game become more of these nodes instead of vanishing. + * Taken from WinNative (maxjivi05, main 0cfc070b + feature/wayland-gamescope + * through f1f34cd7, 2026-09-14..19), GPL-3.0. */ + #include #include #include @@ -57,8 +53,6 @@ #define EXPORT extern "C" __attribute__((visibility("default"))) -// glibc declares the request as unsigned long; bionic as int. The same source also serves the -// Linux runtime's preload (tools/linuxfs), where Steam and SDL run against glibc. #ifdef __GLIBC__ typedef unsigned long ioctl_request_t; #else @@ -69,13 +63,9 @@ static constexpr uint16_t GAMEPAD_VENDOR_ID_BASE = 0x1234; static constexpr uint16_t GAMEPAD_PRODUCT_ID_BASE = 0x5678; static constexpr uint16_t GAMEPAD_VERSION = 0x0110; static constexpr const char *GAMEPAD_NAME_TEMPLATE = "Generic HID Gamepad %d"; -// The gamepad Steam Input presents to a game, and the name SDL reads its slot from. static constexpr uint16_t STEAM_VIRTUAL_VENDOR_ID = 0x28de; static constexpr uint16_t STEAM_VIRTUAL_PRODUCT_ID = 0x11ff; static constexpr const char *STEAM_VIRTUAL_NAME_TEMPLATE = "Microsoft X-Box 360 pad %d"; -// Opt-in identity for the process that reads the pad directly (the Steam client): SDL keys its -// mapping database on bus+vendor+product, and 045E:028E is the most widely mapped id there is. -// Not offset per slot: a real pad reports the same ids on every port. static constexpr uint16_t X360_VENDOR_ID = 0x045E; static constexpr uint16_t X360_PRODUCT_ID = 0x028E; static constexpr const char *X360_NAME_TEMPLATE = "Xbox 360 Controller (%d)"; @@ -90,27 +80,25 @@ 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; static constexpr unsigned int FAKE_INPUT_JS_MINOR_BASE = 0; -// Nodes from event16 up are the virtual pads programs make through the /dev/uinput stand-in; -// below that are the app's own rings, one per slot. static constexpr int UINPUT_EVENT_BASE = 16; static constexpr int UINPUT_EVENT_LIMIT = 64; static constexpr uint32_t UINPUT_DESCRIPTION_MAGIC = 0x46495544; struct FakeInputRingHeader { - uint32_t magic; // 0 - uint32_t version; // 4 - uint32_t event_size; // 8 - uint32_t capacity; // 12 - uint64_t write_seq; // 16 - uint64_t generation; // 24 - // Authoritative absolute-state snapshot, published by the writer under a - // seqlock (odd snapshot_seq = write in progress). The reader replays it as a - // full keyframe whenever the delta stream could have desynced (open, ring - // overflow) so dropped events can recover without periodic duplicate input. - uint64_t snapshot_seq; // 32 - uint32_t snapshot_buttons; // 40 bit i -> kSnapshotButtons[i] pressed - int16_t snapshot_axes[8]; // 44 values in kSnapshotAxisCodes order - uint32_t resync_seq; // 60 + uint32_t magic; + uint32_t version; + uint32_t event_size; + uint32_t capacity; + uint64_t write_seq; + uint64_t generation; + + + + + uint64_t snapshot_seq; + uint32_t snapshot_buttons; + int16_t snapshot_axes[8]; + uint32_t resync_seq; }; static_assert(sizeof(FakeInputRingHeader) == 64, @@ -122,8 +110,6 @@ static constexpr size_t FAKE_INPUT_RING_SIZE = FAKE_INPUT_RING_HEADER_SIZE + (FAKE_INPUT_RING_CAPACITY * FAKE_INPUT_EVENT_SIZE); -// What a program told the /dev/uinput stand-in about the device it made, stored after the ring -// in that device's file so every process that opens the node answers the same identity. struct UinputDescription { uint32_t magic; int32_t owner_pid; @@ -142,7 +128,6 @@ static constexpr size_t UINPUT_FILE_SIZE = FAKE_INPUT_RING_SIZE + sizeof(UinputD struct FakeController { char *event = nullptr; - // Set for a node made through the /dev/uinput stand-in; null for the app's own pads. const UinputDescription *uinput = nullptr; int slot = -1; FakeInputRingHeader *ring = nullptr; @@ -157,9 +142,6 @@ struct FakeController { if (ring) munmap(ring, mapping_size); free(event); } - // Pending keyframe (full absolute-state baseline) currently streaming to the - // guest. The axis/button values are captured from the snapshot when the - // keyframe starts so the frame stays consistent across multi-read delivery. size_t keyframe_remaining = 0; int32_t keyframe_axes[8] = {0, 0, 0, 0, 0, 0, 0, 0}; uint32_t keyframe_buttons = 0; @@ -170,11 +152,6 @@ struct NeutralEventSpec { uint16_t code; }; -// Event template for a full keyframe: every button, every axis/hat, then a -// SYN_REPORT, in this fixed order. The value carried by each event is filled -// from the authoritative snapshot (see keyframe_value); an all-zero snapshot -// yields the neutral baseline. Replayed on open and ring overflow so dropped -// events cannot leave a guest stuck. static const NeutralEventSpec kNeutralEvents[] = { {EV_KEY, BTN_A}, {EV_KEY, BTN_B}, {EV_KEY, BTN_X}, {EV_KEY, BTN_Y}, {EV_KEY, BTN_TL}, {EV_KEY, BTN_TR}, @@ -187,30 +164,15 @@ static const NeutralEventSpec kNeutralEvents[] = { static constexpr size_t kNeutralEventCount = sizeof(kNeutralEvents) / sizeof(kNeutralEvents[0]); -// Axis layout of FakeInputRingHeader::snapshot_axes (mirrors the Java writer). static const uint16_t kSnapshotAxisCodes[8] = { ABS_X, ABS_Y, ABS_RX, ABS_RY, ABS_GAS, ABS_BRAKE, ABS_HAT0X, ABS_HAT0Y}; -// Bit i of FakeInputRingHeader::snapshot_buttons maps to this button code. static const uint16_t kSnapshotButtons[11] = { BTN_A, BTN_B, BTN_X, BTN_Y, BTN_TL, BTN_TR, BTN_SELECT, BTN_START, BTN_THUMBL, BTN_THUMBR, - // Bit 10 is the Steam button: the client's in-game menu is bound to it, and SDL reports it - // as button 8 from this position in the key bits. BTN_MODE}; static constexpr size_t kSnapshotButtonCount = sizeof(kSnapshotButtons) / sizeof(kSnapshotButtons[0]); -// An LD_PRELOAD interposer is called outside its own lifetime. The loader initialises libraries -// in dependency order, and the hooks below belong to whichever library gets there first: on the -// Linux runtime, libpython's initialiser runs before ours and calls close() and read(), so a hook -// fires before this library has been initialised at all. A file-scope std::unordered_map or -// std::recursive_mutex has no object yet at that moment - touching one is undefined behaviour, -// and in practice the process dies before main(). The same window reopens at exit, after static -// destructors have run. (Proton is a Python script, so this killed every game launched from the -// native Steam client, and the compatibility-tool registrar with it.) -// -// So everything with a constructor lives behind an accessor that builds it on first use and -// never destroys it. The leak is deliberate and bounded: one map or mutex for the process. namespace Logger { void init(); } @@ -266,9 +228,6 @@ static std::unordered_map &ff_effects() { return *map; } -// Every fd the maps hold, readable without controller_mutex(): the hooks see each read, write, -// ioctl and poll a process makes, and almost none are on these. Set before an fd is handed out, -// cleared once no map holds it; an fd past the bitmap takes the lock once any such fd exists. static constexpr int kWatchedFds = 1 << 16; static constexpr int kWatchedBits = sizeof(unsigned long) * 8; static std::atomic watched_fds[kWatchedFds / kWatchedBits]; @@ -300,17 +259,6 @@ static bool fake_uinput_enabled() { return config().uinput_enabled; } static bool ring_paths_loaded = false; static int next_ff_id = 0; -// fork() carries over only the calling thread, so a lock another thread was holding at that -// instant stays held in the child by a thread that is not there to release it. Every hook below -// takes controller_mutex, and the Steam client forks while its other threads are inside them, so -// the child blocked on its first open() and never reached exec: the spawn never completed, the -// client's main loop stalled past its own watchdog and it tore the session down. -// -// Taking the lock before the fork is what makes the copy consistent - no thread is part way -// through the tables it guards. The parent then unlocks it. The child cannot: a recursive mutex -// records the owning thread, the child's one thread has a new id, and unlocking one it does not -// own is refused, which would leave the lock held for good. It gets a fresh mutex instead, which -// is sound precisely because the fork was taken with the lock held. static void controller_fork_prepare() { controller_mutex().lock(); } static void controller_fork_parent() { controller_mutex().unlock(); } @@ -325,11 +273,6 @@ static void controller_fork_child() { namespace Logger { int log_enabled; -// FAKE_EVDEV_LOG_FILE: the session's pad.log, shared by every process the library is loaded in -// (the client, each game), so a pad report has the guest side in one file of the session folder. -// Opened with the raw syscalls (open() and close() are this library's own hooks), and only once -// there is a line to write: most processes never log one, and each open is a stop in proot on -// shared storage. static std::atomic log_fd{-1}; static std::atomic log_unusable{false}; static char log_path[PATH_MAX]; @@ -371,7 +314,6 @@ void log(const char *message, ...) { std::cerr.flush(); return; } - // One write per line: O_APPEND keeps lines from different processes whole. char line[512]; struct timespec now; clock_gettime(CLOCK_REALTIME, &now); @@ -392,12 +334,9 @@ void log(const char *message, ...) { ssize_t ignored = write(fd, line, n); (void)ignored; } -} // namespace Logger +} __attribute__((constructor)) static void library_init() { - // Warming the lazily built state, not owning it: when the loader runs this before anything - // calls a hook - the ordinary case - the first hook finds everything already there. Nothing - // below depends on it having run, which is the whole point. config(); controller_mutex(); controller_map(); @@ -408,10 +347,6 @@ send_vibration(int strong, int weak, uint16_t duration_ms, uint16_t slot) { if (!fake_vibration_enabled()) return; - // Rumble is best effort: the app listens on the abstract socket below (RumbleComponent) and - // drives the phone's vibrator. A full listener backlog must never park - // winebus (or every input hook through controller_mutex()), and a closing - // listener must not terminate the guest with SIGPIPE. int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); if (sock < 0) return; @@ -599,8 +534,6 @@ struct SnapshotState { static long long monotonic_ms(); -// The writer protects the events, cursor and snapshot in one publication. -// A busy writer is retried later; inventing a neutral state would lose holds. __attribute__((visibility("hidden"))) static bool read_snapshot(const FakeInputRingHeader *ring, SnapshotState &out) { for (int attempt = 0; attempt < 8; attempt++) { @@ -620,8 +553,6 @@ read_snapshot(const FakeInputRingHeader *ring, SnapshotState &out) { return false; } -// This baseline supersedes all deltas through its cursor. Replaying older -// deltas after it can reassert a control that the snapshot already released. __attribute__((visibility("hidden"))) static void capture_keyframe(FakeController &fake, const SnapshotState &snap) { fake.keyframe_buttons = snap.buttons; @@ -632,25 +563,22 @@ capture_keyframe(FakeController &fake, const SnapshotState &snap) { fake.needs_keyframe = false; } -// Resolve the value a keyframe event should carry from the captured snapshot. __attribute__((visibility("hidden"))) static int32_t keyframe_value(const FakeController &fake, uint16_t type, uint16_t code) { if (type == EV_KEY) { for (size_t i = 0; i < kSnapshotButtonCount; i++) if (kSnapshotButtons[i] == code) return (fake.keyframe_buttons >> i) & 1u; - return 0; // a code the snapshot word does not carry + return 0; } if (type == EV_ABS) { for (int i = 0; i < 8; i++) if (kSnapshotAxisCodes[i] == code) return fake.keyframe_axes[i]; } - return 0; // SYN / unknown + return 0; } -// A virtual pad's ring and description live beside its node, hidden from anything that lists -// event* names: /dev/input/.uinput-event. __attribute__((visibility("hidden"))) static std::string uinput_ring_path(int node) { return std::string(fake_hook_dir()) + "/.uinput-event" + std::to_string(node); } @@ -691,7 +619,6 @@ open_fake_input_ring(const char *event, int flags) { virtual_pad ? reinterpret_cast( static_cast(mapping) + FAKE_INPUT_RING_SIZE) : nullptr; - // A virtual pad whose maker has gone (a crashed client leaves its files) is not there. if (!ring_header_is_valid(ring) || (description && (description->magic != UINPUT_DESCRIPTION_MAGIC || !process_alive(description->owner_pid)))) { @@ -709,8 +636,6 @@ open_fake_input_ring(const char *event, int flags) { controller->uinput = description; controller->mapping_size = mapping_size; controller->generation = ring_generation(ring); - // Establish the cursor at open so taps arriving before the first read stay - // queued. If publication is busy, read() will finish establishing it later. SnapshotState snap; if (read_snapshot(ring, snap) && snap.generation == controller->generation) capture_keyframe(*controller, snap); @@ -725,13 +650,6 @@ open_fake_input_ring(const char *event, int flags) { return fd; } -// Under the Steam client a pad it manages is hidden from the game: the client lists it in -// SDL_GAMECONTROLLER_IGNORE_DEVICES, its overlay refuses the game's open() of the node, and the -// game is meant to see Steam Input's virtual gamepad instead. That one is a uinput device, which -// does not exist here, so the game was left with no controller at all. With -// FAKE_EVDEV_STEAM_VIRTUAL set the pad carries the virtual gamepad's identity for every process -// but the client's own, which must keep seeing the pad as the physical one it reads. -// Whether this process is the Steam client itself. Asked once: a process never becomes another. __attribute__((visibility("hidden"))) static bool is_steam_client() { static int known; if (known == 0) { @@ -757,14 +675,6 @@ __attribute__((visibility("hidden"))) static bool presents_steam_virtual() { return known == 1; } -// The virtual gamepad is an X-Box 360 pad, whose triggers are ABS_Z and ABS_RZ. Wine and SDL place -// a gamepad's axes by their position among the ones it advertises, so the triggers have to sit -// where that pad has them: as ABS_GAS and ABS_BRAKE they follow the sticks, the right stick -// lands on the left trigger, and a released right trigger reads as the right stick held up. That -// is every reader but the Steam client, which maps the pad from what it finds (controller.txt) -// and has always been shown ABS_GAS/ABS_BRAKE: a game Steam Input is off for reads this pad -// itself (the Xbox 360 setting), and so do the desktop's programs. A pad made through -// /dev/uinput is that pad for real, whoever reads it. __attribute__((visibility("hidden"))) static bool presents_xbox_triggers(const FakeController &fake) { return fake.uinput || !is_steam_client(); @@ -778,7 +688,6 @@ presented_abs_code(const FakeController &fake, uint16_t code) { return code; } -// The snapshot keeps the triggers in the ABS_GAS/ABS_BRAKE places either way. __attribute__((visibility("hidden"))) static uint16_t snapshot_abs_code(uint16_t code) { if (code == ABS_Z) return ABS_BRAKE; if (code == ABS_RZ) return ABS_GAS; @@ -807,8 +716,6 @@ __attribute__((visibility("hidden"))) static bool is_fake_input_fd(int fd) { static bool fake_pad_withdrawn(const FakeController &fake); -// A node whose pad is gone: the app republished its ring, or the pad was switched to a type that -// is not this node (it became the Deck or the Steam Controller again, fake_pad_withdrawn). static bool controller_gone(const FakeController &fake) { return ring_generation(fake.ring) != fake.generation || fake_pad_withdrawn(fake); } @@ -820,12 +727,9 @@ __attribute__((visibility("hidden"))) static bool fake_fd_is_stale(int fd) { return controller != controller_map().end() && controller_gone(*controller->second); } -// Caller holds controller_mutex(). static bool fake_has_unread_data(const FakeController &fake) { if (controller_gone(fake)) return false; - // Readiness never consumes resync requests or changes the read cursor. - // Do not spin a nonblocking guest on a producer's unfinished publication. if (fake.keyframe_remaining > 0) return true; uint64_t sequence = __atomic_load_n(&fake.ring->snapshot_seq, __ATOMIC_ACQUIRE); if (sequence & 1ULL) return false; @@ -874,20 +778,6 @@ __attribute__((visibility("hidden"))) static long long monotonic_ms() { return static_cast(now.tv_sec) * 1000LL + now.tv_nsec / 1000000LL; } -// ---- /dev/uinput, stood in for ---- -// -// Under Steam, a game is meant to read Steam Input's output, not the pad: the client hides the -// physical pad from the game (SDL_GAMECONTROLLER_IGNORE_DEVICES, and its overlay refuses the -// open) and makes it a virtual X-Box 360 pad through /dev/uinput, which it then drives with the -// layout the player chose. The sandbox has no uinput the client may use, so the virtual pad never -// appeared and every game was left reading the physical pad as-is. -// -// So /dev/uinput is answered here. A gamepad the client makes becomes another of this library's -// nodes - /dev/input/event16 and up - backed by a ring file of the same layout the app writes, -// and the client's writes are that ring's events. Any process reading the node sees the identity -// the client gave it and gets exactly what the client wrote. A device that is not a gamepad (the -// client's virtual keyboard and mouse) is accepted and its events dropped, which is what happened -// to them before. struct UinputWriter { UinputDescription description{}; bool created = false; @@ -918,8 +808,6 @@ __attribute__((visibility("hidden"))) static bool test_bit(const uint8_t *bits, return bit >= 0 && static_cast(bit) / 8 < size && (bits[bit / 8] >> (bit % 8)) & 1u; } -// A real uinput fd reads EV_UINPUT requests and polls as writable. An eventfd that is never -// signalled does both: nothing to read, always writable, blocking or not as the caller asked. __attribute__((visibility("hidden"))) static int open_uinput(int flags) { int fd = eventfd(0, ((flags & O_NONBLOCK) ? EFD_NONBLOCK : 0) | ((flags & O_CLOEXEC) ? EFD_CLOEXEC : 0)); if (fd < 0) return -1; @@ -932,8 +820,6 @@ __attribute__((visibility("hidden"))) static int open_uinput(int flags) { return fd; } -// The layout the client asks for only matters to readers; what makes it a pad is a south button -// and a left stick, which is also what every pad the ring format carries has. __attribute__((visibility("hidden"))) static bool is_gamepad(const UinputDescription &d) { return test_bit(d.evbits, sizeof(d.evbits), EV_KEY) && test_bit(d.keybits, sizeof(d.keybits), BTN_SOUTH) && test_bit(d.evbits, sizeof(d.evbits), EV_ABS) && test_bit(d.absbits, sizeof(d.absbits), ABS_X); @@ -941,8 +827,6 @@ __attribute__((visibility("hidden"))) static bool is_gamepad(const UinputDescrip __attribute__((visibility("hidden"))) static void unpublish_uinput(UinputWriter &w) { if (!w.published) return; - // Readers holding the mapping see the generation move and report the device gone; the node's - // removal is what tells SDL and Wine through inotify. __atomic_add_fetch(&w.ring->generation, 1, __ATOMIC_RELEASE); std::string node = std::string(fake_hook_dir()) + "/event" + std::to_string(w.node); unlink(node.c_str()); @@ -955,8 +839,6 @@ __attribute__((visibility("hidden"))) static void unpublish_uinput(UinputWriter w.published = false; } -// A node number is claimed by creating its ring file exclusively, so two processes making pads -// at once cannot take the same one. A file left by a process that has gone is reclaimed. __attribute__((visibility("hidden"))) static int claim_uinput_node(int *fd_out) { static auto my_open = reinterpret_cast(dlsym(RTLD_NEXT, "open")); for (int node = UINPUT_EVENT_BASE; node < UINPUT_EVENT_LIMIT; node++) { @@ -977,7 +859,7 @@ __attribute__((visibility("hidden"))) static int claim_uinput_node(int *fd_out) unlink((std::string(fake_hook_dir()) + "/event" + std::to_string(node)).c_str()); unlink((path + ".uevent").c_str()); unlink(path.c_str()); - node--; // try the same number again + node--; } } errno = ENOSPC; @@ -1010,8 +892,6 @@ __attribute__((visibility("hidden"))) static int publish_uinput(UinputWriter &w) shared->owner_pid = w.owner; __atomic_thread_fence(__ATOMIC_RELEASE); - // The identity Wine's HID bus reads through libudev (inputudev.c in libblsession), written - // before the node so the first lookup finds it. char uevent[256]; int length = snprintf(uevent, sizeof(uevent), "PRODUCT=%x/%x/%x/%x\nNAME=\"%s\"\nPHYS=\"%s\"\n", shared->id.bustype, shared->id.vendor, shared->id.product, shared->id.version, @@ -1039,8 +919,6 @@ __attribute__((visibility("hidden"))) static int publish_uinput(UinputWriter &w) return 0; } -// Same publication as the app's writer (FakeInputWriter.flushBufferToRing): the events, then the -// absolute state under its seqlock, then the cursor that makes both visible. __attribute__((visibility("hidden"))) static void flush_uinput(UinputWriter &w) { if (!w.published || w.pending.empty()) { w.pending.clear(); @@ -1081,7 +959,6 @@ __attribute__((visibility("hidden"))) static void flush_uinput(UinputWriter &w) __attribute__((visibility("hidden"))) static ssize_t write_uinput(UinputWriter &w, const void *buf, size_t count) { if (!w.created) { - // The legacy description: written once, before UI_DEV_CREATE. if (count != sizeof(struct uinput_user_dev)) { errno = EINVAL; return -1; @@ -1106,7 +983,6 @@ write_uinput(UinputWriter &w, const void *buf, size_t count) { const auto *events = static_cast(buf); for (size_t i = 0; i < count / sizeof(struct input_event); i++) { w.pending.push_back(events[i]); - // A frame ends at SYN_REPORT; a writer that never sends one still gets its events out. if ((events[i].type == EV_SYN && events[i].code == SYN_REPORT) || w.pending.size() >= 64) flush_uinput(w); } @@ -1141,7 +1017,7 @@ ioctl_uinput(UinputWriter &w, ioctl_request_t op, void *argp) { } if (w.created && number != _IOC_NR(UI_BEGIN_FF_UPLOAD) && number != _IOC_NR(UI_END_FF_UPLOAD) && number != _IOC_NR(UI_BEGIN_FF_ERASE) && number != _IOC_NR(UI_END_FF_ERASE)) { - errno = EINVAL; // the kernel refuses configuration once the device exists + errno = EINVAL; return -1; } switch (number) { @@ -1190,8 +1066,6 @@ ioctl_uinput(UinputWriter &w, ioctl_request_t op, void *argp) { return -1; } return 0; - // Force feedback reaches the client only as EV_UINPUT requests on this fd, and none are ever - // sent: a game's rumble on a virtual pad goes straight to the app (check_ff_event). case _IOC_NR(UI_BEGIN_FF_UPLOAD): case _IOC_NR(UI_END_FF_UPLOAD): case _IOC_NR(UI_BEGIN_FF_ERASE): @@ -1205,8 +1079,6 @@ ioctl_uinput(UinputWriter &w, ioctl_request_t op, void *argp) { } } -// Only the process that made the device takes it down: a forked child closing its inherited -// copy of the fd (as a spawn does before exec) must not remove the parent's pad. __attribute__((visibility("hidden"))) static void close_uinput(int fd) { std::lock_guard guard(controller_mutex()); auto it = uinput_map().find(fd); @@ -1215,21 +1087,6 @@ __attribute__((visibility("hidden"))) static void close_uinput(int fd) { uinput_map().erase(it); } -// ---- The pad as a Steam Deck controller ---- -// -// On a Deck, and on the handhelds InputPlumber serves, the Steam client reads the built-in -// controller itself as a Steam Deck controller: a vendor HID device it talks to over hidraw, which -// is what gives it the Quick Access button, the back grips, gyro and trackpads, and Steam Input's -// full treatment of the pad. With FAKE_EVDEV_DECK set the app's pad in ring slot 0 is presented to -// the client that way: /dev/hidraw16, found through the sysfs tree the app binds into the session -// (SteamDeckPad.kt), streams the Deck's 64-byte state report every 4 ms, as the real one does, built -// from the ring's absolute snapshot; its feature reports are answered as InputPlumber answers them -// for a virtual Deck. The evdev nodes of the app's pads are then withdrawn - the client would -// otherwise count the pad twice - and games get Steam Input's virtual pad (the /dev/uinput stand-in). -// -// A read returns one report and never blocks past the next one, so the fd is one end of a -// SOCK_SEQPACKET socket pair with a thread writing reports into the other end: poll, select and -// epoll then work on it with no help from the hooks here. static constexpr const char *DECK_HIDRAW_PATH = "/dev/hidraw16"; static constexpr unsigned int DECK_HIDRAW_MAJOR = 240; static constexpr unsigned int DECK_HIDRAW_MINOR = 16; @@ -1238,20 +1095,16 @@ static constexpr int DECK_REPORT_INTERVAL_US = 4000; static constexpr const char *DECK_NAME = "Valve Software Steam Deck Controller"; static constexpr const char *DECK_SERIAL = "DROIDDECK0001"; -// InputPlumber, src/drivers/steam_deck/report_descriptor.rs (CONTROLLER_DESCRIPTOR): one 64-byte -// vendor input report and one 64-byte feature report, no report ids. static const uint8_t kDeckReportDescriptor[] = { 0x06, 0xff, 0xff, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x02, 0x09, 0x03, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x06, 0x09, 0x07, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0xb1, 0x02, 0xc0}; -// Feature report ids (SDL, src/joystick/hidapi/steam/controller_constants.h). static constexpr uint8_t DECK_GET_ATTRIBUTES_VALUES = 0x83; static constexpr uint8_t DECK_GET_STRING_ATTRIBUTE = 0xAE; static constexpr uint8_t DECK_GET_CHIP_ID = 0xBA; static constexpr uint8_t DECK_TRIGGER_RUMBLE_CMD = 0xEB; -// Button bits of the state report (SDL, SDL_hidapi_steamdeck.c): the low word, then the high. static constexpr uint32_t DECK_L_R2 = 0x00000001, DECK_L_L2 = 0x00000002, DECK_L_R1 = 0x00000004, DECK_L_L1 = 0x00000008, DECK_L_Y = 0x00000010, DECK_L_B = 0x00000020, DECK_L_X = 0x00000040, DECK_L_A = 0x00000080, DECK_L_UP = 0x00000100, @@ -1264,8 +1117,6 @@ static constexpr uint32_t DECK_L_L5 = 0x00008000, DECK_L_R5 = 0x00010000, DECK_L_LEFT_TOUCH = 0x00080000, DECK_L_RIGHT_TOUCH = 0x00100000; static constexpr uint32_t DECK_H_L4 = 0x00000200, DECK_H_R4 = 0x00000400; static constexpr uint32_t DECK_H_QAM = 0x00040000; -// Bit 11 of the ring snapshot's button word: the Quick Access button, which an Xbox pad does not -// have and the evdev nodes therefore never report (FakeInputWriter.SNAPSHOT_IDX_QAM). static constexpr uint32_t SNAPSHOT_QAM_BIT = 1u << 11; struct DeckHidraw { @@ -1273,29 +1124,24 @@ struct DeckHidraw { FakeInputRingHeader *ring = nullptr; size_t mapping_size = 0; uint8_t pending_feature = 0; - bool touch = false; // Steam's touch controller (below), not the Deck - bool triton = false; // The paired 2025 Steam Controller, with native BLE report IDs - bool emulated = false; // That Steam Controller made from the app's own pad (FAKE_EVDEV_TRITON_PAD) - uint8_t sony = 0; // The app's own pad as a DualShock 4 (4) or DualSense (5), else 0 + bool touch = false; + bool triton = false; + bool emulated = false; + uint8_t sony = 0; }; -// What the Deck has and an Xbox pad does not, after the ring's events: motion (PadMotion) and the -// back grips and trackpads (the second screen's Deck controls, DeckControls), already in the -// Deck's axes and units, under a seqlock (FakeInputWriter.writeMotion / writeDeckControls). -// Absent from a ring file written without one. struct DeckImu { - uint32_t magic; // IMU1 once written - uint32_t imu_mode; // written by this library: the client's SETTING_IMU_MODE | 0x80000000 - uint64_t seq; // odd while being written + uint32_t magic; + uint32_t imu_mode; + uint64_t seq; int16_t accel[3]; int16_t gyro[3]; - int16_t pads[4]; // left X, Y, right X, Y: -32768..32767, Y up - uint16_t pressure[2]; // left, right: 0..32767 - uint32_t controls; // DECK_EXTRA_* bits - uint32_t pad_type; // PAD_TYPE_* | 0x80000000, written by the app; 0 until it does + int16_t pads[4]; + uint16_t pressure[2]; + uint32_t controls; + uint32_t pad_type; }; static_assert(offsetof(DeckImu, pad_type) == 44, "DeckImu::pad_type matches FakeInputWriter"); -// DeckImu::controls (DeckControls.kt). static constexpr uint32_t DECK_EXTRA_L4 = 1, DECK_EXTRA_R4 = 2, DECK_EXTRA_L5 = 4, DECK_EXTRA_R5 = 8, DECK_EXTRA_LPAD_TOUCH = 16, DECK_EXTRA_RPAD_TOUCH = 32, DECK_EXTRA_LPAD_CLICK = 64, @@ -1312,13 +1158,9 @@ static constexpr uint32_t DECK_IMU_MAGIC = 0x31554D49; static constexpr size_t DECK_IMU_BLOCK_SIZE = 64; static_assert(sizeof(DeckImu) <= DECK_IMU_BLOCK_SIZE, "the IMU block is 64 bytes in the ring file"); -// Shared native Steam Controller reports. Field offsets match FakeInputWriter.java. Android writes -// the original BLE input packet; this process writes Steam's HID output report for Android to send. -// A feature read is a round trip: this process bumps get_request, Android reads the controller's -// report characteristic and answers with get_reply set to the same number. struct TritonSharedData { uint32_t magic; - uint32_t connected; // written by the app: the paired controller is connected and streaming + uint32_t connected; uint64_t input_seq; uint32_t report_id; uint32_t input_length; @@ -1353,13 +1195,11 @@ static_assert(sizeof(TritonSharedData) <= TRITON_SHARED_BLOCK_SIZE, "Triton ring static constexpr const char *TRITON_HIDRAW_PATH = "/dev/hidraw18"; static constexpr unsigned int TRITON_HIDRAW_MAJOR = 240; static constexpr unsigned int TRITON_HIDRAW_MINOR = 18; -// The app's own pad as the wired Steam Controller: a node of its own, so a paired one keeps 18. static constexpr const char *TRITON_PAD_HIDRAW_PATH = "/dev/hidraw19"; static constexpr unsigned int TRITON_PAD_HIDRAW_MINOR = 19; static constexpr int TRITON_REPORT_ID_BYTES = 1; static constexpr int TRITON_STATE_BYTES = 45; static constexpr int TRITON_REPORT_INTERVAL_US = 4000; -// A BLE read takes a few connection intervals; past this the controller is taken to be gone. static constexpr int TRITON_FEATURE_TIMEOUT_MS = 500; static constexpr const char *TRITON_NAME = "Steam Controller"; static constexpr const char *TRITON_SERIAL = "DROIDDECKSC01"; @@ -1407,12 +1247,6 @@ static bool fake_deck_enabled() { return enabled == 1; } -// What the app's own pad is to the client, switchable while the session runs (the drawer's -// Controller page): the Deck (/dev/hidraw16), the wired Steam Controller (/dev/hidraw19, built from -// ring slot 0 as the Deck's reports are), or an Xbox 360 pad (its own evdev node, no HID device). -// The app writes the choice into the slot's Deck block (DeckImu::pad_type); until it has, -// FAKE_EVDEV_PAD_TYPE says what the session started with. Only the chosen device can be opened; -// the others end their streams, and the client is told through udev (pad_type_thread). static constexpr uint32_t PAD_TYPE_DECK = 0, PAD_TYPE_STEAM = 1, PAD_TYPE_XBOX = 2, PAD_TYPE_DS4 = 3, PAD_TYPE_DUALSENSE = 4, PAD_TYPE_LAST = PAD_TYPE_DUALSENSE; static constexpr uint32_t PAD_TYPE_SET = 0x80000000u; @@ -1429,7 +1263,6 @@ __attribute__((visibility("hidden"))) static uint32_t session_pad_type() { return static_cast(type); } -// Slot 0's Deck and Triton blocks, mapped once, read-only; null until the ring file has them. __attribute__((visibility("hidden"))) static const uint8_t *ring_blocks() { static std::atomic block{nullptr}; static std::atomic tries{0}; @@ -1455,8 +1288,6 @@ __attribute__((visibility("hidden"))) static const uint8_t *ring_blocks() { return mapped; } -// The paired Bluetooth controller is offered only while the app has it connected: Steam asks a -// device it is offered for its info, and one that never answers holds up every other controller. __attribute__((visibility("hidden"))) static bool triton_connected() { const uint8_t *blocks = ring_blocks(); if (!blocks) return false; @@ -1479,8 +1310,6 @@ static bool fake_triton_enabled() { return enabled == 1; } -// The Deck is the client's alone: a game that found it would have the pad twice, once through it -// and once through Steam Input's virtual pad. __attribute__((visibility("hidden"))) static bool process_is_steam_client() { static int known; if (known == 0) { @@ -1497,7 +1326,6 @@ __attribute__((visibility("hidden"))) static bool process_is_steam_client() { return known == 1; } -// What the client starts with, so a pad log says whether it was offered a Deck at all. __attribute__((constructor)) static void log_client_start() { config(); if (process_is_steam_client()) @@ -1509,8 +1337,6 @@ __attribute__((visibility("hidden"))) static bool is_deck_hidraw_path(const char return pathname && fake_deck_enabled() && !strcmp(pathname, DECK_HIDRAW_PATH) && live_pad_type() == PAD_TYPE_DECK; } -// The app's own pad as a PlayStation controller: /dev/hidraw20 (DualShock 4) or /dev/hidraw21 -// (DualSense), USB devices of their own in SteamDeckPad's sysfs. Returns 4 or 5, or 0. static constexpr const char *DS4_HIDRAW_PATH = "/dev/hidraw20"; static constexpr const char *DUALSENSE_HIDRAW_PATH = "/dev/hidraw21"; static constexpr unsigned int DS4_HIDRAW_MINOR = 20, DUALSENSE_HIDRAW_MINOR = 21; @@ -1531,11 +1357,8 @@ __attribute__((visibility("hidden"))) static bool is_triton_hidraw_path(const ch return pathname && fake_triton_enabled() && !strcmp(pathname, TRITON_HIDRAW_PATH) && triton_connected(); } -// The app's first pad is the Deck now: its evdev node is withdrawn from everyone. Further players' -// pads are left to the client alone, which hands them to games through Steam Input. __attribute__((visibility("hidden"))) static bool is_withdrawn_pad_path(const char *pathname) { if (!fake_deck_enabled() || !pathname || strncmp(pathname, "/dev/input/", 11)) return false; - // As an Xbox 360 pad it is its own evdev node again, for the client and games alike. if (live_pad_type() == PAD_TYPE_XBOX) return false; const char *event = strrchr(pathname, '/') + 1; if (strncmp(event, "event", 5) && strncmp(event, "js", 2)) return false; @@ -1560,15 +1383,12 @@ static inline void put32(uint8_t *at, uint32_t value) { for (int i = 0; i < 4; i++) at[i] = static_cast(value >> (8 * i)); } -// The Deck's state report (SDL, controller_structs.h: ValveInReportHeader_t then -// SteamDeckStatePacket_t) from the ring's snapshot: kSnapshotButtons order, kSnapshotAxisCodes -// order, sticks as evdev has them (Y down), triggers 0..255. __attribute__((visibility("hidden"))) static void build_deck_report(uint8_t *report, uint32_t packet, uint32_t buttons, const int32_t *axes, const DeckExtras &extras) { memset(report, 0, DECK_REPORT_BYTES); - report[0] = 0x01; // report version - report[2] = 0x09; // ID_CONTROLLER_DECK_STATE + report[0] = 0x01; + report[2] = 0x09; report[3] = DECK_REPORT_BYTES; put32(report + 4, packet); static const uint32_t kButtonBits[11] = {DECK_L_A, DECK_L_B, DECK_L_X, DECK_L_Y, @@ -1583,7 +1403,7 @@ build_deck_report(uint8_t *report, uint32_t packet, uint32_t buttons, const int3 if (hat_y > 0) low |= DECK_L_DOWN; if (hat_x < 0) low |= DECK_L_LEFT; if (hat_x > 0) low |= DECK_L_RIGHT; - int32_t left_trigger = axes[5], right_trigger = axes[4]; // ABS_BRAKE, ABS_GAS + int32_t left_trigger = axes[5], right_trigger = axes[4]; if (left_trigger > 127) low |= DECK_L_L2; if (right_trigger > 127) low |= DECK_L_R2; uint32_t controls = extras.controls; @@ -1597,13 +1417,13 @@ build_deck_report(uint8_t *report, uint32_t packet, uint32_t buttons, const int3 if (controls & DECK_EXTRA_RPAD_CLICK) low |= DECK_L_RIGHT_PAD; put32(report + 8, low); put32(report + 12, high); - for (int i = 0; i < 4; i++) put16(report + 16 + i * 2, extras.pads[i]); // s{Left,Right}PadX/Y + for (int i = 0; i < 4; i++) put16(report + 16 + i * 2, extras.pads[i]); for (int i = 0; i < 3; i++) { - put16(report + 24 + i * 2, extras.accel[i]); // sAccelX/Y/Z - put16(report + 30 + i * 2, extras.gyro[i]); // sGyroX/Y/Z + put16(report + 24 + i * 2, extras.accel[i]); + put16(report + 30 + i * 2, extras.gyro[i]); } - put16(report + 56, extras.pressure[0]); // sPressurePadLeft - put16(report + 58, extras.pressure[1]); // sPressurePadRight + put16(report + 56, extras.pressure[0]); + put16(report + 58, extras.pressure[1]); put16(report + 44, std::min(32767, left_trigger * 32767 / 255)); put16(report + 46, std::min(32767, right_trigger * 32767 / 255)); auto stick = [](int32_t value) { return std::max(-32767, std::min(32767, value)); }; @@ -1622,7 +1442,6 @@ __attribute__((visibility("hidden"))) static void *deck_report_thread(void *arg) uint32_t buttons = 0; int32_t axes[8] = {}; DeckExtras extras; - // Switched to another type, the stream ends: the client's read fails and it lets the Deck go. while (live_pad_type() == PAD_TYPE_DECK) { if (read_snapshot(self->ring, snap) && snap.generation == ring_generation(self->ring)) { buttons = snap.buttons; @@ -1631,8 +1450,6 @@ __attribute__((visibility("hidden"))) static void *deck_report_thread(void *arg) read_deck_extras(*self, extras); uint8_t report[DECK_REPORT_BYTES]; build_deck_report(report, ++packet, buttons, axes, extras); - // A reader that fell behind loses reports rather than getting stale ones later; a reader that - // has closed ends the stream. if (send(self->peer, report, sizeof(report), MSG_DONTWAIT | MSG_NOSIGNAL) < 0 && errno != EAGAIN && errno != EWOULDBLOCK) break; @@ -1644,15 +1461,6 @@ __attribute__((visibility("hidden"))) static void *deck_report_thread(void *arg) return nullptr; } -// ---- The pad as a PlayStation controller ---- -// -// The DualShock 4 (054c:09cc) and DualSense (054c:0ce6) as they are on USB, read by the client's -// own PlayStation drivers (SDL's hidapi ps4/ps5 are the reference): input report 1 every 4 ms, -// the feature reports a driver reads at start answered here (calibration, serial, firmware), and -// output reports' rumble on the handheld's motor. Built from the same ring as the Deck's report: -// sticks, triggers, buttons, motion (in the units the calibration below declares), and the -// touchpad from the Deck controls' two trackpads, the left one on its left half and the right one -// on its right half, either's click as the touchpad's. static constexpr int SONY_REPORT_BYTES = 64; static constexpr int SONY_TOUCH_WIDTH = 1920; static constexpr int SONY_TOUCH_HEIGHT_DS4 = 942, SONY_TOUCH_HEIGHT_DUALSENSE = 1080; @@ -1664,8 +1472,6 @@ struct SonyTouch { uint8_t next_id = 0; }; -// One finger per trackpad: Deck pads are -32768..32767 with Y up; the touchpad counts from its top -// left, 12 bits per axis. __attribute__((visibility("hidden"))) static void put_sony_touch(uint8_t *at, SonyTouch &touch, int side, bool down, int16_t x, int16_t y, int height) { if (down && !touch.down[side]) touch.id[side] = touch.next_id++ & 0x7f; @@ -1687,13 +1493,12 @@ build_sony_report(uint8_t *report, uint8_t model, uint32_t packet, uint32_t butt uint8_t *p = report + 1; auto stick = [](int32_t value) { return static_cast(std::max(0, std::min(255, (value + 32768) >> 8))); }; p[0] = stick(axes[0]); - p[1] = stick(axes[1]); // evdev and the PlayStation pads both count Y downwards + p[1] = stick(axes[1]); p[2] = stick(axes[2]); p[3] = stick(axes[3]); int32_t hat_x = axes[6], hat_y = axes[7]; - static const uint8_t kHat[3][3] = {{7, 0, 1}, {6, 8, 2}, {5, 4, 3}}; // [y+1][x+1] + static const uint8_t kHat[3][3] = {{7, 0, 1}, {6, 8, 2}, {5, 4, 3}}; uint8_t hat = kHat[(hat_y > 0) - (hat_y < 0) + 1][(hat_x > 0) - (hat_x < 0) + 1]; - // Snapshot order (kButtonBits): A B X Y L1 R1 View Menu L3 R3 Steam. uint8_t face = (buttons & 4 ? 0x10 : 0) | (buttons & 1 ? 0x20 : 0) | (buttons & 2 ? 0x40 : 0) | (buttons & 8 ? 0x80 : 0); uint8_t shoulders = (buttons & 16 ? 0x01 : 0) | (buttons & 32 ? 0x02 : 0) | (axes[5] > 0 ? 0x04 : 0) | (axes[4] > 0 ? 0x08 : 0) | (buttons & 64 ? 0x10 : 0) | (buttons & 128 ? 0x20 : 0) | @@ -1702,9 +1507,6 @@ build_sony_report(uint8_t *report, uint8_t model, uint32_t packet, uint32_t butt uint8_t system = (buttons & 1024 ? 0x01 : 0) | (click ? 0x02 : 0); uint8_t left_trigger = static_cast(std::max(0, std::min(255, axes[5]))); uint8_t right_trigger = static_cast(std::max(0, std::min(255, axes[4]))); - // Motion: the Deck block's gyro is 2000 dps and its accel 2 g over 16 bits; the calibration - // reports declare 16 units per degree/s and 8192 per g, and the axes are the Deck's reordered as - // SDL reads both (Deck X, Z, -Y). int16_t gyro[3] = {extras.gyro[0], extras.gyro[2], static_cast(-extras.gyro[1])}; int16_t accel[3] = {static_cast(extras.accel[0] / 2), static_cast(extras.accel[2] / 2), static_cast(-extras.accel[1] / 2)}; @@ -1715,13 +1517,13 @@ build_sony_report(uint8_t *report, uint8_t model, uint32_t packet, uint32_t butt p[6] = static_cast(system | ((packet & 0x3f) << 2)); p[7] = left_trigger; p[8] = right_trigger; - put16(p + 9, static_cast(packet * 750)); // 5.33 us ticks: 4 ms + put16(p + 9, static_cast(packet * 750)); for (int i = 0; i < 3; i++) { put16(p + 12 + i * 2, gyro[i]); put16(p + 18 + i * 2, accel[i]); } - p[29] = 0x1b; // on the cable, full - p[32] = 1; // one touch packet + p[29] = 0x1b; + p[32] = 1; p[33] = static_cast(packet); put_sony_touch(p + 34, touch, 0, left, extras.pads[0], extras.pads[1], SONY_TOUCH_HEIGHT_DS4); put_sony_touch(p + 38, touch, 1, right, extras.pads[2], extras.pads[3], SONY_TOUCH_HEIGHT_DS4); @@ -1737,11 +1539,11 @@ build_sony_report(uint8_t *report, uint8_t model, uint32_t packet, uint32_t butt put16(p + 15 + i * 2, gyro[i]); put16(p + 21 + i * 2, accel[i]); } - put32(p + 27, packet * 12000); // 0.33 us ticks: 4 ms + put32(p + 27, packet * 12000); put_sony_touch(p + 32, touch, 0, left, extras.pads[0], extras.pads[1], SONY_TOUCH_HEIGHT_DUALSENSE); put_sony_touch(p + 36, touch, 1, right, extras.pads[2], extras.pads[3], SONY_TOUCH_HEIGHT_DUALSENSE); - p[52] = 0x2a; // charging, full - p[53] = 0x08; // USB + p[52] = 0x2a; + p[53] = 0x08; } } @@ -1756,7 +1558,6 @@ __attribute__((visibility("hidden"))) static void *sony_report_thread(void *arg) int32_t axes[8] = {}; DeckExtras extras; SonyTouch touch; - // Switched to another type, the stream ends, as unplugging the controller would. while (live_pad_type() == type) { if (read_snapshot(self->ring, snap) && snap.generation == ring_generation(self->ring)) { buttons = snap.buttons; @@ -1780,7 +1581,6 @@ __attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags, uin const char *path = sony == 4 ? DS4_HIDRAW_PATH : sony == 5 ? DUALSENSE_HIDRAW_PATH : DECK_HIDRAW_PATH; static auto my_open = reinterpret_cast(dlsym(RTLD_NEXT, "open")); if (!process_is_steam_client()) { - // Only the client reads the Deck; a game sees Steam Input's virtual pad instead. Logger::log("deck: %s refused: not the Steam client\n", path); errno = ENOENT; return -1; @@ -1793,7 +1593,6 @@ __attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags, uin errno = ENODEV; return -1; } - // The IMU block is mapped only where the file has one: past its end a mapping faults. struct stat ring_stat; size_t mapping_size = FAKE_INPUT_RING_SIZE; if (fstat(ring_fd, &ring_stat) == 0 && @@ -1814,7 +1613,6 @@ __attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags, uin munmap(mapping, mapping_size); return -1; } - // Room for a few reports only, so what the client reads is never more than a few ms old. int buffer = DECK_REPORT_BYTES * 4; setsockopt(pair[1], SOL_SOCKET, SO_SNDBUF, &buffer, sizeof(buffer)); if (!(flags & O_CLOEXEC)) fcntl(pair[0], F_SETFD, 0); @@ -1879,7 +1677,6 @@ __attribute__((visibility("hidden"))) static void *triton_report_thread(void *ar uint8_t report[TRITON_REPORT_ID_BYTES + 64] = {}; uint8_t report_id = 0x45; size_t length = TRITON_STATE_BYTES; - // A disconnect ends the stream, as unplugging the controller would. while (triton_connected()) { if (read_triton_state(*self, report_id, report + 1, length)) { report[0] = report_id; @@ -1895,8 +1692,6 @@ __attribute__((visibility("hidden"))) static void *triton_report_thread(void *ar return nullptr; } -// Button bits of the Triton state report (SDL, SDL_hidapi_steam_triton.c). SDL reads MENU as Back -// and VIEW as Start, the reverse of the Deck's names. static constexpr uint32_t TRITON_A = 0x00000001, TRITON_B = 0x00000002, TRITON_X = 0x00000004, TRITON_Y = 0x00000008, TRITON_QAM = 0x00000010, TRITON_R3 = 0x00000020, TRITON_VIEW = 0x00000040, TRITON_R4 = 0x00000080, TRITON_R5 = 0x00000100, @@ -1909,17 +1704,10 @@ static constexpr uint32_t TRITON_A = 0x00000001, TRITON_B = 0x00000002, TRITON_X TRITON_LSTICK_TOUCH = 0x01000000, TRITON_LPAD_TOUCH = 0x02000000, TRITON_LPAD_CLICK = 0x04000000, TRITON_LTRIGGER_CLICK = 0x08000000, TRITON_RGRIP_TOUCH = 0x10000000, TRITON_LGRIP_TOUCH = 0x20000000; -// The pad is built in, so it is the wired controller: 28de:1302 on USB, state report 0x42 (the -// same layout as the Bluetooth 0x45), where the paired one is 28de:1303 on Bluetooth. static constexpr uint8_t TRITON_STATE_WIRED = 0x42; static constexpr int16_t TRITON_WIRED_PRODUCT = 0x1302; -// A stick counts as touched once it leaves its centre: the pad has no capacitive caps to ask. static constexpr int32_t TRITON_STICK_TOUCH = 2000; -// The Triton BLE state report (SDL, controller_structs.h: TritonMTUNoQuat_t) from the ring's -// snapshot and the Deck block, which already hold the trackpads, back buttons and motion in the -// units both controllers share. A handheld is held whenever it is played, so both grips read as -// touched: Steam's grip-sense activators (gyro while held, say) then simply stay on. __attribute__((visibility("hidden"))) static void build_triton_report(uint8_t *report, uint32_t packet, uint32_t buttons, const int32_t *axes, const DeckExtras &extras) { @@ -1936,7 +1724,7 @@ build_triton_report(uint8_t *report, uint32_t packet, uint32_t buttons, const in if (axes[7] > 0) bits |= TRITON_DOWN; if (axes[6] < 0) bits |= TRITON_LEFT; if (axes[6] > 0) bits |= TRITON_RIGHT; - int32_t left_trigger = axes[5], right_trigger = axes[4]; // ABS_BRAKE, ABS_GAS: 0..255 + int32_t left_trigger = axes[5], right_trigger = axes[4]; if (left_trigger >= 250) bits |= TRITON_LTRIGGER_CLICK; if (right_trigger >= 250) bits |= TRITON_RTRIGGER_CLICK; if (abs(axes[0]) > TRITON_STICK_TOUCH || abs(axes[1]) > TRITON_STICK_TOUCH) bits |= TRITON_LSTICK_TOUCH; @@ -1964,7 +1752,7 @@ build_triton_report(uint8_t *report, uint32_t packet, uint32_t buttons, const in put16(report + 23, extras.pads[2]); put16(report + 25, extras.pads[3]); put16(report + 27, extras.pressure[1]); - put32(report + 29, packet * TRITON_REPORT_INTERVAL_US); // IMU timestamp, microseconds + put32(report + 29, packet * TRITON_REPORT_INTERVAL_US); for (int i = 0; i < 3; i++) { put16(report + 33 + i * 2, extras.accel[i]); put16(report + 39 + i * 2, extras.gyro[i]); @@ -2069,24 +1857,6 @@ __attribute__((visibility("hidden"))) static int open_triton_hidraw(int flags, b } -// ---- Steam's touch controller ---- -// -// docs/development/steam-touch-controller.md. Steam builds its touch controller (Steam Input's -// Mobile Touch type, the one Steam Link's on-screen controls are) for any HID device with the ids -// 0000:11fb. With FAKE_TOUCHCTL_RING set, the client is offered one as /dev/hidraw17 beside the -// Deck, its sysfs written by the app (SteamDeckPad.kt). The app draws the controls; this side only -// carries the device's traffic through the file FAKE_TOUCHCTL_RING names, which both map: -// -// - the app writes the 40-byte input report (under a seqlock), whether the device is plugged in, -// and the battery level; -// - this side writes what the client tells the device: the app and action set it is on (output -// report 4), the action set layers added and removed (5, 6), rumble (1, also passed to the app's -// vibration like any pad's) and the setting report (3), and how many readers have it open. -// -// Plugged in and out by the app (while its controls are up and no physical pad is in use), as -// Steam Link withdraws its touch controls: an unplugged device ends its open stream, refuses new -// opens, and a udev event (libblsession's stand-in monitor, udevmon.c) tells the client's HID -// discovery either way. Only the client is offered it; a game reads Steam Input's virtual pad. static constexpr const char *TOUCH_HIDRAW_PATH = "/dev/hidraw17"; static constexpr unsigned int TOUCH_HIDRAW_MINOR = 17; static constexpr int TOUCH_REPORT_BYTES = 40; @@ -2094,25 +1864,21 @@ static constexpr int TOUCH_REPORT_INTERVAL_US = 8000; static constexpr const char *TOUCH_NAME = "Mobile Touch Control"; static constexpr const char *TOUCH_SERIAL = "DROIDDECK0001"; static constexpr const char *TOUCH_DEVPATH = "/devices/droiddeck/usb2/2-1:1.0/0003:0000:11FB.0002/hidraw/hidraw17"; -// Generic Desktop / Game Pad: for a vendor id that is not Valve's the client opens only a device -// with a gamepad usage. One 40-byte input report, one 64-byte output and one 64-byte feature -// report, no report ids. Must match SteamDeckPad.kt. static const uint8_t kTouchReportDescriptor[] = { 0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x09, 0x02, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x28, 0x81, 0x02, 0x09, 0x03, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0x91, 0x02, 0x09, 0x04, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0xb1, 0x02, 0xc0}; -// The shared file. Must match SteamTouchDevice.kt. struct TouchRingFile { - uint32_t magic; // TCH1 + uint32_t magic; uint32_t version; - uint64_t report_seq; // odd while the app writes report + uint64_t report_seq; uint8_t report[TOUCH_REPORT_BYTES]; uint32_t plugged; - uint32_t battery; // percent - // Written here. - uint64_t state_seq; // odd while being written + uint32_t battery; + + uint64_t state_seq; uint32_t opened; uint32_t appid; uint32_t action_set; @@ -2168,7 +1934,6 @@ __attribute__((visibility("hidden"))) static bool is_touch_hidraw_path(const cha return pathname && !strcmp(pathname, TOUCH_HIDRAW_PATH) && process_is_steam_client() && touch_ring(); } -// The state the client gave the device, written under the ring's seqlock. template __attribute__((visibility("hidden"))) static void touch_state(F &&write) { TouchRingFile *ring = touch_ring(); @@ -2185,8 +1950,6 @@ static inline uint32_t get32(const uint8_t *at) { static_cast(at[3]) << 24; } -// An output report from the client (Steam Link's CVirtualController::HandleFeatureReports): the -// type, then its fields. __attribute__((visibility("hidden"))) static void touch_output(const uint8_t *data, size_t size) { if (size < 9) return; static std::atomic seen[8]; @@ -2195,7 +1958,7 @@ __attribute__((visibility("hidden"))) static void touch_output(const uint8_t *da if (n && (n & (n - 1)) == 0) Logger::log("touch: output report %u (x%u): app %u, %u\n", type, n, get32(data + 1), get32(data + 5)); switch (type) { - case 1: { // rumble: low, high, duration + case 1: { uint16_t low = data[1] | data[2] << 8, high = data[3] | data[4] << 8; uint32_t ms = get32(data + 5); touch_state([&](TouchRingFile &r) { @@ -2207,10 +1970,10 @@ __attribute__((visibility("hidden"))) static void touch_output(const uint8_t *da send_vibration(low, high, static_cast(std::min(ms, 0xffff)), 0); break; } - case 3: // a setting: 0x30 and a value + case 3: if (data[1] == 0x30) touch_state([&](TouchRingFile &r) { r.setting30 = data[2] | data[3] << 8; }); break; - case 4: // the app and its action set + case 4: touch_state([&](TouchRingFile &r) { r.appid = get32(data + 1); r.action_set = get32(data + 5); @@ -2218,8 +1981,8 @@ __attribute__((visibility("hidden"))) static void touch_output(const uint8_t *da r.action_seq++; }); break; - case 5: // a layer added to the app's action set - case 6: // and removed + case 5: + case 6: touch_state([&](TouchRingFile &r) { uint32_t layer = get32(data + 5); uint32_t count = std::min(r.layer_count, 8); @@ -2259,7 +2022,6 @@ __attribute__((visibility("hidden"))) static void *touch_report_thread(void *arg struct timespec interval = {0, TOUCH_REPORT_INTERVAL_US * 1000L}; nanosleep(&interval, nullptr); } - // Ending the stream is the unplug: the client's read fails and it lets the controller go. syscall(SYS_close, self->peer); touch_state([](TouchRingFile &r) { if (r.opened) r.opened--; }); return nullptr; @@ -2312,17 +2074,17 @@ __attribute__((visibility("hidden"))) static int ioctl_touch(DeckHidraw &touch, } size_t size = _IOC_SIZE(op); switch (_IOC_NR(op)) { - case 0x01: // HIDIOCGRDESCSIZE + case 0x01: *static_cast(argp) = sizeof(kTouchReportDescriptor); return 0; - case 0x02: { // HIDIOCGRDESC + case 0x02: { auto *descriptor = static_cast(argp); uint32_t want; memcpy(&want, descriptor, sizeof(want)); memcpy(descriptor + 4, kTouchReportDescriptor, std::min(want, sizeof(kTouchReportDescriptor))); return 0; } - case 0x03: { // HIDIOCGRAWINFO + case 0x03: { struct { uint32_t bustype; int16_t vendor; @@ -2331,13 +2093,13 @@ __attribute__((visibility("hidden"))) static int ioctl_touch(DeckHidraw &touch, memcpy(argp, &info, sizeof(info)); return 0; } - case 0x04: return copy_ioctl_string(op, argp, TOUCH_NAME); // HIDIOCGRAWNAME - case 0x05: return copy_ioctl_string(op, argp, "usb-droiddeck-2/input0"); // HIDIOCGRAWPHYS - case 0x08: return copy_ioctl_string(op, argp, TOUCH_SERIAL); // HIDIOCGRAWUNIQ - case 0x06: // HIDIOCSFEATURE: Steam Link's controller ignores these + case 0x04: return copy_ioctl_string(op, argp, TOUCH_NAME); + case 0x05: return copy_ioctl_string(op, argp, "usb-droiddeck-2/input0"); + case 0x08: return copy_ioctl_string(op, argp, TOUCH_SERIAL); + case 0x06: touch.pending_feature = size > 0 ? static_cast(argp)[0] : 0; return static_cast(size); - case 0x07: { // HIDIOCGFEATURE: report 2 is the battery + case 0x07: { auto *buf = static_cast(argp); uint8_t id = size ? buf[0] : 0; memset(buf, 0, size); @@ -2355,8 +2117,6 @@ __attribute__((visibility("hidden"))) static int ioctl_touch(DeckHidraw &touch, } } -// Plugging in and out: the app flips `plugged`; this tells the client's HID discovery, which -// otherwise never looks again (the sandbox gives it no kernel uevents). __attribute__((visibility("hidden"))) static int inject_uevent(const char *action, const char *devpath, const char *subsystem, const char *devname, unsigned major, unsigned minor) { @@ -2370,7 +2130,6 @@ __attribute__((visibility("hidden"))) static void touch_uevent(const char *actio Logger::log("touch: %s (udev event to %d monitor%s)\n", action, sent, sent == 1 ? "" : "s"); } -// The devices each pad type is, as SteamDeckPad.kt describes them and inputudev.c the evdev node. static constexpr const char *DECK_DEVPATH = "/devices/droiddeck/usb1/1-1:1.2/0003:28DE:1205.0001/hidraw/hidraw16"; static constexpr const char *TRITON_PAD_DEVPATH = "/devices/droiddeck/usb3/3-1:1.0/0003:28DE:1302.0004/hidraw/hidraw19"; static constexpr const char *PAD_EVDEV_DEVPATH = "/devices/virtual/input/droiddeck0/event0"; @@ -2393,10 +2152,6 @@ __attribute__((visibility("hidden"))) static int pad_type_uevent(uint32_t type, return inject_uevent(action, PAD_EVDEV_DEVPATH, "input", "/dev/input/event0", major(rdev), minor(rdev)); } -// SDL's HIDAPI in the client (the PlayStation pads' driver) finds hidraw devices by watching /dev -// with inotify, with udev off, and looks again on any entry named hidraw. The guest cannot -// create one in /dev, so the client's watch of /dev also covers this directory (inotify_add_watch -// below), where a switch makes and removes one. __attribute__((visibility("hidden"))) static std::string hidraw_event_dir() { return std::string(fake_hook_dir()) + "/.hidraw-events"; } @@ -2414,7 +2169,6 @@ __attribute__((visibility("hidden"))) static unsigned pad_type_minor(uint32_t ty : type == PAD_TYPE_STEAM ? TRITON_PAD_HIDRAW_MINOR : DECK_HIDRAW_MINOR; } -// A switch, as the client sees one: the old device unplugged, then the new one plugged in. __attribute__((visibility("hidden"))) static void *pad_type_thread(void *) { static const char *kNames[] = {"Steam Deck controller", "Steam Controller", "Xbox 360 controller", "DualShock 4", "DualSense"}; @@ -2435,15 +2189,11 @@ __attribute__((visibility("hidden"))) static void *pad_type_thread(void *) { uint32_t now = live_pad_type(); if (now == was || now > PAD_TYPE_LAST) continue; int removed = pad_type_uevent(was, "remove"); - // Time for the client to let the old device go before the new one appears. struct timespec settle = {0, 300 * 1000 * 1000}; nanosleep(&settle, nullptr); int added = pad_type_uevent(now, "add"); if (was != PAD_TYPE_XBOX) poke_hidraw_watchers(pad_type_minor(was)); if (now != PAD_TYPE_XBOX) poke_hidraw_watchers(pad_type_minor(now)); - // The pad's evdev node comes or goes with the Xbox 360 type. Its hotplug is watched with - // inotify on /dev/input (SDL's, with udev off), so it is moved out and back in: everyone - // watching sees it removed and added, and opens it again only if it is the pad now. if (was == PAD_TYPE_XBOX || now == PAD_TYPE_XBOX) { std::string node = std::string(fake_hook_dir()) + "/event0"; std::string aside = std::string(fake_hook_dir()) + "/.event0-switching"; @@ -2489,13 +2239,8 @@ __attribute__((constructor)) static void start_touch_controller() { if (pthread_create(&thread, nullptr, touch_plug_thread, nullptr) == 0) pthread_detach(thread); } -// libudev (systemd's sd-device) accepts a device only where its directory is on sysfs, which it -// checks with fstatfs on the directory it resolved. The Deck's sysfs is the app's own files bound -// into /sys, so those paths are said to be sysfs; nothing else is touched. static constexpr long SYSFS_FS_MAGIC = 0x62656572; -// FAKE_DECK_SYSFS_LISTING: stand-ins for the listings of /sys, /sys/class and /sys/bus -// (SteamDeckPad.listingDir), for a client that is not allowed to list the real ones. __attribute__((visibility("hidden"))) static const char *deck_listing_dir() { static const char *dir = getenv("FAKE_DECK_SYSFS_LISTING"); return dir && *dir ? dir : nullptr; @@ -2517,9 +2262,6 @@ __attribute__((visibility("hidden"))) static bool is_deck_sysfs_fd(int fd) { return is_deck_sysfs_path(path); } -// Once per process: proof the client's enumeration reached the Deck's sysfs at all. No line here -// and no open of /dev/hidraw16 means the client never looked; a line here and no open means it -// looked and turned the device down. __attribute__((visibility("hidden"))) static void note_deck_sysfs(const char *how, int result) { static std::atomic noted{false}; if (!noted.exchange(true)) @@ -2563,20 +2305,12 @@ __attribute__((visibility("hidden"))) static int copy_ioctl_string(ioctl_request return static_cast(std::min(size, strlen(value) + 1)); } -// Feature reports carry the report id (0) first, then the Valve message: type, length, payload. -// The client repeats some feature reports (settings, rumble) for as long as it has the pad; each -// is logged the first time and then at 2, 4, 8... so the log shows what it asked and how often -// without a line per report. __attribute__((visibility("hidden"))) static void log_deck_feature(const char *what, uint8_t feature) { static std::atomic counts[2][256]; unsigned n = ++counts[what[0] == 's'][feature]; if ((n & (n - 1)) == 0) Logger::log("deck: feature 0x%02x %s (x%u)\n", feature, what, n); } -// A settings report (ID_SET_SETTINGS_VALUES) is a list of (setting id, 16-bit value) triples after -// the length byte. Returns SETTING_IMU_MODE (48, a SETTING_GYRO_MODE_* bitmask from SDL's -// controller_constants.h) when the report changes it, logged, else -1: the client repeats its -// settings for as long as it has the pad. static constexpr uint8_t DECK_SET_SETTINGS_VALUES = 0x87; static constexpr uint8_t DECK_SETTING_IMU_MODE = 48; @@ -2593,9 +2327,6 @@ __attribute__((visibility("hidden"))) static int deck_imu_mode_change(const uint return -1; } -// Tells the app whether the client wants motion (DeckImu::imu_mode, FakeInputWriter.readImuMode): -// PadMotion reads the phone's sensors only while it does. The mapping is read-only, and the -// client changes the mode a few times a minute at most, so the file is written directly. __attribute__((visibility("hidden"))) static void publish_imu_mode(const DeckHidraw &deck, int mode) { static auto my_open = reinterpret_cast(dlsym(RTLD_NEXT, "open")); if (deck.mapping_size < FAKE_INPUT_RING_SIZE + DECK_IMU_BLOCK_SIZE) return; @@ -2631,7 +2362,6 @@ deck_feature(DeckHidraw &deck, ioctl_request_t op, uint8_t *buf, bool set) { uint8_t reply[DECK_REPORT_BYTES + 1] = {}; switch (deck.pending_feature) { case DECK_GET_ATTRIBUTES_VALUES: { - // InputPlumber's capture from a real Deck (steam_deck_uhid.rs, handle_get_report). static const uint8_t kAttributes[] = {0x00, 0x83, 0x2d, 0x01, 0x05, 0x12, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x2b, 0x12, 0xa9, 0x62, 0x04, 0xad, 0xf1, 0xe4, 0x65, 0x09, 0x2e, 0x00, 0x00, @@ -2661,8 +2391,6 @@ deck_feature(DeckHidraw &deck, ioctl_request_t op, uint8_t *buf, bool set) { return static_cast(length); } -// The PlayStation pads' report descriptor: a gamepad with input report 1, the output report the -// model takes (5 on a DualShock 4, 2 on a DualSense) and the feature reports a driver reads. static const uint8_t kDs4ReportDescriptor[] = { 0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x06, 0x00, 0xff, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x85, 0x01, 0x09, 0x20, 0x95, 0x3f, 0x81, 0x02, 0x85, 0x05, 0x09, 0x21, 0x95, 0x1f, 0x91, 0x02, @@ -2675,8 +2403,6 @@ static const uint8_t kDualSenseReportDescriptor[] = { 0x85, 0x05, 0x09, 0x22, 0x95, 0x28, 0xb1, 0x02, 0x85, 0x09, 0x09, 0x23, 0x95, 0x13, 0xb1, 0x02, 0x85, 0x20, 0x09, 0x24, 0x95, 0x3f, 0xb1, 0x02, 0xc0}; -// Calibration both models report the same way (SDL: LoadCalibrationData): no bias, and the -// ranges that make a unit 1/16 degree/s and 1/8192 g, the scale build_sony_report writes in. __attribute__((visibility("hidden"))) static void put_sony_calibration(uint8_t *reply) { for (int axis = 0; axis < 3; axis++) { put16(reply + 7 + axis * 4, 8640); @@ -2700,9 +2426,9 @@ __attribute__((visibility("hidden"))) static int sony_feature(const DeckHidraw & if ((pad.sony == 4 && id == 0x02) || (pad.sony == 5 && id == 0x05)) { put_sony_calibration(reply); } else if ((pad.sony == 4 && (id == 0x12 || id == 0x81)) || (pad.sony == 5 && id == 0x09)) { - for (int i = 0; i < 6; i++) reply[1 + i] = SONY_MAC[5 - i]; // the address, last byte first + for (int i = 0; i < 6; i++) reply[1 + i] = SONY_MAC[5 - i]; } else if (pad.sony == 5 && id == 0x20) { - reply[44] = 0x30; // firmware 0x0430: past the versions SDL treats as old + reply[44] = 0x30; reply[45] = 0x04; } log_deck_feature("read", id); @@ -2711,8 +2437,6 @@ __attribute__((visibility("hidden"))) static int sony_feature(const DeckHidraw & return static_cast(length); } -// Rumble from the output report, when its flags say it carries rumble: a DualShock 4's report 5 -// (right/weak, then left/strong at 4 and 5), a DualSense's report 2 (at 3 and 4). __attribute__((visibility("hidden"))) static void sony_output(const DeckHidraw &pad, const uint8_t *data, size_t size) { if (!data || size < 6) return; int weak, strong; @@ -2727,7 +2451,6 @@ __attribute__((visibility("hidden"))) static void sony_output(const DeckHidraw & } else { return; } - // The drivers send a new report when the rumble changes and a zero one to stop it. send_vibration(strong * 257, weak * 257, strong || weak ? 2000 : 0, 0); } @@ -2763,7 +2486,7 @@ __attribute__((visibility("hidden"))) static int ioctl_sony(DeckHidraw &pad, ioc : "Sony Interactive Entertainment DualSense Wireless Controller"); case 0x05: return copy_ioctl_string(op, argp, pad.sony == 4 ? "usb-droiddeck-4/input3" : "usb-droiddeck-5/input3"); case 0x08: return copy_ioctl_string(op, argp, "5e:43:4b:44:44:02"); - case 0x06: // HIDIOCSFEATURE: nothing the pads need is set this way + case 0x06: return static_cast(_IOC_SIZE(op)); case 0x07: return sony_feature(pad, static_cast(argp), _IOC_SIZE(op)); default: @@ -2779,17 +2502,17 @@ ioctl_deck(DeckHidraw &deck, ioctl_request_t op, void *argp) { return -1; } switch (_IOC_NR(op)) { - case 0x01: // HIDIOCGRDESCSIZE + case 0x01: *static_cast(argp) = sizeof(kDeckReportDescriptor); return 0; - case 0x02: { // HIDIOCGRDESC: struct hidraw_report_descriptor { __u32 size; __u8 value[4096]; } + case 0x02: { auto *descriptor = static_cast(argp); uint32_t size; memcpy(&size, descriptor, sizeof(size)); memcpy(descriptor + 4, kDeckReportDescriptor, std::min(size, sizeof(kDeckReportDescriptor))); return 0; } - case 0x03: { // HIDIOCGRAWINFO: struct hidraw_devinfo { __u32 bustype; __s16 vendor, product; } + case 0x03: { struct { uint32_t bustype; int16_t vendor; @@ -2798,11 +2521,11 @@ ioctl_deck(DeckHidraw &deck, ioctl_request_t op, void *argp) { memcpy(argp, &info, sizeof(info)); return 0; } - case 0x04: return copy_ioctl_string(op, argp, DECK_NAME); // HIDIOCGRAWNAME - case 0x05: return copy_ioctl_string(op, argp, "usb-droiddeck-1/input2"); // HIDIOCGRAWPHYS - case 0x08: return copy_ioctl_string(op, argp, DECK_SERIAL); // HIDIOCGRAWUNIQ - case 0x06: return deck_feature(deck, op, static_cast(argp), true); // HIDIOCSFEATURE - case 0x07: return deck_feature(deck, op, static_cast(argp), false); // HIDIOCGFEATURE + case 0x04: return copy_ioctl_string(op, argp, DECK_NAME); + case 0x05: return copy_ioctl_string(op, argp, "usb-droiddeck-1/input2"); + case 0x08: return copy_ioctl_string(op, argp, DECK_SERIAL); + case 0x06: return deck_feature(deck, op, static_cast(argp), true); + case 0x07: return deck_feature(deck, op, static_cast(argp), false); default: Logger::log("deck: unhandled hidraw ioctl 0x%02x\n", _IOC_NR(op)); errno = EINVAL; @@ -2831,10 +2554,6 @@ __attribute__((visibility("hidden"))) static void triton_queue_output(DeckHidraw Logger::log("triton: %s report 0x%02x (x%u, %zu bytes)\n", feature ? "feature" : "output", report_id, n, size); } -// HIDIOCGFEATURE: the client reads the controller's info (report 1) this way, and a device that -// answers with nothing is dropped as a zombie. The read goes to the controller itself, as SDL's -// BLE backend does it (hid.m, get_feature_report): its reply is the report, with the report ID -// put in front when the controller left it out. Called without controller_mutex: it waits. __attribute__((visibility("hidden"))) static int triton_get_feature(DeckHidraw &triton, uint8_t *report, size_t size) { if (!report || size < 2 || triton.mapping_size < TRITON_SHARED_OFFSET + TRITON_SHARED_BLOCK_SIZE) { errno = EINVAL; @@ -2877,9 +2596,6 @@ __attribute__((visibility("hidden"))) static int triton_get_feature(DeckHidraw & return -1; } -// The emulated controller's feature reports: report ID 1, then the Valve message (type, length, -// payload), answered here as the Deck's are. The identity is the one a 2025 Steam Controller gave -// on device (HWID 72, its firmware timestamp), with this library's serial. static constexpr uint32_t TRITON_HWID = 72; static constexpr uint32_t TRITON_FW_TIMESTAMP = 0x6AC686B3; @@ -2902,7 +2618,6 @@ __attribute__((visibility("hidden"))) static int triton_pad_feature(DeckHidraw & reply[1] = triton.pending_feature; switch (triton.pending_feature) { case DECK_GET_ATTRIBUTES_VALUES: { - // (attribute, 32-bit value) pairs: product id, capabilities, firmware timestamp, hardware id. static const uint8_t kIds[] = {0x01, 0x02, 0x04, 0x09, 0x0a, 0x0b}; const uint32_t values[] = {static_cast(TRITON_WIRED_PRODUCT), 0, TRITON_FW_TIMESTAMP, TRITON_HWID, 0, 4000}; reply[2] = sizeof(kIds) * 5; @@ -2914,7 +2629,7 @@ __attribute__((visibility("hidden"))) static int triton_pad_feature(DeckHidraw & } case DECK_GET_STRING_ATTRIBUTE: reply[2] = 0x14; - reply[3] = 0x01; // the unit serial + reply[3] = 0x01; memcpy(reply + 4, TRITON_SERIAL, strlen(TRITON_SERIAL)); break; case DECK_GET_CHIP_ID: @@ -2930,25 +2645,22 @@ __attribute__((visibility("hidden"))) static int triton_pad_feature(DeckHidraw & return static_cast(length); } -// The emulated controller's haptic output reports (SDL, controller_structs.h) on the handheld's -// own motor: rumble as asked, the trackpad pulses and effects as short ticks. __attribute__((visibility("hidden"))) static void triton_pad_output(const uint8_t *data, size_t size) { if (!data || size < 2) return; switch (data[0]) { - case 0x80: // ID_OUT_REPORT_HAPTIC_RUMBLE: type, intensity, left {speed, gain}, right {speed, gain} + case 0x80: if (size >= 10) { int left = data[4] | data[5] << 8, right = data[7] | data[8] << 8; - // SDL repeats a held rumble every 40 ms, so each report lasts a little longer than that. send_vibration(left, right, left || right ? 100 : 0, 0); } break; - case 0x81: // ID_OUT_REPORT_HAPTIC_PULSE: side, on_us, off_us, repeat count + case 0x81: if (size >= 8) { int on_ms = (data[2] | data[3] << 8) / 1000, repeat = data[6] | data[7] << 8; send_vibration(0, 40000, std::max(8, std::min(200, on_ms * std::max(1, repeat))), 0); } break; - case 0x82: case 0x83: case 0x84: case 0x85: // command, tone, sweep, script + case 0x82: case 0x83: case 0x84: case 0x85: send_vibration(0, 30000, 15, 0); break; default: @@ -2970,7 +2682,6 @@ __attribute__((visibility("hidden"))) static int ioctl_triton(DeckHidraw &triton uint32_t size; memcpy(&size, descriptor, sizeof(size)); memcpy(descriptor + 4, kTritonReportDescriptor, std::min(size, sizeof(kTritonReportDescriptor))); - // The wired controller's state report (SteamDeckPad's TRITON_WIRED_REPORT_DESCRIPTOR). if (triton.emulated && size > 15) descriptor[4 + 15] = TRITON_STATE_WIRED; return 0; } @@ -2988,8 +2699,6 @@ __attribute__((visibility("hidden"))) static int ioctl_triton(DeckHidraw &triton case 0x05: return copy_ioctl_string(op, argp, triton.emulated ? "usb-droiddeck-3/input0" : "bluetooth-droiddeck-1"); case 0x08: return copy_ioctl_string(op, argp, TRITON_SERIAL); case 0x06: { - // The client's feature reports are the report ID and 64 bytes; the last is padding the BLE - // characteristic does not take (SteamControllerBle.sendOutputReport drops it), so 64 are kept. size_t size = _IOC_SIZE(op); if (!argp || size < 2 || size > 65) { errno = EINVAL; @@ -3008,10 +2717,6 @@ __attribute__((visibility("hidden"))) static int ioctl_triton(DeckHidraw &triton } } -// A device scan that fails on a phone and not on the Thor: libudev gives up its whole scan on any -// sysfs directory it cannot read for a reason other than its absence, and a phone's SELinux policy -// denies directories a permissive device lets through. The client's /sys failures that are not -// ENOENT are logged, each path once, so pad.log names the directory that stopped the scan. __attribute__((visibility("hidden"))) static std::string note_sys_failure(int dirfd, const char *pathname) { int saved_errno = errno; std::string path; @@ -3052,11 +2757,6 @@ __attribute__((visibility("hidden"))) static std::string note_sys_failure(int di return path; } -// An enforcing SELinux policy (every retail phone; the Thor only with it forced on) refuses an app -// a listing of /sys, /sys/class and /sys/bus. libudev lists /sys/class to find the subsystems to -// descend into and gives up its whole scan when it cannot, so the client never reached hidraw and -// found no Deck. The client is listed the stand-ins instead - hidraw under class - and opens -// /sys/class/hidraw itself, which is ours and readable. Only on that refusal, only for the client. __attribute__((visibility("hidden"))) static std::string deck_listing_for(const char *path) { const char *listing = deck_listing_dir(); if (!path || !listing || errno != EACCES || !fake_deck_enabled() || !process_is_steam_client()) return {}; @@ -3072,9 +2772,6 @@ __attribute__((visibility("hidden"))) static std::string deck_listing_for(const } #ifdef __GLIBC__ -// sd-device's path chase now requires a mount ID, which statx only supplies from Linux 5.8. -// Older handheld kernels still publish the real ID in fdinfo. Scope this compatibility path to -// libudev/libsystemd in the Deck-reading Steam client, and never invent a unique mount ID. __attribute__((visibility("hidden"))) static bool deck_device_library(void *caller) { Dl_info info; if (!dladdr(caller, &info) || !info.dli_fname) return false; @@ -3099,7 +2796,6 @@ __attribute__((visibility("hidden"))) static bool deck_mount_id_from_fdinfo( } if (target < 0) return false; struct stat st; - // Pin the inode and check it still matches the successful statx before adding information. bool same = real_fstat(target, &st) == 0 && (buf->stx_mask & STATX_INO) && st.st_ino == buf->stx_ino && major(st.st_dev) == buf->stx_dev_major && minor(st.st_dev) == buf->stx_dev_minor; @@ -3129,7 +2825,7 @@ EXPORT int statx(int dirfd, const char *path, int flags, unsigned mask, struct s static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "statx")); int result = real(dirfd, path, flags, mask, buf); int saved_errno = errno; - constexpr unsigned unique_mount_id = 0x4000; // STATX_MNT_ID_UNIQUE, newer than build headers. + constexpr unsigned unique_mount_id = 0x4000; if (result == 0 && (mask & STATX_MNT_ID) && !(buf->stx_mask & (STATX_MNT_ID | unique_mount_id)) && fake_deck_enabled() && process_is_steam_client() && deck_device_library(__builtin_return_address(0))) { @@ -3553,7 +3249,6 @@ EXPORT int inotify_add_watch(int fd, const char *pathname, uint32_t mask) { } else if (!strcmp(pathname, "/dev/input")) { pathname = fake_hook_dir(); } else if (!strcmp(pathname, "/dev") && fake_deck_enabled() && process_is_steam_client()) { - // Beside /dev itself, not instead: the same descriptor gets both directories' events. std::string events = hidraw_event_dir(); mkdir(events.c_str(), 0700); my_inotify_add_watch(fd, events.c_str(), mask); @@ -3586,8 +3281,6 @@ static bool wait_snapshot(std::shared_ptr fake, return false; } if (read_snapshot(fake->ring, snap)) return true; - // Enumeration queries must not fail merely because Android is publishing - // an input frame. Other hooks (including Binder) can proceed while we wait. guard.unlock(); struct timespec wait = {0, 1000000}; int result = nanosleep(&wait, nullptr); @@ -3602,7 +3295,6 @@ static bool wait_snapshot(std::shared_ptr fake, static int forward_ioctl(int fd, ioctl_request_t op, void *argp) { using ioctl_fn = int (*)(int, ioctl_request_t, ...); - // Preserve later preload hooks, including GPU allocation adapters. static const auto next = reinterpret_cast(dlsym(RTLD_NEXT, "ioctl")); return next ? next(fd, op, argp) : static_cast(syscall(SYS_ioctl, fd, op, argp)); } @@ -3615,9 +3307,6 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) { argp = va_arg(va, void *); va_end(va); - // The lock must not be held across the passthrough: binder's transport is a - // blocking ioctl(BINDER_WRITE_READ), so a parked binder pool thread would own - // controller_mutex() for as long as it waits and deadlock every other ioctl. if (!watched(fd)) return forward_ioctl(fd, op, argp); std::unique_lock guard(controller_mutex()); auto maker = uinput_map().find(fd); @@ -3643,7 +3332,6 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) { int number = (op >> 0 & 0xFF); const char *event = controller->second->event ? controller->second->event : "event0"; int event_number = controller->second->slot; - // A virtual pad answers with what its maker described, not with the app's pad identity. const UinputDescription *made = controller->second->uinput; if (type == 0x45 && number == 0x1) { @@ -3866,8 +3554,6 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) { guard.unlock(); return syscall(SYS_read, fd, buf, count); } - // Keep the mapping alive across waits, even if close() removes the fd and - // another open reuses its number. Every state access still holds the lock. auto handle = controller->second; FakeController &fake = *handle; if (count < FAKE_INPUT_EVENT_SIZE) { @@ -3913,8 +3599,6 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) { memcpy(static_cast(buf) + i * FAKE_INPUT_EVENT_SIZE, &ev, FAKE_INPUT_EVENT_SIZE); } - // A producer can lap the reader while it copies. Discard that copy - // instead of delivering torn/overwritten events or losing a release. __atomic_thread_fence(__ATOMIC_ACQUIRE); if (snap.sequence != __atomic_load_n(&fake.ring->snapshot_seq, __ATOMIC_RELAXED)) continue; @@ -3923,8 +3607,6 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) { } } - // Finish a captured frame before handling a newer resync. Its counter is - // acknowledged only when that newer snapshot is actually captured. size_t events = std::min(requested, fake.keyframe_remaining); struct timeval now = {}; gettimeofday(&now, nullptr); @@ -4006,7 +3688,6 @@ EXPORT ssize_t writev(int fd, const struct iovec *iov, int iovcnt) { } auto made = uinput_map().find(fd); if (made != uinput_map().end()) { - // One description or whole events per vector, which is how uinput writers write. ssize_t total = 0; for (int i = 0; i < iovcnt; i++) { ssize_t written = write_uinput(*made->second, iov[i].iov_base, iov[i].iov_len); @@ -4154,8 +3835,6 @@ EXPORT int ppoll(struct pollfd *fds, nfds_t nfds, if (!is_fake_input_fd(fds[i].fd)) continue; if (!sigmask) return poll_fake(fds, nfds, static_cast(timespec_to_ms(timeout)), nullptr); - // Keep signals pending between polling slices. Each kernel ppoll applies - // the requested mask atomically with its wait, just like a single ppoll. sigset_t all_signals, original_mask; sigfillset(&all_signals); int error = pthread_sigmask(SIG_SETMASK, &all_signals, &original_mask); @@ -4315,8 +3994,6 @@ EXPORT int select(int nfds, fd_set *readfds, fd_set *writefds, } #ifdef __GLIBC__ -// Steam's binaries were linked against an older glibc and reach the calls above through these -// names instead. static mode_t open_mode(int flags, va_list va) { return (flags & (O_CREAT | O_TMPFILE)) ? va_arg(va, mode_t) : 0; } diff --git a/app/src/main/cpp/framegen/VulkanRendererContext.h b/app/src/main/cpp/framegen/VulkanRendererContext.h index cec4a4119..efbc00c68 100644 --- a/app/src/main/cpp/framegen/VulkanRendererContext.h +++ b/app/src/main/cpp/framegen/VulkanRendererContext.h @@ -84,12 +84,11 @@ struct VkTable { PFN_vkCmdSetScissor CmdSetScissor; PFN_vkCmdPipelineBarrier CmdPipelineBarrier; PFN_vkCmdCopyImage CmdCopyImage; - PFN_vkCmdBlitImage CmdBlitImage; // frame-gen capture resolution: composite -> swapchain upscale - // Compute: the native LSFG chain is 25 compute dispatches. + PFN_vkCmdBlitImage CmdBlitImage; + PFN_vkCmdDispatch CmdDispatch; PFN_vkCreateComputePipelines CreateComputePipelines; PFN_vkUnmapMemory UnmapMemory; - // win-fg native: clears its flow scratch, resets its scratch pool on resize. PFN_vkCmdClearColorImage CmdClearColorImage; PFN_vkResetDescriptorPool ResetDescriptorPool; PFN_vkCmdCopyBufferToImage CmdCopyBufferToImage; @@ -125,11 +124,8 @@ struct VkTable { #include #include -// Renderer-neutral direct-scanout impl (owns the SurfaceControl/AHB state). -// Included after SCANOUT_LOG is defined above; its own definition is guarded. #include "../scanout/ScanoutContext.h" -// Native (compositor-side) LSFG frame generation - capability gate. #include "lsfg/lsfg_probe.h" #include #include @@ -139,89 +135,83 @@ namespace winfg { class Engine; } static constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2; -// Push-constant layouts for the spatial-upscaler post passes. The leading vec4 -// `ndc` matches upscale.vert; the remaining members are read only by the -// fragment stage. std430 offsets line up with these C structs. -struct SgsrPushConstants { // 36 bytes + + + +struct SgsrPushConstants { float ndc[4]; - float viewportInfo[4]; // xy = 1/inputSize, zw = inputSize px - float edgeSharpness; // SGSR EdgeSharpness, from the slider + float viewportInfo[4]; + float edgeSharpness; }; -struct NisPushConstants { // 36 bytes +struct NisPushConstants { float ndc[4]; - float viewportInfo[4]; // xy = 1/inputSize, zw = inputSize px - float sharpness; // NIS sharpness [0..1], from the slider + float viewportInfo[4]; + float sharpness; }; -struct EasuPushConstants { // 88 bytes +struct EasuPushConstants { float ndc[4]; uint32_t con0[4], con1[4], con2[4], con3[4]; float outW, outH; }; -struct RcasPushConstants { // 40 bytes +struct RcasPushConstants { float ndc[4]; - uint32_t con[4]; // con.x = bit-packed sharpness + uint32_t con[4]; float outW, outH; }; -// Composable post effects (AMD CAS sharpen + fake-HDR). Both lead with vec4 ndc -// (offset 0) like the upscaler PCs and stay well under the 88-byte PC range. -struct CasPushConstants { // 32 bytes + + +struct CasPushConstants { float ndc[4]; - float resolution[2]; // input texture size in px - float sharpness; // CAS SHARPNESS term [0..1] + float resolution[2]; + float sharpness; float _pad; }; -struct HdrPushConstants { // 24 bytes +struct HdrPushConstants { float ndc[4]; - float resolution[2]; // input texture size in px + float resolution[2]; }; -// Phase 2 screen effects ported from the GL EffectComposer. Each leads with vec4 -// ndc (offset 0) like the upscaler PCs and stays well under the 88-byte PC range. -struct FxaaPushConstants { // 24 bytes + + +struct FxaaPushConstants { float ndc[4]; - float resolution[2]; // input texture size in px + float resolution[2]; }; -struct ToonPushConstants { // 24 bytes +struct ToonPushConstants { float ndc[4]; - float resolution[2]; // input texture size in px + float resolution[2]; }; -struct ColorPushConstants { // 32 bytes +struct ColorPushConstants { float ndc[4]; - float brightness; // additive [-1,1] - float contrast; // [0,2] - float gamma; // [0.1,5] - float saturation; // [0,2]; 1 = unchanged, 0 = greyscale. - // APPENDED last so the three offsets above - // stay where color.frag already had them. + float brightness; + float contrast; + float gamma; + float saturation; + + }; -struct NtscPushConstants { // 28 bytes +struct NtscPushConstants { float ndc[4]; - float resolution[2]; // screen size in px (TextureSize/resolution) - float frameCount; // animated chroma-phase counter + float resolution[2]; + float frameCount; }; -struct CrtPushConstants { // 24 bytes +struct CrtPushConstants { float ndc[4]; - float resolution[2]; // input texture size in px (unused by shader) + float resolution[2]; }; -struct DebandPushConstants { // 28 bytes +struct DebandPushConstants { float ndc[4]; - float resolution[2]; // input texture size in px (layout parity) - float strength; // dither amplitude in LSBs (1.0 = +/-1/255) + float resolution[2]; + float strength; }; class VulkanRendererContext { public: - // `lsfgVk11Compat` (experimental) lets the LSFG capability probe accept a - // Vulkan 1.1/1.2 device that offers the extensions the chain needs; it has - // to be known before vkCreateDevice, hence a constructor argument. VulkanRendererContext(ANativeWindow* window, int cWidth, int cHeight, void* adrenotoolsHandle = nullptr, bool lsfgVk11Compat = false); ~VulkanRendererContext(); void onSurfaceResized(int width, int height); void setTransform(float ox, float oy, float sx, float sy); - // #413: game-region clip rect (surface px). Stored and applied as the swapchain scissor in - // recordCmdBuf so a FILL/STRETCH game confined to a half (TOP/BOTTOM alignment) can't bleed into - // the on-screen-controls half. A full-surface (or w<=0) rect means "no clip" (CENTER). void setClipRegion(int x, int y, int w, int h); void updatePointerPosition(short x, short y); void updateWindowContent(int64_t id, void* pixels, short w, short h, short stride, int x, int y); @@ -241,13 +231,7 @@ public: void scanoutSetBuffer(AHardwareBuffer* ahb, int x, int y, int w, int h, int fenceFd = -1); void scanoutSetCursorImage(void* pixels, short w, short h, short stride); void scanoutSetCursorPos(short x, short y, short hotX, short hotY); - // The renderer-neutral scanout implementation. The scanout methods above are - // thin forwarders to this (see VulkanRendererScanout.cpp); the cursor render- - // thread signalling stays in the forwarders, not in ScanoutContext. ScanoutContext scanout; - // Bound to scanout's atomics so the existing JNI ABI (r->scanoutActive / - // r->gameFrameDelivered direct member access) keeps compiling against a - // single source of truth. Must be declared after `scanout`. std::atomic& scanoutActive = scanout.scanoutActive; std::atomic& gameFrameDelivered = scanout.gameFrameDelivered; std::atomic surfaceDetached{false}; @@ -286,47 +270,16 @@ public: void setPresentMode(VkPresentModeKHR mode); std::vector getSupportedPresentModes() const; - // --- Native LSFG frame generation ------------------------------------- - // Capability verdict for the compositor-side LSFG engine. Filled at device - // creation and completed once the swapchain format is known; read by the - // UI (through JNI) to grey the engine out with a reason. const lsfg::Caps& lsfgCaps() const { return lsfgCaps_; } - // Arm/disarm native LSFG frame generation for this session. Changing the - // armed state recreates the swapchain, because the composite path needs - // TRANSFER_DST usage and a deeper image queue that a normal session does - // not pay for. void setFrameGenArmed(bool armed, int multiplier); bool frameGenArmed() const { return fgArmed_.load(std::memory_order_relaxed); } - // Live frame-gen telemetry for the in-game readout: - // [0] generations the governor currently trusts - // [1] generations actually planned for the last source frame - // [2] measured source (real) frames per second - // [3] measured presented frames per second - // [4] thermal status, -1 when the device gives no signal - // [5] GPU milliseconds the chain spends per generated frame, -1 unknown void frameGenStats(float out[6]) const; - // Why the selected native frame-gen engine cannot run here, for the UI: - // -1 not known yet (caps not probed), 0 fine, 1 this driver lacks what the - // engine needs (lsfgCaps().reason says which gate), 2 the engine failed to start. int frameGenProblem() const; - // Path to the SPIR-V cache built from the user's Lossless.dll. Setting it - // drops any existing engine so the next armed frame rebuilds from it. void setLsfgCachePath(const char* path); - // Flow scale (0.25-1.0) and the panel's real refresh rate. The pacer never - // generates above the refresh rate. - // Experimental: `captureHeight` sizes the composite ring the chain runs on, - // the width following the swapchain's aspect, so a phone GPU generates at - // game-like resolution and the result is blitted up. 0 = panel; - // kFgCaptureGame = the X screen's height (containerHeight), which already - // carries the per-game override and render scale; otherwise a pixel height. static constexpr int32_t kFgCaptureGame = -1; void setFrameGenTuning(float flowScale, float refreshHz, int32_t captureHeight = 0); - // Which native engine generates: 0 = LSFG (needs the cache built from the - // user's Lossless.dll), 1 = win-fg (our own chain, embedded, needs nothing). - // Switching drops the other engine so only one ever holds GPU resources. void setFrameGenEngine(int kind); - // win-fg only: interpolation model (3/4) and performance preset (0..2). void setWinFgTuning(int model, int perfPreset); private: @@ -359,11 +312,9 @@ private: ANativeWindow* window; int surfaceWidth, surfaceHeight, containerWidth, containerHeight; - // #413 compositor clip rect (surface px). clipRegionW<=0 => disabled (whole swapchain). Written from - // the renderer thread (setClipRegion), read from the render thread (recordCmdBuf) -> atomic. std::atomic clipRegionX{0}, clipRegionY{0}, clipRegionW{0}, clipRegionH{0}; void* adrenotoolsHandle = nullptr; - bool lsfgVk11Compat_ = false; // see the constructor + bool lsfgVk11Compat_ = false; int filterMode = 0; bool swapRB = false; float maxAnisotropy = 1.0f; @@ -386,9 +337,6 @@ private: std::vector framePreUpload; std::vector framePostUpload; - // [P0] All SurfaceControl / AHardwareBuffer scanout state moved to - // ScanoutContext (member `scanout` above). Dead members (lastDst*, - // ScanoutPending/scanoutPending*, fnSTSetBackPressure) were dropped. std::atomic pointerX{0}, pointerY{0}; float sceneOffsetX=0.f, sceneOffsetY=0.f, sceneScaleX=1.f, sceneScaleY=1.f; @@ -428,138 +376,69 @@ private: VkPipeline pipeline = VK_NULL_HANDLE; - // ---- Spatial upscaler (SGSR / FSR1) ---- - // upscalerMode enum (mirrored by JNI nativeSetUpscaler): - // 0 = none (passthrough; current sampler governs scaling) - // 1 = linear (rebuild base sampler LINEAR; no shader pass) - // 2 = nearest (rebuild base sampler NEAREST; no shader pass) - // 3 = sgsr (Snapdragon GSR 1.0; single pass; aspect-fit / letterbox) - // 4 = fsr (AMD FSR1 EASU+RCAS; two passes; fill / stretch) - // 5 = fsr_fit (AMD FSR1 EASU+RCAS; two passes; aspect-fit / letterbox) - // 6 = sharpen (RCAS-only; any resolution; aspect-fit / letterbox) - // 7 = nis (NVIDIA Image Scaling NVScaler; single pass; aspect-fit) - // 8 = sgsr_quality (SGSR 1 edge-direction variant; single pass; aspect-fit) - // Shader upscaling only engages for modes 3-8 AND when the game render - // resolution (container) is smaller than the swapchain. Otherwise the - // existing direct-to-swapchain path is used unchanged. int upscalerMode = 0; - // Independent of upscalerMode: high-quality Lanczos downscale for the - // supersampling case (game render res > display res). When enabled and the - // render res exceeds the swapchain, replaces the bilinear minify with a - // proper downscale pass. Supersampling and the upscale modes are mutually - // exclusive in practice (one is render>display, the other render CAS SHARPNESS + int casSharpness = 60; bool hdrEnabled = false; - float upscaleSharpness01 = 0.75f; // slider/100; RCAS lobe scale, SGSR edge derived - // Debanding: terminal TPDF/IGN dither pass before the 8-bit swapchain quantize. + float upscaleSharpness01 = 0.75f; + bool debandEnabled = false; - int debandStrength = 100; // slider 0..200 -> strength/100 LSBs + int debandStrength = 100; - // Phase 2 screen effects (GL EffectComposer parity). FXAA/Toon/CRT/NTSC are - // binary; Color is the brightness/contrast/gamma grade (always-applied via the - // sliders, no toggle) and is "enabled" only when not at neutral (0,0,1). bool fxaaEnabled = false; bool toonEnabled = false; bool crtEnabled = false; bool ntscEnabled = false; - bool colorEnabled = false; // derived: !neutral grade - float colorBrightness = 0.0f; // [-1,1] (slider/100, clamped) - float colorContrast = 0.0f; // [0,2] (slider/100, clamped) - float colorGamma = 1.0f; // [0.1,5] - float colorSaturation = 1.0f; // [0,2] (slider/100, clamped) - uint32_t ntscFrameCounter = 0; // animates NTSC chroma phase + bool colorEnabled = false; + float colorBrightness = 0.0f; + float colorContrast = 0.0f; + float colorGamma = 1.0f; + float colorSaturation = 1.0f; + uint32_t ntscFrameCounter = 0; - VkSampler upscaleSampler = VK_NULL_HANDLE; // linear clamp; offscreen/mid input + VkSampler upscaleSampler = VK_NULL_HANDLE; - // === Native LSFG: composite target ring =================================== - // Frame generation cannot composite straight into a swapchain image: the - // finished frame has to be READABLE (it becomes the next frame's LSFG - // input) and generated frames have to be STORAGE-WRITABLE by a compute - // dispatch. Android swapchain images are COLOR_ATTACHMENT only, and - // storage support on a swapchain format is not something a driver owes us. - // - // So when frame gen is armed the whole existing recording is redirected at - // a composite image we own - format-identical to the swapchain, so every - // existing pipeline stays render-pass compatible - and a copy moves it into - // the acquired swapchain image at the end. With frame gen off, not one of - // these objects is created and the direct-to-swapchain path is untouched. struct CompositeTarget { VkImage img = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; - VkImageView view = VK_NULL_HANDLE; // colour attachment + sampled - VkImageView storageView = VK_NULL_HANDLE; // compute writes (generate) + VkImageView view = VK_NULL_HANDLE; + VkImageView storageView = VK_NULL_HANDLE; VkFramebuffer fb = VK_NULL_HANDLE; - VkDescriptorSet ds = VK_NULL_HANDLE; // sampled, for later passes + VkDescriptorSet ds = VK_NULL_HANDLE; }; - // Hard ceiling: (max generations + 1) presentable frames per source frame, - // times a queue depth of 2, capped so the footprint stays bounded. static constexpr uint32_t kMaxCompositeTargets = 7; std::vector compositeTargets; - VkRenderPass compositeRenderPass = VK_NULL_HANDLE; // CLEAR -> GENERAL + VkRenderPass compositeRenderPass = VK_NULL_HANDLE; uint32_t compositeW = 0, compositeH = 0; - uint32_t compositeIndex = 0; // rotates per composite; gives history for free - bool compositeArmed = false; // targets exist AND this frame uses them - bool swapchainTransferDst = false; // swapchain was created with TRANSFER_DST + uint32_t compositeIndex = 0; + bool compositeArmed = false; + bool swapchainTransferDst = false; - // Set from the app when the native LSFG engine is selected for this - // session. Read on the render thread; false keeps every path as it was. std::atomic fgArmed_{false}; std::atomic fgMultiplier_{0}; - // Tuning is written by the UI thread and consumed by the render thread, so - // it is published through atomics and applied to the engine at the top of - // the frame-gen block. The UI thread never touches the engine itself. std::atomic fgFlowScale_{1.0f}; std::atomic fgRefreshHz_{0.0f}; std::atomic fgConfigDirty_{true}; - // Experimental (see setFrameGenTuning): capture height of the composite - // ring. 0 = panel resolution, kFgCaptureGame = the X screen's height. std::atomic fgCaptureHeight_{0}; - // Extent the composite ring should have for the current swapchain and - // capture height (width follows the swapchain aspect, both even). void compositeExtentFor(uint32_t& w, uint32_t& h) const; - // The extent the scene is rendered at this frame: the composite ring while - // frame gen runs on a ring smaller than the swapchain (experimental capture - // resolution), otherwise the swapchain. Every full-target pass (the effect - // chain, the spatial upscalers, the render-scale downscale) sizes itself by - // this, so those keep working below panel resolution and their - // intermediates shrink with the ring. VkExtent2D renderExtent() const { if (compositeActive() && (compositeW != swapchainExt.width || compositeH != swapchainExt.height)) return VkExtent2D{compositeW, compositeH}; return swapchainExt; } - // Copy (same extent) or blit (composite smaller than the swapchain) a - // composite-ring image, already in TRANSFER_SRC, into a swapchain image - // already in TRANSFER_DST. void recordCompositeToSwapchainTransfer(VkCommandBuffer cb, VkImage src, uint32_t imgIdx); bool createCompositeRenderPass(); bool ensureCompositeTargets(uint32_t w, uint32_t h, uint32_t count); void destroyCompositeTargets(); - // True when this frame should composite off-swapchain. Call on the render - // thread; every gate must hold or we fall back to the untouched path. bool compositeActive() const; - // Render pass / framebuffer this frame draws its FINAL pass into. VkRenderPass targetRenderPass() const; VkFramebuffer targetFramebuffer(uint32_t imgIdx) const; - // Copy the finished composite into the acquired swapchain image and leave - // it in PRESENT_SRC. No-op when the composite path is not active. void copyCompositeToSwapchain(VkCommandBuffer cb, uint32_t imgIdx); - // === Native LSFG: the software cursor ==================================== - // LSFG interpolates whatever it is given, so a cursor composited into the - // frame gets warped along the flow field and smears. It is therefore - // excluded from the composite while frame gen is armed and drawn once into - // EVERY presented image instead - real and generated alike - through a - // load-op render pass that leaves the image ready to present. VkRenderPass cursorOverlayRenderPass = VK_NULL_HANDLE; struct CursorOverlay { bool draw = false; @@ -569,19 +448,16 @@ private: CursorOverlay cursorOverlay_{}; bool createCursorOverlayRenderPass(); - // Draw the cursor into an already-copied swapchain image and leave it in - // PRESENT_SRC. Takes over the final transition from the copy helpers. void recordCursorOverlay(VkCommandBuffer cb, uint32_t imgIdx); - // True when the cursor is being drawn per present rather than composited. bool cursorDrawnPerPresent() const; - // === Native LSFG: the per-source-frame present plan ======================= - // Interpolation produces frames that belong BETWEEN N-1 and N, so the - // generated frames are presented FIRST and real frame N is held back one - // slot. All presents for one source frame are queued together: with FIFO - // the driver then shows them on consecutive vblanks, so the pacing falls - // out of the present mode for free - no sleeps, no render-mode change. - static constexpr uint32_t kMaxPresentsPerFrame = 4; // 1 real + up to 3 generated + + + + + + + static constexpr uint32_t kMaxPresentsPerFrame = 4; struct FrameGenPlan { uint32_t generations = 0; uint32_t presents = 1; @@ -594,30 +470,17 @@ private: bool lsfgEngineTried_ = false; std::unique_ptr winfgEngine_; bool winfgEngineTried_ = false; - std::atomic fgEngineKind_{0}; // 0 = lsfg, 1 = win-fg + std::atomic fgEngineKind_{0}; std::atomic fgModel_{4}; std::atomic fgPerfPreset_{1}; bool ensureWinFgEngine(); - // Capability gate for the SELECTED engine. win-fg's shaders need only a - // storage-capable swapchain format; the LSFG chain also needs the fp16 / - // memory-model feature set, which some otherwise capable GPUs lack. bool fgCapsOk() const; - // Sync objects are indexed per PRESENT, not per composite: each pending - // present needs its own image-available and render-finished semaphore. uint32_t syncSlot(uint32_t k) const { return currentFrame * kMaxPresentsPerFrame + k; } - // Destroy and rebuild every acquire/present semaphore. Called with the - // swapchain, so no stale pending signal can survive into the new one. void recreateSyncObjects(); uint32_t fgAcquireFailLog_ = 0; - // Measured PRESENTS per second - the rate that actually reaches the panel, - // counting generated frames. The pacer's own "loop rate" cannot serve this - // purpose: it is sampled once per SOURCE frame, so it always equals the - // guest rate and contains no evidence that generation happened at all. float fgPresentedRate_ = 0.0f; - // Measured SOURCE frames per second over the same window, for engines that - // do not measure it themselves (win-fg). float fgSourceRate_ = 0.0f; uint32_t fgSourceAccum_ = 0; uint32_t fgPresentAccum_ = 0; @@ -626,22 +489,10 @@ private: void trackPresentedRate(uint32_t presents); bool ensureLsfgEngine(); - // One command buffer per pending present: slot 0 carries the composite, - // the chain's shared passes and generated frame 0; each later generated - // frame and the real frame are recorded and submitted on their own, so a - // finished frame reaches the presentation engine without waiting for the - // rest of the chain. uint32_t cmdSlot(uint32_t k) const { return currentFrame * kMaxPresentsPerFrame + k; } - // Shared chain passes (the 24 shaders every generated frame depends on). void recordFrameGenProcess(VkCommandBuffer cb); - // Generated frame g: synthesise into a spare composite target and copy it - // into the swapchain image reserved for it. void recordFrameGenGeneration(VkCommandBuffer cb, uint32_t g); - // Chain cost: a timestamp pair per frame slot, start before the shared - // passes, end after the last generation's compute (before its copy, so a - // vblank wait on the swapchain image is not counted). Read back after the - // slot's fence wait, one frame later. VkQueryPool fgQueryPool_ = VK_NULL_HANDLE; bool fgTimestampsOk_ = false; float fgTimestampPeriodNs_ = 0.0f; @@ -654,22 +505,21 @@ private: void readFgQueryResult(); VkFormat offscreenFmt = VK_FORMAT_R8G8B8A8_UNORM; - VkRenderPass offscreenRenderPass = VK_NULL_HANDLE; // CLEAR -> SHADER_READ_ONLY + VkRenderPass offscreenRenderPass = VK_NULL_HANDLE; VkPipelineLayout postPipeLayout = VK_NULL_HANDLE; VkPipeline sgsrPipeline = VK_NULL_HANDLE; - VkPipeline nisPipeline = VK_NULL_HANDLE; // NVIDIA Image Scaling (mode 7) - VkPipeline sgsrQualityPipeline = VK_NULL_HANDLE; // SGSR 1 edge-direction (mode 8) + VkPipeline nisPipeline = VK_NULL_HANDLE; + VkPipeline sgsrQualityPipeline = VK_NULL_HANDLE; VkPipeline easuPipeline = VK_NULL_HANDLE; VkPipeline rcasPipeline = VK_NULL_HANDLE; VkPipeline downscalePipeline = VK_NULL_HANDLE; - // CAS/HDR can be either a non-final pass (writes an fx target via - // offscreenRenderPass) or the final pass (writes the swapchain via renderPass); - // a pipeline is render-pass-specific, so keep one variant of each. - VkPipeline casPipelineOff = VK_NULL_HANDLE; // -> fx target (offscreenRenderPass) - VkPipeline casPipelineSwap = VK_NULL_HANDLE; // -> swapchain (renderPass) + + + + VkPipeline casPipelineOff = VK_NULL_HANDLE; + VkPipeline casPipelineSwap = VK_NULL_HANDLE; VkPipeline hdrPipelineOff = VK_NULL_HANDLE; VkPipeline hdrPipelineSwap = VK_NULL_HANDLE; - // Phase 2 effects: same Off (-> fx target) / Swap (-> swapchain) variant pair. VkPipeline fxaaPipelineOff = VK_NULL_HANDLE; VkPipeline fxaaPipelineSwap = VK_NULL_HANDLE; VkPipeline toonPipelineOff = VK_NULL_HANDLE; @@ -680,10 +530,8 @@ private: VkPipeline ntscPipelineSwap = VK_NULL_HANDLE; VkPipeline crtPipelineOff = VK_NULL_HANDLE; VkPipeline crtPipelineSwap = VK_NULL_HANDLE; - // Debanding is always the LAST effect (terminal) -> only a swapchain variant. VkPipeline debandPipelineSwap = VK_NULL_HANDLE; - // offscreen composite target @ game/container resolution VkImage offscreenImg = VK_NULL_HANDLE; VkDeviceMemory offscreenMem = VK_NULL_HANDLE; VkImageView offscreenView = VK_NULL_HANDLE; @@ -691,7 +539,6 @@ private: VkDescriptorSet offscreenDS = VK_NULL_HANDLE; int offscreenW = 0, offscreenH = 0; - // FSR intermediate (EASU output) @ upscale output resolution VkImage midImg = VK_NULL_HANDLE; VkDeviceMemory midMem = VK_NULL_HANDLE; VkImageView midView = VK_NULL_HANDLE; @@ -699,8 +546,6 @@ private: VkDescriptorSet midDS = VK_NULL_HANDLE; int midW = 0, midH = 0; - // Effect-chain intermediates @ swapchain resolution. fx1 holds the scaled (or - // composited) image fed into CAS/HDR; fx2 ping-pongs when both effects run. VkImage fx1Img = VK_NULL_HANDLE; VkDeviceMemory fx1Mem = VK_NULL_HANDLE; VkImageView fx1View = VK_NULL_HANDLE; @@ -715,21 +560,18 @@ private: VkDescriptorSet fx2DS = VK_NULL_HANDLE; int fx2W = 0, fx2H = 0; - // Per-frame upscale plan, computed in renderFrame, consumed by recordCmdBuf. - // upFrame.mode reuses the upscalerMode enum (3=sgsr,4=fsr,5=fsr_fit,6=sharpen,7=nis,8=sgsr_quality) - // plus an internal sentinel (UPMODE_DOWNSCALE) for the supersampling path. struct UpscaleFrame { bool active = false; int mode = 0; int outX = 0, outY = 0, outW = 0, outH = 0; - bool cas = false; // run CAS this frame (snapshot of casEnabled) - bool hdr = false; // run HDR this frame (snapshot of hdrEnabled) - bool fxaa = false; // snapshot of fxaaEnabled - bool toon = false; // snapshot of toonEnabled - bool color = false; // snapshot of colorEnabled (non-neutral grade) - bool ntsc = false; // snapshot of ntscEnabled - bool crt = false; // snapshot of crtEnabled - bool deband = false;// snapshot of debandEnabled (terminal dither) + bool cas = false; + bool hdr = false; + bool fxaa = false; + bool toon = false; + bool color = false; + bool ntsc = false; + bool crt = false; + bool deband = false; SgsrPushConstants sgsrPC{}; NisPushConstants nisPC{}; EasuPushConstants easuPC{}; diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_alpha.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_alpha.cpp index 04a37b720..52b5c76c6 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_alpha.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_alpha.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_alpha.hpp" #include "lsfg_shaders.hpp" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_alpha.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_alpha.hpp index 9a4e15ddf..55db1c754 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_alpha.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_alpha.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_beta.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_beta.cpp index d9413e647..cc9746e11 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_beta.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_beta.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_beta.hpp" #include "lsfg_shaders.hpp" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_beta.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_beta.hpp index 7d259f9e2..a09efcb3b 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_beta.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_beta.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_chain.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_chain.cpp index 54acb1a0f..121b16a68 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_chain.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_chain.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_chain.hpp" #include "lsfg_shaders.hpp" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_chain.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_chain.hpp index 4bab527fd..f0611dac0 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_chain.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_chain.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_common.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_common.cpp index c93275486..5b61acd4c 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_common.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_common.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_common.hpp" #include "lsfg_shaders.hpp" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_common.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_common.hpp index c5ab3e0ad..3abe5cfb9 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_common.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_common.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_delta.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_delta.cpp index cf9b31bfa..d09f31d20 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_delta.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_delta.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_delta.hpp" #include "lsfg_shaders.hpp" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_delta.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_delta.hpp index e40ffe7bd..45e3b4adc 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_delta.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_delta.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_dll.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_dll.cpp index 0af479be8..3a37f032a 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_dll.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_dll.cpp @@ -1,9 +1,5 @@ -// See lsfg_dll.h. PE resource walk + on-device SPIR-V cache for the Lossless -// Scaling frame-generation chain. -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk (PancakeTAS). Ported to WinNative (lsfg_dll.c) and DroidDeck -// by @maxjivi05. GPL-3.0-or-later. +// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS). +// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later. #include "lsfg_dll.h" #include "lsfg_dxbc.h" @@ -25,9 +21,9 @@ namespace lsfg { namespace { -// ---- PE constants --------------------------------------------------------- -constexpr uint16_t kDosMagic = 0x5A4Du; // "MZ" -constexpr uint32_t kPeSignature = 0x00004550u; // "PE\0\0" + +constexpr uint16_t kDosMagic = 0x5A4Du; +constexpr uint32_t kPeSignature = 0x00004550u; constexpr uint16_t kPe32Magic = 0x010Bu; constexpr uint16_t kPe32PlusMagic = 0x020Bu; @@ -47,7 +43,6 @@ constexpr size_t kResourceEntrySize = 8u; constexpr uint32_t kResourceSubdirectoryBit = 0x80000000u; constexpr uint32_t kResourceTypeRcData = 10u; -// ---- SPIR-V constants ----------------------------------------------------- constexpr uint32_t kSpirvMagic = 0x07230203u; constexpr size_t kSpirvHeaderWords = 5u; constexpr uint32_t kSpirvWordCountShift = 16u; @@ -64,7 +59,7 @@ constexpr uint32_t kVariantFp32Offset = 98u; constexpr uint32_t kMaxResourceId = 512u; constexpr size_t kMaxSpirvWords = 16u * 1024u * 1024u; -constexpr uint32_t kCacheMagic = 0x4C534642u; // "BFSL" little-endian +constexpr uint32_t kCacheMagic = 0x4C534642u; constexpr uint32_t kCacheVersion = 1u; struct CacheHeader { @@ -83,7 +78,6 @@ struct PeSection { uint32_t rawSize = 0; }; -// Read-only mapping of the DLL. Closes itself; the DLL is never executed. class PeImage { public: ~PeImage() { close(); } @@ -168,7 +162,6 @@ private: std::vector sections_; }; -// RCDATA id -> blob, for the ids we care about. struct ResourceTable { const uint8_t* data[kMaxResourceId] = {}; uint32_t size[kMaxResourceId] = {}; @@ -217,7 +210,6 @@ bool resourceReadLeaf(const PeImage& img, size_t leafOffset, return true; } -// Walk type -> name -> language and keep the first language of every RCDATA id. bool collectRcData(const PeImage& img, size_t resourceBase, ResourceTable& out) { size_t typeTotal = 0; if (!resourceEntryCount(img, resourceBase, typeTotal)) return false; @@ -256,7 +248,7 @@ bool collectRcData(const PeImage& img, size_t resourceBase, ResourceTable& out) if (!resourceReadLeaf(img, resourceBase + langOffset, blob, blobSize)) continue; out.data[nameId] = blob; out.size[nameId] = blobSize; - break; // first language wins + break; } } } @@ -289,7 +281,6 @@ DllStatus parseResources(PeImage& img, ResourceTable& table) { return DllStatus::Ok; } -// ---- SPIR-V adoption (precompiled-variant path) --------------------------- bool isSpirvModule(const uint8_t* blob, uint32_t size) { if (!blob || size < kSpirvHeaderWords * sizeof(uint32_t)) return false; @@ -315,9 +306,6 @@ uint32_t lookupDescriptorSet(const std::vector& w, uint32_t targetId) return 0; } -// Precompiled blobs are renumbered into set/binding order - that is the -// convention they were built with. The DXBC path uses encounter order instead -// (see lsfg_dxbc), which is what DXVK's output pairs with. Do not merge them. bool renumberBindingsSetOrder(std::vector& w) { struct Slot { uint32_t set; uint32_t binding; size_t literalOffset; }; std::vector slots; @@ -399,8 +387,6 @@ bool writeCache(const std::string& cachePath, const CacheHeader& header, const M if (ok) ok = fsync(fileno(f)) == 0; fclose(f); - // Temp file + rename, so a failed or interrupted build can never leave a - // half-written cache that would later be loaded as if it were complete. if (!ok || rename(tempPath.c_str(), cachePath.c_str()) != 0) { unlink(tempPath.c_str()); return false; @@ -416,9 +402,8 @@ bool readCacheHeader(const std::string& cachePath, CacheHeader& out) { return ok && out.magic == kCacheMagic && out.version == kCacheVersion; } -} // namespace +} -// ---- public API ----------------------------------------------------------- const std::vector& shaderIds() { static const std::vector ids = [] { @@ -584,7 +569,6 @@ DllStatus loadModules(const std::string& cachePath, ModuleSet& outSet) { return status; } -// ---- SPIR-V version lowering (Vulkan 1.1/1.2 compat) ---------------------- namespace { @@ -593,14 +577,14 @@ constexpr uint32_t kSpirvOpExtInstImport= 11u; constexpr uint32_t kSpirvOpMemoryModel = 14u; constexpr uint32_t kSpirvOpCapability = 17u; -// Capabilities that are core in the version the translator emits but not in -// the versions we lower to. -constexpr uint32_t kCapVulkanMemoryModel = 5345u; // core from 1.5 -constexpr uint32_t kCapVulkanMemoryModelDeviceScope = 5346u; // core from 1.5 -constexpr uint32_t kCapDemoteToHelperInvocation = 5379u; // core from 1.6 + + +constexpr uint32_t kCapVulkanMemoryModel = 5345u; +constexpr uint32_t kCapVulkanMemoryModelDeviceScope = 5346u; +constexpr uint32_t kCapDemoteToHelperInvocation = 5379u; std::vector encodeOpExtension(const char* name) { - const size_t len = strlen(name) + 1; // incl. NUL + const size_t len = strlen(name) + 1; const size_t strWords = (len + 3) / 4; std::vector ins(1 + strWords, 0u); ins[0] = ((uint32_t)(1 + strWords) << kSpirvWordCountShift) | kSpirvOpExtension; @@ -624,17 +608,13 @@ bool hasOpExtension(const std::vector& w, const char* name) { return false; } -} // namespace +} bool downgradeSpirv(std::vector& w, uint32_t targetVersion) { if (w.size() < kSpirvHeaderWords || w[0] != kSpirvMagic) return false; if (targetVersion < kSpirv14) return false; - if (w[1] <= targetVersion) return true; // already low enough + if (w[1] <= targetVersion) return true; - // Walk the preamble first and only touch the module once it is known to - // be lowerable: note which extension-gated capabilities are declared and - // where the capability block ends (OpExtension must follow every - // OpCapability and precede OpExtInstImport / OpMemoryModel). bool needMemoryModel = false; size_t insertAt = kSpirvHeaderWords; size_t off = kSpirvHeaderWords; @@ -645,9 +625,6 @@ bool downgradeSpirv(std::vector& w, uint32_t targetVersion) { if (opcode == kSpirvOpCapability && length >= 2) { const uint32_t cap = w[off + 1]; if (cap == kCapVulkanMemoryModel || cap == kCapVulkanMemoryModelDeviceScope) needMemoryModel = true; - // Core only in 1.6, and VK_EXT_shader_demote_to_helper_invocation - // is never enabled on the compat path: refuse rather than hand the - // driver a module that is not legal at the lowered version. if (cap == kCapDemoteToHelperInvocation) return false; insertAt = off + length; } else if (opcode == kSpirvOpExtension) { @@ -659,7 +636,6 @@ bool downgradeSpirv(std::vector& w, uint32_t targetVersion) { off += length; } - // 1.5 has the memory model as core; only 1.4 needs it declared. if (targetVersion < kSpirv15 && needMemoryModel && !hasOpExtension(w, "SPV_KHR_vulkan_memory_model")) { const auto e = encodeOpExtension("SPV_KHR_vulkan_memory_model"); @@ -669,4 +645,4 @@ bool downgradeSpirv(std::vector& w, uint32_t targetVersion) { return true; } -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_dll.h b/app/src/main/cpp/framegen/lsfg/lsfg_dll.h index 74d6ca83d..a20a6c631 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_dll.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_dll.h @@ -1,35 +1,7 @@ +// LSFG from Camille LaVey / the Eden Emulator Project, following upstream lsfg-vk (PancakeTAS). +// Ported to WinNative and DroidDeck by @maxjivi05. GPL-3.0-or-later. + #pragma once -// ============================================================================ -// lsfg_dll - extract the Lossless Scaling frame-generation shader chain from -// the user's own Lossless.dll, and cache it on device as SPIR-V. -// -// The 25 compute shaders that make up LSFG 3.1 are NOT redistributable. They -// ship as RCDATA resources inside Lossless.dll from the user's own paid copy -// of Lossless Scaling (Steam app 993090). Every implementation - upstream -// lsfg-vk, LSFG-Android, eden, WinNative - requires the user to supply their -// own DLL and extracts the resources on device. DroidDeck does the same, and -// bundles nothing. -// -// The DLL is mmap'd READ-ONLY and parsed as data. It is never loaded as a -// library and never executed. -// -// Resource layout, measured against real installs: -// * The base chain lives at RCDATA ids 255, 256 and 280-302 (25 modules), -// and is DXBC. It has always been DXBC. -// * Lossless Scaling MAY additionally carry precompiled SPIR-V copies at -// base+49 (native fp16) and base+98 (native fp32). No build currently -// downloadable from Steam carries them - public buildId 19655272 was -// measured with zero SPIR-V anywhere in the 311 MB install - so the DXBC -// path is the normal path and the translator is not optional. The SPIR-V -// path is kept because it costs nothing and a future build may ship them. -// -// Both producers fill the same shader-id -> SPIR-V words map, so everything -// downstream of this file is source-agnostic. -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk (PancakeTAS). Ported to WinNative (lsfg_dll.c) and DroidDeck -// by @maxjivi05. GPL-3.0-or-later. -// ============================================================================ #include #include @@ -39,44 +11,37 @@ namespace lsfg { enum class DllStatus { Ok = 0, - NotInstalled, // no such file / cannot open - UnreadableFile, // opens but cannot be mapped or is empty + NotInstalled, + UnreadableFile, NotPortableExecutable, - MissingShaders, // parses, but the base chain is not all there - TranslationFailed, // a shader failed DXBC->SPIR-V or SPIR-V adoption - CacheUnusable // cache could not be written or read back + MissingShaders, + TranslationFailed, + CacheUnusable }; -// SPIR-V version words. The translator emits 1.6; a Vulkan 1.1 device takes -// 1.4 (via VK_KHR_spirv_1_4) and a 1.2 device 1.5. One definition for the -// probe, the engine and the shader loader. constexpr uint32_t kSpirv14 = 0x00010400u; constexpr uint32_t kSpirv15 = 0x00010500u; constexpr uint32_t kSpirv16 = 0x00010600u; enum class Variant { None = 0, - SpirvFp16, // precompiled fp16 blobs at base+49 - SpirvFp32, // precompiled fp32 blobs at base+98 - DxbcTranslated // base chain, translated on device (the normal case) + SpirvFp16, + SpirvFp32, + DxbcTranslated }; -// The 25 shader ids that make up the chain. constexpr uint32_t kShaderMipmaps = 255u; constexpr uint32_t kShaderGenerate = 256u; constexpr uint32_t kShaderChainFirst= 280u; constexpr uint32_t kShaderChainLast = 302u; constexpr uint32_t kShaderCount = 25u; -// Upstream spelling for the two ids the ported chain files refer to by name. -// Kept so those files stay byte-close to their upstream form; these alias the -// constants above rather than being a second source of truth. constexpr uint32_t LSFG_SHADER_MIPMAPS = kShaderMipmaps; constexpr uint32_t LSFG_SHADER_GENERATE = kShaderGenerate; struct Module { uint32_t id = 0; - std::vector words; // SPIR-V + std::vector words; }; struct ModuleSet { @@ -87,41 +52,19 @@ struct ModuleSet { bool complete() const; }; -// The canonical shader-id list, in dispatch-independent order. const std::vector& shaderIds(); const char* statusName(DllStatus s); const char* variantName(Variant v); -// Does this file look like a usable Lossless.dll? Checks the base chain only, -// so a DLL without the SPIR-V variants (i.e. every build on Steam today) is -// reported as valid - the translator handles it. DllStatus validateDll(const std::string& dllPath); -// Parse the DLL, produce all 25 SPIR-V modules, and write them to cachePath -// (via temp file + rename, so a failed build cannot leave a half-written -// cache behind). Records which producer ran in the cache header. DllStatus buildCache(const std::string& dllPath, const std::string& cachePath, bool preferFp16); -// Load a previously built cache. DllStatus loadModules(const std::string& cachePath, ModuleSet& outSet); -// Which producer built an existing cache, without loading the modules. DllStatus cacheVariant(const std::string& cachePath, Variant& outVariant); -// Experimental Vulkan 1.1 / 1.2 compat: lower a DXBC-translated module's -// declared SPIR-V version to `targetVersion` (kSpirv14 or kSpirv15). Below 1.5 -// the Vulkan memory model is not core, so SPV_KHR_vulkan_memory_model is -// declared for it (the device side enables VK_KHR_vulkan_memory_model). The -// cache is built without a device in hand, always at the translator's native -// 1.6, so this runs at load time against whatever the device can take. A -// module already at or below the target is left untouched. Returns false, -// leaving the module untouched, for a malformed module, a target below 1.4, or -// a module declaring DemoteToHelperInvocation: that capability is core only in -// 1.6 and its extension is never enabled on the compat path, so such a module -// cannot be made legal here (DXVK emits it for pixel-shader discard only, so -// the 25 compute shaders never carry it). Whether the device then accepts the -// result is vkCreateShaderModule's / vkCreateComputePipelines' verdict. bool downgradeSpirv(std::vector& words, uint32_t targetVersion); -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.cpp index c8084119f..01f904e9d 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.cpp @@ -1,10 +1,5 @@ -// See lsfg_dxbc.h. DXBC -> SPIR-V via DXVK's vendored translator. -// -// Follows upstream lsfg-vk's src/extract/trans.cpp, including its -// encounter-order binding renumber. Ported to WinNative (lsfg_dxbc.cpp) and -// DroidDeck by @maxjivi05 from the work of Camille LaVey / the Eden Emulator -// Project (GPL-3.0-or-later); the translator itself is DXVK's, zlib licensed, -// vendored under cpp/thirdparty/dxbc. +// Ported to WinNative and DroidDeck by @maxjivi05 from Camille LaVey / the Eden Emulator Project (GPL-3.0-or-later). +// DXVK's translator is zlib licensed; vendored under cpp/thirdparty/dxbc. #include "lsfg_dxbc.h" @@ -21,12 +16,12 @@ namespace { struct BindingSlot { uint32_t bindingIndex = 0; - uint32_t bindingOffset = 0; // word offset of the Binding literal + uint32_t bindingOffset = 0; uint32_t setIndex = 0; uint32_t setOffset = 0; }; -} // namespace +} bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector& outWords) { outWords.clear(); @@ -38,9 +33,6 @@ bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector const dxvk::DxbcModuleInfo info{}; auto code = module.compile(info, "CS"); - // Collect the descriptor decorations in the order the Binding - // decorations appear. Everything before the first OpFunction is the - // decoration block, so the scan stops there. std::vector slotsByVarId; std::vector varIdsInOrder; for (auto ins : code) { @@ -60,7 +52,6 @@ bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector if (ins.opCode() == spv::OpFunction) break; } - // Renumber to a dense 0..n range, in encounter order. uint32_t next = 0; for (const uint32_t varId : varIdsInOrder) { const BindingSlot& slot = slotsByVarId[varId]; @@ -75,11 +66,9 @@ bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector std::memcpy(outWords.data(), code.data(), byteSize); return true; } catch (...) { - // DXVK's compiler signals malformed bytecode by throwing. A bad - // resource must degrade to "this DLL is unusable", never to a crash. outWords.clear(); return false; } } -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.h b/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.h index 3fa643728..17ba1876d 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_dxbc.h @@ -1,39 +1,11 @@ #pragma once -// ============================================================================ -// lsfg_dxbc - DXBC -> SPIR-V for the Lossless Scaling shader chain. -// -// Why this exists at all: the base chain inside Lossless.dll is DXBC, and has -// always been DXBC. Lossless Scaling 3.2.2's release note ("Added shaders -// intended for use by the lsfg-vk project") describes precompiled SPIR-V -// copies at RCDATA base+49 / base+98 - but no build downloadable from Steam -// carries them. Measured against the current public build (buildId 19655272, -// 2025-08-19; linux_testing byte-identical on depot 993091): the whole 311 MB -// install contains zero occurrences of the SPIR-V magic word, and the DLL's -// 202 RCDATA entries are all DXBC. -// -// So a translator is required, not optional. Upstream lsfg-vk reaches the same -// conclusion and links DXVK's `dxbc` in src/extract/trans.cpp; we vendor the -// same subset (zlib licence) under cpp/thirdparty/dxbc and follow its -// trans.cpp step for step. -// -// The binding renumber here is ENCOUNTER ORDER - the order in which Binding -// decorations appear in DXVK's output - which is what DXVK's own layout pairs -// with. The precompiled-SPIR-V path in lsfg_dll.cpp uses a set/binding sort -// instead, because those blobs were built with that convention. The two must -// not be merged. -// ============================================================================ +// DXVK's dxbc subset is zlib licensed; vendored under cpp/thirdparty/dxbc. #include #include namespace lsfg { -// Translate one DXBC compute shader to SPIR-V, renumbering descriptor bindings -// into a dense 0..n range in encounter order. Returns false (leaving outWords -// empty) on any malformed input - DXVK's compiler throws, and everything is -// caught here. -// Emits DXVK's native SPIR-V 1.6; lowering for a Vulkan 1.1/1.2 device happens -// at load time (lsfg_dll.h downgradeSpirv). bool translateDxbc(const uint8_t* bytecode, uint32_t size, std::vector& outWords); -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_engine.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_engine.cpp index ba9e8a021..fe0ea3ebe 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_engine.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_engine.cpp @@ -1,8 +1,3 @@ -// See lsfg_engine.h. -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative (vkr_lsfg.cpp) and DroidDeck by -// @maxjivi05, where it runs on gamescope's output in the compositor. #include "lsfg_engine.h" @@ -21,9 +16,6 @@ namespace lsfg { namespace { -// The chain needs two real frames in its history before anything it produces -// is meaningful, and we require the plan to stay warm for a couple of frames -// so a single settled sample cannot start generation from garbage. constexpr uint64_t kRequiredFrames = 2; constexpr uint32_t kRecurrenceFrames = 2; constexpr uint64_t kTelemetryInterval = 120; @@ -50,8 +42,6 @@ VkImageMemoryBarrier makeTransition(VkImage image, VkAccessFlags srcAccess, return b; } -// Copy the just-composited frame into the chain's input ring. `source` is the -// composite target, which the composite render pass leaves in GENERAL. void copyPresentedFrame(VkCommandBuffer cmd, VkImage source, LsfgImage& destination, VkExtent2D extent) { const VkImageMemoryBarrier before[] = { @@ -88,7 +78,7 @@ void copyPresentedFrame(VkCommandBuffer cmd, VkImage source, LsfgImage& destinat destination.SetLayout(VK_IMAGE_LAYOUT_GENERAL); } -} // namespace +} Engine::Engine() = default; @@ -142,9 +132,6 @@ void Engine::setGuestExtent(uint32_t width, uint32_t height) { peakGuestExtent_.height = std::max(peakGuestExtent_.height, height); } -// The flow pyramid is the expensive part, so its resolution is scaled relative -// to the largest guest surface seen: a game running well below panel -// resolution does not need a panel-resolution flow field. float Engine::effectiveFlowScale(uint32_t width) const { if (width == 0 || peakGuestExtent_.width == 0) return flowScale_; const float ratio = (float)peakGuestExtent_.width / (float)width; @@ -171,8 +158,6 @@ bool Engine::prepare(uint32_t width, uint32_t height, VkFormat format) { chain_ = std::make_unique(device_, *shaders_, VkExtent2D{width, height}, format, scale); if (!chain_->Valid()) { - // A device that cannot build the chain reports unsupported once and - // stops trying, rather than thrashing allocations every frame. LSFG_LOGW("chain build failed at %ux%u; frame generation unavailable", width, height); chain_.reset(); unavailable_ = true; @@ -204,17 +189,9 @@ uint32_t Engine::plan(uint32_t capacity, uint64_t sourceFrames) { plan_ = pacer_.Plan(std::min(capacity, kMaxGenerations), sourceFrames); - // The pacer says how many frames FIT in the panel's budget. The governor - // says how many this device can actually afford right now - on a handheld - // the chain competes with the game for one GPU, so an extra generation has - // to prove it improves total output without collapsing the real frame rate. if (governorEnabled_) { const LsfgPacerStats s = pacer_.Stats(); governor_.configure((uint32_t)std::min(pacer_.MaxGenerations(), kMaxGenerations)); - // presentedRate_, NOT the pacer's loop rate. The pacer samples its loop - // once per SOURCE frame, so its "loop rate" is the guest rate by - // construction and can never show that generation added anything - the - // governor could not accept a probe on any hardware. plan_.generations = governor_.cap((uint32_t)plan_.generations, s.source_rate, presentedRate_); } @@ -223,12 +200,6 @@ uint32_t Engine::plan(uint32_t capacity, uint64_t sourceFrames) { warmStreak_ = warm_ ? warmStreak_ + 1 : 0; generating_ = warm_ && warmStreak_ >= kRecurrenceFrames && plan_.generations > 0; - // The ring is only seeded while generating, so the first frame after any - // gap - arming, or a pacer discontinuity after a hitch - would interpolate - // between this frame and whatever the other slot held when generation last - // stopped. That is a visible warp, and it happens every time the engine - // starts. Spend this frame filling the ring instead and generate from the - // next one, when both slots are genuinely consecutive. primeHistory_ = false; if (generating_ && !historyFresh()) { primeHistory_ = true; @@ -265,11 +236,6 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32 lastCount_ = count; lastGenerations_ = generations; - // Seeding the input ring is a full-resolution image copy every frame, so it - // is NOT free at 100 fps - it was costing real frame rate while producing - // nothing. Copy only when generating, on the one priming frame that - // precedes generation, or while the governor is warming up towards a - // probe - so the ring is populated by the time it is needed. const bool needHistory = generations > 0 || primeHistory_ || followEveryFrame() || (governorEnabled_ && governor_.wantsHistory()); if (needHistory) { @@ -278,13 +244,6 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32 haveCopied_ = true; } - // The SHARED chain is 24 of the 25 shaders - the whole flow pyramid - and - // only `generate` runs per generated frame. Running it while producing - // nothing spends almost the entire cost of frame generation for no frames - // at all, every frame. That is what pinned the GPU at 100% on device - // regardless of the game's own settings, and it fed straight back into the - // governor: the chain made the source rate collapse, the governor saw the - // collapse and refused to generate, and refusing did not stop the chain. if (warm_ && (generations > 0 || followEveryFrame())) chain_->DispatchShared(cmd, count); } @@ -318,4 +277,4 @@ void Engine::reset() { plan_ = {}; } -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_engine.h b/app/src/main/cpp/framegen/lsfg/lsfg_engine.h index c8a5028d0..b04726425 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_engine.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_engine.h @@ -1,25 +1,4 @@ #pragma once -// ============================================================================ -// lsfg_engine - the compositor's handle on native LSFG frame generation. -// -// Owns the shader modules, the interpolation chain and the pacer, and exposes -// the small contract the render loop needs: -// -// prepare(w, h, format) build/rebuild the chain for this size -// plan(capacity, sourceFrames) how many frames to generate this time -// process(cmd, source, w, h, gens) take frame N in, run the shared chain -// generateInto(cmd, g, i, img, view) synthesise generated frame g -// -// Ordering matters and is not obvious: interpolation produces frames that -// belong BETWEEN N-1 and N, so the generated frames are presented FIRST and -// the real frame N is held back one slot. That one output interval is the -// unavoidable cost of interpolating rather than extrapolating; no placement of -// this code changes it. -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative (vkr_lsfg.cpp) and DroidDeck by -// @maxjivi05, where it runs on gamescope's output in the compositor. -// ============================================================================ #include #include @@ -28,7 +7,7 @@ #include #include "lsfg_common.hpp" -#include "lsfg_dll.h" // kSpirv16 +#include "lsfg_dll.h" #include "lsfg_governor.h" #include "lsfg_pacer.hpp" @@ -37,27 +16,16 @@ namespace lsfg { class LsfgShaders; class LsfgChain; -// Hard ceiling on generated frames per source frame (2x..4x -> 1..3). constexpr uint32_t kMaxGenerations = 3; class Engine { public: - // Both the constructor and destructor are out-of-line ON PURPOSE. The - // shader set and the chain are held by unique_ptr to forward-declared - // types, and an inline constructor would make the compiler emit the - // exception-cleanup path for those members at every construction site - - // which needs the complete types there. Keeping both here confines that to - // lsfg_engine.cpp, where the types are complete. Engine(); ~Engine(); Engine(const Engine&) = delete; Engine& operator=(const Engine&) = delete; - // Build the shader modules from a cache produced by lsfg_dll::buildCache. - // Returns false if the cache does not yield all 25 modules. `spirvTarget` - // is the highest SPIR-V version the device accepts (lsfg_probe.h); the - // cached 1.6 modules are lowered to it when it is below that. bool init(VkDevice device, VkPhysicalDevice physicalDevice, const std::string& cachePath, uint32_t spirvTarget = kSpirv16); @@ -71,28 +39,18 @@ public: bool needsRebuild(uint32_t width, uint32_t height, VkFormat format) const; bool prepare(uint32_t width, uint32_t height, VkFormat format); - // How many frames to generate for this source frame. Returns 0 until the - // history slots are valid and the rates have settled, so the first frames - // after a start or a resize are never interpolated from garbage. uint32_t plan(uint32_t capacity, uint64_t sourceFrames); - // Take the just-composited frame as LSFG input and run the shared part of - // the chain (everything except `generate`). `source` must be in GENERAL. void process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32_t height, uint32_t generations); - // Synthesise generated frame `generation` into the given storage image. void generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t targetIndex, VkImage targetImage, VkImageView targetView, uint32_t width, uint32_t height); - // What the probe governor currently trusts, and the latest thermal - // reading (-1 = no signal). Surfaced for the HUD and diagnostics. uint32_t acceptedGenerations() const { return governor_.accepted(); } int thermalStatus() const { return governor_.thermalStatus(); } float sourceRate() const; - // The rate that actually reaches the panel, generated frames included. - // Supplied by the renderer, which is the only thing that counts presents. float loopRate() const { return presentedRate_; } void setPresentedRate(float fps) { presentedRate_ = fps; } @@ -102,16 +60,8 @@ public: private: float effectiveFlowScale(uint32_t width) const; - // A generated frame is a blend of the last two REAL frames, so the input - // ring - which is two images deep - only holds usable history when the - // other slot was filled by the frame immediately before this one. It is - // not, every time generation resumes after a pause: the ring is seeded - // only while generating, so the slot still carries whatever was there when - // generation last stopped. bool historyFresh() const { return haveCopied_ && lastCopiedCount_ + 1 == frameCount_; } - // With a target rate the pacer plans 0..3 generations frame by frame, so the ring and the - // shared chain have to follow every frame it keeps warm, not just the ones that generate. bool followEveryFrame() const { return warm_ && pacer_.Config().target_rate != 0; } Device device_{}; @@ -120,12 +70,6 @@ private: std::unique_ptr chain_; LsfgPacer pacer_; ProbeGovernor governor_; - // The governor is OFF by default: the device's own thermal management is - // the authority, and a probe loop that can veto the user's setting is not - // wanted here. The multiplier is then what was asked for, still clamped by - // what the panel can show. Kept behind a flag rather than deleted because - // the probe/backoff logic is the right answer for a device WITHOUT decent - // native throttling, and this is the only thing that would have to change. bool governorEnabled_ = false; LsfgPlan plan_{}; @@ -151,4 +95,4 @@ private: bool unavailable_{}; }; -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_gamma.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_gamma.cpp index 574382ed7..83d117cb3 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_gamma.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_gamma.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_gamma.hpp" #include "lsfg_shaders.hpp" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_gamma.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_gamma.hpp index d1cd38486..c3dcb3848 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_gamma.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_gamma.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_generate.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_generate.cpp index 7be96561c..176208376 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_generate.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_generate.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_generate.hpp" #include "lsfg_dll.h" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_generate.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_generate.hpp index 0612b7422..716650da4 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_generate.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_generate.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_governor.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_governor.cpp index 828b5be6b..11de0eb8a 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_governor.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_governor.cpp @@ -1,4 +1,3 @@ -// See lsfg_governor.h. #include "lsfg_governor.h" @@ -12,13 +11,10 @@ namespace lsfg { namespace { -// A probe has to run long enough to survive a single unlucky frame, and the -// baseline long enough to be a fair comparison. constexpr float kBaselineSeconds = 2.0f; constexpr float kProbeSeconds = 2.0f; constexpr uint32_t kMinSamples = 30; -// Keep the extra generation only if it actually paid off. constexpr float kOutputGainRequired = 1.15f; constexpr float kSourceFloor = 0.70f; @@ -28,20 +24,20 @@ constexpr uint32_t kBackoffSteps = 4; constexpr float kThermalPollSeconds = 2.0f; constexpr float kThermalDecaySeconds = 1.0f; -// ADEVICE_THERMAL_STATUS_* from , which is API 30. Resolved -// by dlsym so the build keeps working below that and on devices that do not -// ship the service; -1 simply means "no thermal signal", never "hot". -// Android reports a STATUS, not a temperature - NONE 0, LIGHT 1, MODERATE 2, -// SEVERE 3, CRITICAL 4, EMERGENCY 5, SHUTDOWN 6 - so there is no degree value -// here to raise. The equivalent of "let it run hotter" is to act on a higher -// status, which is what these now do. -// -// A handheld under a real 3D load sits at SEVERE routinely; blocking there -// made the governor refuse on a device that was working exactly as intended. -// It now only stops GROWING at SEVERE and only gives frames back at CRITICAL, -// leaving the device's own thermal management as the authority. -constexpr int kThermalBlock = 4; // CRITICAL: give generations back -constexpr int kThermalNoGrowth = 3; // SEVERE: keep what we have, stop probing + + + + + + + + + + + + +constexpr int kThermalBlock = 4; +constexpr int kThermalNoGrowth = 3; using PFN_AThermal_acquireManager = void* (*)(); using PFN_AThermal_getCurrentThermalStatus = int (*)(void*); @@ -72,17 +68,12 @@ float phaseSeconds(std::chrono::steady_clock::time_point a, return std::chrono::duration(b - a).count(); } -} // namespace +} void ProbeGovernor::configure(uint32_t maxGenerations) { if (maxGenerations_ == maxGenerations) return; - // Raising the ceiling must NOT throw away what we already know works - - // going 2x -> 4x used to reset the accepted level to zero and restart the - // whole baseline/probe cycle, so a user raising the multiplier got FEWER - // frames. Only clamp down to a ceiling that has actually been lowered. maxGenerations_ = maxGenerations; if (accepted_ > maxGenerations_) accepted_ = maxGenerations_; - // Re-measure against the new ceiling, but keep the accepted level. phase_ = Phase::Baseline; havePhaseStart_ = false; sourceAccum_ = loopAccum_ = 0.0f; @@ -132,19 +123,8 @@ uint32_t ProbeGovernor::cap(uint32_t requested, float sourceRate, float loopRate if (!havePhaseStart_) enterBaseline(now); pollThermal(now); - // Thermal override: at SEVERE and above we stop asking for more and give - // generations back, but SLOWLY. - // - // This runs once per source frame - about forty times a second - so the - // previous version's unconditional accepted_-- was not "give one back", it - // was "slam to zero in three frames", and its unconditional enterBackoff() - // reset the backoff clock every single call, so the backoff could never - // expire and the governor could never measure or recover. One decay per - // second, and no churn at all once there is nothing left to give back. thermalBlocked_ = thermal_ >= kThermalBlock; if (thermal_ >= kThermalBlock) { - // Logged, sparingly. This is a REFUSAL TO EVEN MEASURE, and it was - // silent: on device it looked identical to "measured and declined". if ((refusalLog_++ % 600u) == 0u) GOV_LOGI("thermal status %d - not probing (accepted=%u)", thermal_, accepted_); const bool decayDue = !haveThermalDecay_ @@ -183,12 +163,10 @@ uint32_t ProbeGovernor::cap(uint32_t requested, float sourceRate, float loopRate sourceAccum_ = loopAccum_ = 0.0f; samples_ = 0; } else { - // Also previously silent. "Baseline completed but we did not - // probe" is a decision worth seeing in a log. if ((refusalLog_++ % 600u) == 0u) GOV_LOGI("not probing: accepted=%u max=%u requested=%u thermal=%d", accepted_, maxGenerations_, requested, thermal_); - enterBaseline(now); // nothing to try; measure again later + enterBaseline(now); } } break; @@ -226,12 +204,10 @@ uint32_t ProbeGovernor::cap(uint32_t requested, float sourceRate, float loopRate break; } - // While probing we allow one more than is currently accepted; otherwise the - // accepted level is the ceiling, and the pacer's own answer still applies. const uint32_t allowed = phase_ == Phase::Probing ? std::min(accepted_ + 1, maxGenerations_) : accepted_; return std::min(requested, allowed); } -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_governor.h b/app/src/main/cpp/framegen/lsfg/lsfg_governor.h index 0ae9b696d..3d04f4605 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_governor.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_governor.h @@ -1,29 +1,4 @@ #pragma once -// ============================================================================ -// lsfg_governor - decides whether an extra generated frame is actually WORTH -// it on this device, right now. -// -// The ported pacer answers "how many frames fit in the panel's budget". That -// is necessary but not sufficient on a handheld: the LSFG chain competes with -// the game for the same GPU, so a naive fixed 3x on a GPU-bound title lowers -// the real frame rate by more than the generated frames add. The user sees a -// higher HUD number and a worse-looking game. -// -// So the multiplier is treated as a ceiling to earn, not a setting to obey. -// The governor starts at 0 generations, periodically PROBES one more, and -// keeps it only if the probe measurably paid off: -// -// * total output rate must improve by at least 1.15x, and -// * the real (source) frame rate must not collapse below 0.70x baseline. -// -// A failed probe reverts immediately and backs off 5s, 15s, 30s, then 60s, so -// a device that cannot afford another generation stops being asked. -// -// Thermals are the second input a phone needs and a desktop does not. -// Sustained frame generation is a thermal decision as much as a performance -// one, so the governor gives back generations as the device heats and refuses -// to probe while it is throttling. -// ============================================================================ #include #include @@ -34,22 +9,15 @@ class ProbeGovernor { public: void configure(uint32_t maxGenerations); - // Cap the pacer's answer. Called once per source frame with the measured - // rates; returns the number of generations actually permitted. uint32_t cap(uint32_t requested, float sourceRate, float loopRate); void reset(); uint32_t accepted() const { return accepted_; } bool probing() const { return phase_ == Phase::Probing; } - // Whether the chain's input ring needs to be kept fed. Seeding it is a - // full-resolution copy every frame, so it is skipped during the long idle - // stretches - a backoff, or a thermal block - and resumed during Baseline, - // which gives it a couple of seconds to fill before a probe can start. bool wantsHistory() const { return !thermalBlocked_ && phase_ != Phase::Backoff; } - // Latest thermal reading, 0 (none) .. 6 (shutdown); -1 when unavailable. int thermalStatus() const { return thermal_; } private: @@ -68,11 +36,9 @@ private: Clock::time_point phaseStart_{}; bool havePhaseStart_ = false; - // Rolling means over the current phase window. float sourceAccum_ = 0.0f, loopAccum_ = 0.0f; uint32_t samples_ = 0; - // What the accepted level achieves, measured during Baseline. float baselineSource_ = 0.0f, baselineLoop_ = 0.0f; bool haveBaseline_ = false; @@ -87,4 +53,4 @@ private: bool haveThermalPoll_ = false; }; -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_jni.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_jni.cpp index ab13096d6..32dfd552d 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_jni.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_jni.cpp @@ -1,8 +1,3 @@ -// JNI surface for native LSFG shader-cache management. -// -// Deliberately free of any renderer handle: extracting the shader chain from -// Lossless.dll and caching it is a slow, once-per-DLL operation that the app -// runs off the render thread, long before a game launches. #include "lsfg_dll.h" @@ -20,7 +15,7 @@ std::string toStdString(JNIEnv* env, jstring s) { return out; } -} // namespace +} extern "C" JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_gpu_LsfgNative_nativeValidateDll(JNIEnv* env, jclass, jstring dllPath) { diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.cpp index ef1e11318..2e42facd6 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.cpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_mipmaps.hpp" #include "lsfg_dll.h" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.hpp index d301c26ef..e863b7491 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_mipmaps.hpp @@ -3,10 +3,6 @@ // SPDX-FileCopyrightText: Copyright 2025 lsfg-vk // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_pacer.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_pacer.cpp index 8ddf453c2..57307ee05 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_pacer.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_pacer.cpp @@ -1,9 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_pacer.hpp" @@ -21,7 +17,6 @@ constexpr float SOURCE_SMOOTHING = 0.15f; constexpr float SOURCE_STALE_SECONDS = 0.5f; constexpr float DISCONTINUITY_SECONDS = 0.25f; constexpr float HEADROOM_EPSILON = 0.02f; -// A target is aimed just under, so rounding never puts output past the panel's refresh rate. constexpr float TARGET_MARGIN = 0.1f; constexpr float CREDIT_EPSILON = 1.0e-4f; constexpr float SOURCE_ACCUM_FLOOR = 0.01f; @@ -121,17 +116,6 @@ LsfgPlan LsfgPacer::Plan(size_t capacity, uint64_t source_frames) { if (target_rate > 0.0f) target_rate -= TARGET_MARGIN; if (target_rate == 0.0f) { - // No explicit target rate: the multiplier is the multiplier, full stop. - // The panel-headroom clamp was here briefly and was removed at the - // user's decision; this is also upstream's behaviour on this branch. - // - // What that means above the panel's refresh rate, for the record: the - // display cannot show more frames than it refreshes, so under FIFO the - // surplus presents queue and the render thread waits on acquire. Nothing - // is displayed faster, and the compositor falls behind on guest - // deliveries instead. Below the refresh rate - every source at or under - // panel/4 - this branch behaves identically with or without the clamp. - // To restore it: limit = std::min(ceiling, HeadroomLimit()). output_credit = 0.0f; limit = ceiling; return LsfgPlan{limit, true}; diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_pacer.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_pacer.hpp index fe4a43ee5..5762ee47c 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_pacer.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_pacer.hpp @@ -1,9 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_probe.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_probe.cpp index 823283419..36ed36ced 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_probe.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_probe.cpp @@ -1,4 +1,3 @@ -// See lsfg_probe.h. Capability gate for native compositor-side LSFG. #include "lsfg_probe.h" #include "../VulkanRendererContext.h" @@ -16,7 +15,7 @@ bool hasExtension(const std::vector& exts, const char* na return false; } -} // namespace +} FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd, const std::vector& deviceExtensions, @@ -33,33 +32,21 @@ FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd, const bool below12 = props.apiVersion < VK_API_VERSION_1_2; if (below12) { - // Below 1.2 there is no VkPhysicalDeviceVulkan12Features to chain, and - // the SPIR-V 1.6 modules will not load anyway. Unless the experimental - // compat path is on AND the driver offers the extensions that make the - // chain loadable as SPIR-V 1.4, don't touch the device further: - // device creation stays exactly as it is today. if (!allowVk11) return fs; fs.hasSpirv14 = hasExtension(deviceExtensions, VK_KHR_SPIRV_1_4_EXTENSION_NAME); fs.hasFloatControls = hasExtension(deviceExtensions, VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); fs.hasMemoryModelExt = hasExtension(deviceExtensions, VK_KHR_VULKAN_MEMORY_MODEL_EXTENSION_NAME); if (!fs.hasSpirv14 || !fs.hasFloatControls || !fs.hasMemoryModelExt) { - // Report what was looked at so explain() can name the gap. fs.queried = true; return fs; } fs.extensionPath = true; fs.spirvTarget = kSpirv14; } else if (!fs.apiAtLeast13 && allowVk11) { - // 1.2: SPIR-V 1.5 is core, VulkanMemoryModel is core. Nothing to - // enable beyond the features; the modules only need their header - // lowered from 1.6 to 1.5. Same experimental gate as the 1.1 path so - // a production build keeps rejecting 1.2 exactly as before. fs.extensionPath = true; fs.spirvTarget = kSpirv15; } - // On 1.2+ the Vulkan12Features struct carries the memory model; below - // that the KHR struct does (same sType value, same layout). VkPhysicalDeviceVulkan12Features v12{}; v12.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; VkPhysicalDeviceVulkanMemoryModelFeaturesKHR mm{}; @@ -87,7 +74,7 @@ FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd, std::vector extensionNames(const FeatureSupport& f) { std::vector out; if (!f.extensionPath || f.deviceApiVersion >= VK_API_VERSION_1_2) return out; - out.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); // dependency of spirv_1_4 + out.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME); out.push_back(VK_KHR_SPIRV_1_4_EXTENSION_NAME); out.push_back(VK_KHR_VULKAN_MEMORY_MODEL_EXTENSION_NAME); return out; @@ -108,10 +95,6 @@ bool probeStorageFormat(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt) { VkFormatProperties fp{}; vk.GetPhysicalDeviceFormatProperties(pd, fmt, &fp); - // The composite target is COLOR_ATTACHMENT (effect chain writes it), - // SAMPLED (next frame's LSFG input reads it), STORAGE (generate writes it) - // and both blit ends (it is copied - or, below panel resolution, blitted - - // into the swapchain image). const VkFormatFeatureFlags need = VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT @@ -163,4 +146,4 @@ void explain(Caps& caps) { } } -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_probe.h b/app/src/main/cpp/framegen/lsfg/lsfg_probe.h index 38d1d1a4f..3042b1040 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_probe.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_probe.h @@ -1,45 +1,17 @@ #pragma once -// ============================================================================ -// lsfg_probe - capability gate for native (compositor-side) LSFG frame -// generation. -// -// The Lossless Scaling chain is 25 compute shaders that DXVK's DXBC translator -// emits as SPIR-V 1.6 with OpCapability VulkanMemoryModel and -// StorageImageWriteWithoutFormat. Three consequences, all checked here: -// -// * SPIR-V 1.6 will not load on a Vulkan 1.1 device, so the DEVICE (not just -// the instance) must report 1.3+ ... OR, on the experimental Vulkan 1.1 -// compat path, offer VK_KHR_spirv_1_4 + VK_KHR_vulkan_memory_model so the -// same modules can be handed over as SPIR-V 1.4 (see downgradeSpirv in -// lsfg_dll.h). The stock Qualcomm driver on Adreno 7xx phones reports -// 1.1.128 yet carries every one of those extensions (measured on an -// Adreno 710 / HyperOS device with `cmd gpu vkjson`). -// * vulkanMemoryModel, shaderStorageImageWriteWithoutFormat and -// shaderStorageImageExtendedFormats must be ENABLED at device creation. -// Today the renderer enables no features at all, so all three are off. -// * `generate` writes into a storage image, and Android swapchain formats -// are frequently not storage-capable - so the format is probed separately -// once the swapchain has picked one. -// -// A device failing any gate reports unsupported UP FRONT, with a reason, so -// the UI can grey the engine out instead of failing later inside -// vkCreateShaderModule or vkCreateComputePipelines. -// ============================================================================ #include #include #include -#include "lsfg_dll.h" // kSpirv14/15/16 +#include "lsfg_dll.h" struct VkTable; namespace lsfg { -// Which of the three required features the physical device OFFERS. Queried -// before vkCreateDevice; what we actually enable is recorded in Caps below. struct FeatureSupport { - bool queried = false; // vkGetPhysicalDeviceFeatures2 resolved + bool queried = false; bool apiAtLeast13 = false; bool vulkanMemoryModel = false; bool vulkanMemoryModelDeviceScope= false; @@ -47,65 +19,41 @@ struct FeatureSupport { bool storageImageExtendedFormats = false; uint32_t deviceApiVersion = 0; - // Experimental Vulkan 1.1 compat: the device is below 1.3 but offers the - // extensions that make the chain loadable. When set, the renderer must - // enable those extensions at device creation (see extensionNames) and the - // modules are downgraded to `spirvTarget` at load. bool extensionPath = false; - bool hasSpirv14 = false; // VK_KHR_spirv_1_4 - bool hasFloatControls = false; // VK_KHR_shader_float_controls (spirv_1_4 depends on it) - bool hasMemoryModelExt = false; // VK_KHR_vulkan_memory_model + bool hasSpirv14 = false; + bool hasFloatControls = false; + bool hasMemoryModelExt = false; uint32_t spirvTarget = kSpirv16; - // Every hard device-level gate passes (format is checked separately). bool deviceGatesPass() const { return queried && (apiAtLeast13 || extensionPath) && vulkanMemoryModel && storageImageWriteWithoutFormat && storageImageExtendedFormats; } }; -// What was actually enabled + the running verdict. Owned by the renderer. struct Caps { FeatureSupport features; - bool featuresEnabled = false; // the chain was passed to vkCreateDevice + bool featuresEnabled = false; bool storageOnSwapchainFormat = false; - // The swapchain format blits with VK_FILTER_LINEAR (needs - // SAMPLED_IMAGE_FILTER_LINEAR on the source). Not a gate: without it the - // capture-resolution upscale falls back to NEAREST. bool linearBlitOnSwapchainFormat = false; VkFormat probedFormat = VK_FORMAT_UNDEFINED; char reason[160] = "not probed"; - // The single question the UI and the render path ask. bool supported() const { return featuresEnabled && features.deviceGatesPass() && storageOnSwapchainFormat; } }; -// Ask the physical device which of the required features it offers. -// Safe on any driver: if vkGetPhysicalDeviceFeatures2 cannot be resolved, or -// the device reports below Vulkan 1.2 and `allowVk11` is false, nothing is -// chained and `queried` is left false - the caller then creates the device -// exactly as it always has. With `allowVk11`, a 1.1 device is accepted when -// `deviceExtensions` lists VK_KHR_spirv_1_4, VK_KHR_shader_float_controls and -// VK_KHR_vulkan_memory_model, and the memory-model features are queried -// through the KHR struct instead of VkPhysicalDeviceVulkan12Features. FeatureSupport queryFeatures(const VkTable& vk, VkPhysicalDevice pd, const std::vector& deviceExtensions, bool allowVk11); -// The device extensions the extension path needs enabled, in the order they -// should be pushed onto VkDeviceCreateInfo. Empty unless extensionPath. std::vector extensionNames(const FeatureSupport& f); -// Probe VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT on the live swapchain format. -// `generate` writes into an image of this format via a compute dispatch. bool probeStorageFormat(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt); -// Whether the format may be the source of a VK_FILTER_LINEAR blit. bool probeLinearBlit(const VkTable& vk, VkPhysicalDevice pd, VkFormat fmt); -// Fill caps.reason with the FIRST gate that failed (or "supported"). void explain(Caps& caps); -} // namespace lsfg +} diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_shaders.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_shaders.cpp index d3266d173..05562d390 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_shaders.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_shaders.cpp @@ -1,9 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #include "lsfg_shaders.hpp" #include "lsfg_common.hpp" @@ -27,12 +23,6 @@ LsfgShaders::LsfgShaders(const Device& device_, const std::string& cache_path, return; } - // Vulkan 1.1/1.2 compat: the cache is always built at the translator's - // native SPIR-V 1.6 (no device exists at import time), so lower each module - // to what THIS device accepts. A no-op on 1.3+ devices. Only the - // DXBC-translated chain is lowered: downgradeSpirv knows the capabilities - // DXVK emits, not whatever a precompiled vendor SPIR-V variant may carry, - // and relabelling one of those could hand the driver invalid SPIR-V. uint32_t lowered = 0; for (Module& module : set.modules) { if (module.words.size() > 1 && module.words[1] > spirv_target) { diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_shaders.hpp b/app/src/main/cpp/framegen/lsfg/lsfg_shaders.hpp index d91b00c18..0647564a9 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_shaders.hpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_shaders.hpp @@ -1,9 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -// -// LSFG from the work of Camille LaVey / the Eden Emulator Project, following -// upstream lsfg-vk. Ported to WinNative and DroidDeck by @maxjivi05; only the -// Vulkan dispatch differs here (see lsfg_vkd.h). #pragma once @@ -20,8 +16,6 @@ class Device; class LsfgShaders { public: LsfgShaders() = default; - // `spirv_target` is the highest SPIR-V version the device accepts (see - // lsfg_probe.h); cached modules above it are lowered before creation. LsfgShaders(const Device& device, const std::string& cache_path, uint32_t spirv_target = kSpirv16); ~LsfgShaders(); diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_vkd.cpp b/app/src/main/cpp/framegen/lsfg/lsfg_vkd.cpp index 1e7877e92..b89807077 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_vkd.cpp +++ b/app/src/main/cpp/framegen/lsfg/lsfg_vkd.cpp @@ -1,4 +1,3 @@ -// See lsfg_vkd.h. Fills the chain's dispatch from the renderer's own table. #include "lsfg_vkd.h" #include "../VulkanRendererContext.h" diff --git a/app/src/main/cpp/framegen/lsfg/lsfg_vkd.h b/app/src/main/cpp/framegen/lsfg/lsfg_vkd.h index c86d2224f..3e0ce0c0e 100644 --- a/app/src/main/cpp/framegen/lsfg/lsfg_vkd.h +++ b/app/src/main/cpp/framegen/lsfg/lsfg_vkd.h @@ -1,24 +1,4 @@ #pragma once -// ============================================================================ -// lsfg_vkd - the Vulkan entry points the LSFG chain calls, as a single global. -// -// Two dispatch styles meet here. DroidDeck's compositor resolves Vulkan -// through a per-context VkTable (`vk_`), because an adrenotools-loaded driver -// lives in an isolated linker namespace and shares no global symbols with the -// system loader. The LSFG chain, ported from Eden by way of WinNative, calls -// through a file-scope `vkd`. Rather than rewrite ~2,300 lines of proven chain -// code to thread a table through every call, this shim exposes exactly the 34 -// entry points the chain uses and is filled from the renderer's own table when -// the chain is created. -// -// That keeps the ported files close to their upstream form, which matters: -// they are the part of this feature that is hardest to review by eye and -// easiest to break with a well-meaning edit. -// -// Single-renderer assumption: there is one Vulkan compositor per process, so -// one global table is correct. lsfgVkdInit() is idempotent and simply -// overwrites; lsfgVkdReady() reports whether every pointer resolved. -// ============================================================================ #include @@ -63,8 +43,5 @@ struct LsfgVkDispatch { extern LsfgVkDispatch vkd; -// Fill `vkd` from the renderer's table. Returns false (and leaves the chain -// unusable) if any entry point failed to resolve - better to report the engine -// unsupported than to call through a null pointer mid-frame. bool lsfgVkdInit(const VkTable& table); bool lsfgVkdReady(); diff --git a/app/src/main/cpp/framegen/winfg/config.hpp b/app/src/main/cpp/framegen/winfg/config.hpp index d8786697c..a3bf09a5e 100644 --- a/app/src/main/cpp/framegen/winfg/config.hpp +++ b/app/src/main/cpp/framegen/winfg/config.hpp @@ -1,6 +1,3 @@ -// win-fg - runtime config: enable gate, model, multiplier, and the synthesis -// tuning parameters. Sourced from environment variables and an optional -// conf.toml (a config file, when present, wins over the env defaults). #pragma once #include #include @@ -12,143 +9,110 @@ namespace winfg { struct Config { - bool enabled = false; // gate; loader also honours the manifest enable var - // GRANULAR PRESENT-PATH DEBUG TRACE. Default OFF. When ON, winfg_QueuePresentKHR - // emits a per-frame, step-by-step trail (logcat tag "win-fg") so a freeze/ - // crash-on-enable leaves an obvious LAST line at the exact stage that hung or - // faulted (acquire-spare, compute-submit, present-generated/real, fence wait, - // fallback). Zero behaviour change and effectively zero cost when off - one - // predicted-not-taken bool test per step, no logcat writes. - // WIN_FG_DEBUG=1|0 / conf.toml debug=on|off. + bool enabled = false; + + + + + + + bool debug = false; - // ── EVEN-CADENCE FRAME PACING ───────────────────────────────────────────── - // Default ON. The 2x insert path presents the GENERATED frame then the REAL - // frame back-to-back in one vkQueuePresentKHR hook call, so on Mailbox/ - // Immediate both land in ~one instant followed by a full frame-interval gap - // until the next pair - uneven in time → judder ("slows down to catch up"). - // With pacing on, the layer measures the real-frame interval (EMA of a - // monotonic clock) and holds the REAL present to its scheduled beat so the - // generated frame lands near the temporal MIDPOINT between consecutive real - // frames (even gen,real,gen,real cadence). Pure bounded CPU sleep - no GPU - // wait added; if a frame is already behind schedule the wait is SKIPPED - // (present immediately) so worst case is one un-paced pair, never a hitch. - // FIFO self-paces so the waits collapse to ~0 there. Off ⇒ byte-identical - // back-to-back behaviour. WIN_FG_PACING=on|off / conf.toml pacing=on|off. - // ISOLATION BUILD: default flipped to OFF to A/B the AYANEO-750 post-setting-change - // flicker regression against the pacing feature (everything else byte-identical). bool pacing = false; - int model = 4; // 3 = symmetric flow, 4 = bidir + occlusion gate - int multiplier = 2; // generated presents per real present + 1 - // EXTRA SWAPCHAIN IMAGE HEADROOM (device-freeze fix). On top of the +1 spare the - // insert path needs, request this many ADDITIONAL images at swapchain creation: - // requested minImageCount = app_min + 1 (spare) + extra_images, - // clamped to surfaceCaps.maxImageCount (0 = unlimited -> no clamp), never below - // app_min+1. The extra headroom stops the guest's NEXT vkAcquireNextImageKHR from - // starving while win-fg holds a spare + a real frame in flight to the AHB-backed - // host compositor. Some SoCs (Adreno 840 / Turnip) recycle displayed AHBs too - // slowly to tolerate the extra present with only the +1 spare and freeze on the - // first inserted frame; the AYANEO (Adreno 750) recycles fast enough to tolerate - // it. Default 2 (a few MB VRAM), applied on all devices. On a device that already - // works this only grows the pool - no FPS/quality change; the pool-headroom guard - // in QueuePresentKHR stays a no-op when headroom is adequate. - // WIN_FG_EXTRA_IMAGES / conf.toml extra_images. + int model = 4; + int multiplier = 2; + + + + + + + + + + + + + int extraImages = 2; - // ── FLOW-RESOLUTION PERFORMANCE PRESET ──────────────────────────────────── - // Trades base-FPS-drop against optical-flow quality by selecting the FINEST - // pyramid level the dense flow is solved at (FrameGen::kFlowFinest): - // 0 = Quality -> kFlowFinest = 1 (~1/2-res flow; best motion, biggest FG cost) - // 1 = Balanced -> kFlowFinest = 2 (~1/4-res; TODAY's behaviour) [DEFAULT] - // 2 = Performance -> kFlowFinest = 3 (~1/8-res; cheapest -> smallest base-FPS drop) - // Out-of-range clamps to Balanced (1), so an unset/older conf.toml is byte- - // identical to today. Changing this LIVE rebuilds the affected per-size flow - // images (like a resize) inside the layer and resets the flow predictor, so a - // preset change is fully hot - no FG toggle needed. See FrameGen::configure / - // FrameGen::flowFinestForPreset. WIN_FG_PERF_PRESET / conf.toml perf_preset. - int perfPreset = 1; // 0 quality, 1 balanced (default), 2 performance - float flowScale = 1.0f; // scales solved flow magnitude - // C1 GLOBAL-MOTION PRE-WARP. Estimate the per-frame camera affine (LK) and - // remove it before the dense SAD flow search so the search only sees coherent - // object-only residual motion (kills fast-pan "melt"). 0 = auto (engage only - // when the LK solve is stable), 1 = on (force-engage once a solve exists), - // 2 = off (never pre-warp; identity affine → pipeline == pre-C1 exactly). - // WIN_FG_GM=auto|on|off / conf.toml global_motion=auto|on|off. - int gmMode = 0; // 0 auto, 1 on, 2 off - // C2 FLOW REGULARIZATION (TV-L1 smoothness prior). After C1 removes the camera - // motion, the dense flow (flowLvl_ at kFlowFinest, 1/4-res) is the OBJECT-only - // residual; it still carries spurious per-block vectors. C2 runs N semi-implicit - // TV-L1 denoising iterations on that field - an L1 data term + edge-aware Total- - // Variation regularizer - to kill incoherent vectors while KEEPING true motion - // discontinuities (object edges) sharp. Solved in-place; expand's median + C1 - // add-back consume the cleaned field unchanged. 0 iterations ⇒ byte-identical to - // pre-C2. See shaders/of3_flowreg.comp. - // WIN_FG_FLOWREG=auto|on|off / conf.toml flow_reg=auto|on|off. - int frMode = 0; // 0 auto (engaged), 1 on (engaged), 2 off - int frIters = 4; // TV-L1 iterations at kFlowFinest (0 ⇒ off) - float frLambda = 2.0f; // L1 data-fidelity weight (higher ⇒ trust SAD flow) - float frDt = 0.25f; // semi-implicit smoothing step (higher ⇒ smoother) - float frEdge = 8.0f; // luma-gradient edge sensitivity for the TV weight g - float frEps = 0.05f; // Charbonnier epsilon (px) for the TV diffusivity - // synthesis (wfg_synth) tuning - float beta = 8.0f; // softmax sharpness on importance Z - float lambda = 0.6f; // FB-consistency vs photometric weight - // Tightened 2026-08-17 to prioritise "no ghost" over max sharpness - the - // remaining single-frame ghosts came from pixels that the previous defaults - // still trusted with warp when they should have cross-faded. Raising both - // pushes borderline pixels toward the safe cross-fade path (softer, but no - // smear). Prewarp will lift the ceiling further; this is the "safety net" - // knob change until then. - // Walked back from (0.10, 9.0) tighten after user reported softening. The - // real fix for the residual ghosts is the 3a-fallback path change in - // layer.cpp (present real curr on fallback instead of blitting synth over); - // the gate can stay closer to the original values now. - float epsilon = 0.07f; // disocclusion floor (0.05 -> 0.10 -> 0.07) - float photoScale = 7.5f; // photometric residual scale into Z (6.0 -> 9.0 -> 7.5) - // HUD exclusion rect in pixel coords (x0,y0,x1,y1). Fragments inside are - // passed through as the real current frame (no warp, no synth) so text / - // overlays don't ghost. Disabled when x0 >= x1 or y0 >= y1 (the default). - // Sourced from WIN_FG_HUD_RECT="x0,y0,x1,y1" env var or conf.toml - // (hudRect="x0,y0,x1,y1"). Pattern is Isygold's Vegas DXVK framegen - - // host knows where the HUD is, tell the shader to skip it. + + + + + + + + + + + + int perfPreset = 1; + float flowScale = 1.0f; + + + + + + + int gmMode = 0; + + + + + + + + + + int frMode = 0; + int frIters = 4; + float frLambda = 2.0f; + float frDt = 0.25f; + float frEdge = 8.0f; + float frEps = 0.05f; + + float beta = 8.0f; + float lambda = 0.6f; + + + + + + + + + + + float epsilon = 0.07f; + float photoScale = 7.5f; + + + + + + float hudX0 = 0.0f; float hudY0 = 0.0f; float hudX1 = 0.0f; float hudY1 = 0.0f; - // ── TRAINING-DATA CAPTURE MODE (Route-B VFI dataset collection) ─────────── - // A dev-only gate. When OFF (default) the present path is byte-identical to a - // build without this feature - a single bool test and nothing else. When ON, - // every real present is downscaled on the GPU, read back async, and written to - // disk as lossless QOI (motion-rich 256² triplet patches by default, or full - // downscaled frames) plus a JSONL manifest, for offline (i-1,i+1)->i training. - // NEVER captures win-fg's generated frames - capture reads the real swapchain - // image at the TOP of QueuePresentKHR, before any interpolation. - // WIN_FG_CAPTURE=on|off / conf.toml capture=on|off. bool capture = false; - // Downscale target box (aspect-preserving, never upscales). WIN_FG_CAPTURE_W/H, - // conf capture_width/capture_height. int captureW = 1280; int captureH = 720; - // 0 = patch (self-contained aligned triplets, DEFAULT), 1 = frame (full - // downscaled frames in order). WIN_FG_CAPTURE_MODE=patch|frame. int capMode = 0; - int capPatches = 3; // patches per triplet in patch mode (WIN_FG_CAPTURE_PATCHES) - int capPatchSize = 256; // patch edge in downscaled px (WIN_FG_CAPTURE_PATCH) - // Skip a unit whose inter-frame motion (mean per-pixel luma abs-diff, 0..255) - // is below this. WIN_FG_CAPTURE_MOTION / conf capture_motion. + int capPatches = 3; + int capPatchSize = 256; + + float capMotion = 2.0f; - // Output dir; empty => $HOME/.cache/winfg-capture. WIN_FG_CAPTURE_DIR / - // conf capture_dir. A per-run session subdir is created under it. std::string capDir; - // Rolling-shard size cap (MiB). Output is a small number of large packed - // .wfgcap containers, not thousands of loose files; a new shard opens when the - // current one passes this. WIN_FG_CAPTURE_SHARD_MB / conf capture_shard_mb. int capShardMB = 1024; void sanitize() { if (model < 3) model = 3; if (model > 4) model = 4; if (multiplier < 2) multiplier = 2; if (multiplier > 4) multiplier = 4; if (extraImages < 0) extraImages = 0; if (extraImages > 8) extraImages = 8; - if (perfPreset < 0 || perfPreset > 2) perfPreset = 1; // clamp out-of-range -> Balanced + if (perfPreset < 0 || perfPreset > 2) perfPreset = 1; if (gmMode < 0) gmMode = 0; if (gmMode > 2) gmMode = 2; if (frMode < 0) frMode = 0; if (frMode > 2) frMode = 2; if (frIters < 0) frIters = 0; if (frIters > 16) frIters = 16; @@ -158,7 +122,6 @@ struct Config { if (frEps < 1e-3f) frEps = 1e-3f; if (flowScale < 0.05f) flowScale = 0.05f; if (flowScale > 4.0f) flowScale = 4.0f; if (beta < 0.0f) beta = 0.0f; if (lambda < 0.0f) lambda = 0.0f; - // capture knobs if (captureW < 16) captureW = 16; if (captureW > 7680) captureW = 7680; if (captureH < 16) captureH = 16; if (captureH > 4320) captureH = 4320; if (capMode < 0) capMode = 0; if (capMode > 1) capMode = 1; @@ -176,7 +139,6 @@ static inline int envi(const char* k, int d) { const char* v = std::getenv(k); return v ? std::atoi(v) : d; } -// "auto"/"on"/"off" (also true/false/on/off numerics) -> {0 auto, 1 on, 2 off}. static inline int parse_tristate(const std::string& v, int dflt) { if (v.empty()) return dflt; if (v == "auto") return 0; @@ -185,19 +147,16 @@ static inline int parse_tristate(const std::string& v, int dflt) { return dflt; } -// "on"/"off"/"1"/"true"/... -> bool. static inline bool parse_bool(const std::string& v, bool dflt) { int t = parse_tristate(v, dflt ? 1 : 2); return t == 1; } -// "patch"|"frame" -> {0 patch, 1 frame}. static inline int parse_capmode(const std::string& v, int dflt) { if (v == "patch" || v == "patches" || v == "0") return 0; if (v == "frame" || v == "frames" || v == "1") return 1; return dflt; } -// key=value TOML-lite reader (flat keys, '#' comments) - enough for our knobs. static inline void apply_toml(Config& c, const std::string& path) { std::ifstream f(path); if (!f) return; @@ -229,7 +188,6 @@ static inline void apply_toml(Config& c, const std::string& path) { else if (k == "lambda") c.lambda = std::strtof(v.c_str(), nullptr); else if (k == "epsilon") c.epsilon = std::strtof(v.c_str(), nullptr); else if (k == "photoScale") c.photoScale = std::strtof(v.c_str(), nullptr); - // ── capture-mode knobs ──────────────────────────────────────────────── else if (k == "capture") c.capture = parse_bool(v, c.capture); else if (k == "capture_dir") c.capDir = v; else if (k == "capture_width") c.captureW = std::atoi(v.c_str()); @@ -240,7 +198,6 @@ static inline void apply_toml(Config& c, const std::string& path) { else if (k == "capture_motion") c.capMotion = std::strtof(v.c_str(), nullptr); else if (k == "capture_shard_mb") c.capShardMB = std::atoi(v.c_str()); else if (k == "hudRect") { - // "x0,y0,x1,y1" in pixel coords float r[4] = {0,0,0,0}; int n = 0; size_t start = 0; std::string s = v; for (; n < 4; ++n) { @@ -255,22 +212,18 @@ static inline void apply_toml(Config& c, const std::string& path) { } } -// Path of the conf.toml the app writes (used for hot-reload mtime checks). static inline std::string conf_path() { if (const char* p = std::getenv("WIN_FG_CONF")) return p; const char* home = std::getenv("HOME"); return home ? std::string(home) + "/.config/win-fg/conf.toml" : std::string(); } -// Resolve the capture output root: explicit override, else $HOME/.cache/winfg-capture, -// else a /tmp fallback so a HOME-less prefix still writes somewhere pullable. static inline std::string capture_root(const Config& c) { if (!c.capDir.empty()) return c.capDir; if (const char* home = std::getenv("HOME")) return std::string(home) + "/.cache/winfg-capture"; return std::string("/tmp/winfg-capture"); } -// env defaults first, then conf.toml overrides (file wins when present). static inline Config load_config() { Config c; c.enabled = envi("WIN_FG_ENABLE", 0) != 0; @@ -292,7 +245,6 @@ static inline Config load_config() { c.lambda = envf("WIN_FG_LAMBDA", c.lambda); c.epsilon = envf("WIN_FG_EPSILON", c.epsilon); c.photoScale = envf("WIN_FG_PHOTOSCALE", c.photoScale); - // ── capture-mode env (conf.toml still wins below) ───────────────────────── 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); @@ -303,7 +255,6 @@ static inline Config load_config() { 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")) { - // "x0,y0,x1,y1" - pixel coords; disabled when x0>=x1 or y0>=y1 float r[4] = {0,0,0,0}; int n = 0; std::string s = h; size_t start = 0; for (; n < 4; ++n) { @@ -321,37 +272,24 @@ static inline Config load_config() { return c; } -// UBO layout - MUST match wfg_synth.comp binding 0. -// The vec4 hudRect is 16-byte aligned in std140, sits at offset 32 (after the -// 8 leading floats which naturally occupy 32B). Disabled when hudX0>=hudX1. struct SynthUBO { float flowScale, alpha, beta, lambda, epsilon, photoScale, pad0, pad1; float hudX0, hudY0, hudX1, hudY1; }; -// UBO layout - MUST match of3_flow / of3_expand_m4 binding 0 (Model3UBO). struct FlowUBO { float flowScale; uint32_t level; float occlLo; float occlHi; }; -// C1 global-motion UBO - MUST match binding 1 in of3_gm_reduce / of3_gm_prewarp / -// of3_expand(_m4). std140: four vec4 = 64 bytes. The affine is in centered-UV -// space: P = [[p1,p3],[p2,p4]], translation (tx,ty); warp uvSrc = uv + P*(uv-0.5) -// + t maps a prev(template) coord to the curr(image) coord it samples (prev→curr). -// lin[] = running LK estimate (linearization point fed to of3_gm_reduce) -// app[] = APPLIED affine (of3_gm_prewarp + expand add-back); identity when -// disengaged so the pipeline is byte-identical to pre-C1. struct GMUBO { - float lin[4]; // p1,p2,p3,p4 - float linT[4]; // tx,ty, engaged, pad - float app[4]; // p1,p2,p3,p4 - float appT[4]; // tx,ty, pad, pad + float lin[4]; + float linT[4]; + float app[4]; + float appT[4]; }; -// C2 flow-regularization UBO - MUST match binding 0 (vec4 params) in -// of3_flowreg.comp. std140: one vec4 = 16 bytes. See Config::fr* fields. struct FlowRegUBO { - float dt; // params.x - semi-implicit smoothing step - float lambda; // params.y - L1 data-fidelity weight (θ = dt·lambda) - float edgeAlpha; // params.z - luma-gradient edge sensitivity for g - float epsTV; // params.w - Charbonnier epsilon (px) for the TV diffusivity + float dt; + float lambda; + float edgeAlpha; + float epsTV; }; -} // namespace winfg +} diff --git a/app/src/main/cpp/framegen/winfg/embedded_shaders.hpp b/app/src/main/cpp/framegen/winfg/embedded_shaders.hpp index 934d07a61..20dd4477c 100644 --- a/app/src/main/cpp/framegen/winfg/embedded_shaders.hpp +++ b/app/src/main/cpp/framegen/winfg/embedded_shaders.hpp @@ -1,5 +1,3 @@ -// GENERATED by tools/embed_spv.py - do not edit by hand. -// win-fg embedded SPIR-V: our optical-flow front end + synthesis only. #pragma once #include #include @@ -41,4 +39,4 @@ static inline Blob blob(Shader s) { default: return { nullptr, 0 }; } } -} } // namespace winfg::embedded +} } diff --git a/app/src/main/cpp/framegen/winfg/framegen.cpp b/app/src/main/cpp/framegen/winfg/framegen.cpp index 2848937d5..4ddd12021 100644 --- a/app/src/main/cpp/framegen/winfg/framegen.cpp +++ b/app/src/main/cpp/framegen/winfg/framegen.cpp @@ -1,4 +1,3 @@ -// win-fg - compute frame-generation engine implementation. #include "framegen.hpp" #include "embedded_shaders.hpp" #include "log.hpp" @@ -49,10 +48,6 @@ void FrameGen::destroyImage(Img& i) { i = Img{}; } -// C1: allocate the per-slot host-visible SSBOs the LK reduce writes partials into. -// Host-visible + coherent so the CPU can read them back after the owning frame's -// fence signals (no staging copy needed; the buffers are tiny - kGmThreads*kGmStride -// floats each, ~64 KB). Persistently mapped. bool FrameGen::makeGmBuffers() { const VkDeviceSize sz = (VkDeviceSize)kGmThreads * kGmStride * sizeof(float); const VkMemoryPropertyFlags want = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; @@ -75,8 +70,6 @@ bool FrameGen::makeGmBuffers() { bool FrameGen::makePipe(Pipe& p, int shaderId, const std::vector&) { - // binding layout is derived per shader below in init(); here we only build - // the module + a generic layout supplied by caller via setLayout already set. embedded::Blob b = embedded::blob((embedded::Shader)shaderId); VkShaderModuleCreateInfo mi{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; mi.codeSize = b.words * 4; mi.pCode = b.code; @@ -101,7 +94,6 @@ void FrameGen::destroyPipe(Pipe& p) { p = Pipe{}; } -// Build a descriptor-set layout from an explicit (binding, type) list. static bool buildSetLayout(const DeviceDispatch* dd, VkDevice dev, const std::vector>& binds, VkDescriptorSetLayout& out) { @@ -123,12 +115,11 @@ bool FrameGen::init(const DeviceDispatch* dd, const InstanceDispatch* id, si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; VKOK(dd_->CreateSampler(device_, &si, nullptr, &linear_)); - // descriptor pool sized generously for our per-frame sets. VkDescriptorPoolSize sizes[] = { {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 256}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 256}, {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 64}, - {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 16}, // C1 LK-reduce SSBOs + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 16}, }; VkDescriptorPoolCreateInfo pci{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; pci.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; @@ -141,19 +132,15 @@ bool FrameGen::init(const DeviceDispatch* dd, const InstanceDispatch* id, const auto U = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; const auto SB = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - // per-shader binding sets (match the GLSL layout(binding=...) declarations) if (!buildSetLayout(dd_, dev, {B{32,S},B{48,W}}, pLuma_.setLayout)) return false; if (!buildSetLayout(dd_, dev, {B{32,S},B{48,W}}, pDown_.setLayout)) return false; if (!buildSetLayout(dd_, dev, {B{0,U},B{32,S},B{33,S},B{34,S},B{48,W}}, pFlow_.setLayout)) return false; if (!buildSetLayout(dd_, dev, {B{0,U},B{32,S},B{33,S},B{34,S},B{48,W}}, pFlowM4_.setLayout)) return false; - // C1: expand sets gain a second UBO (binding 1 = GMUBO) for the global-motion add-back. if (!buildSetLayout(dd_, dev, {B{0,U},B{1,U},B{32,S},B{48,W},B{49,W}}, pExpand_.setLayout)) return false; if (!buildSetLayout(dd_, dev, {B{0,U},B{1,U},B{32,S},B{48,W},B{49,W}}, pExpandM4_.setLayout)) return false; if (!buildSetLayout(dd_, dev, {B{0,U},B{32,S},B{33,S},B{34,S},B{35,S},B{48,W}}, pSynth_.setLayout)) return false; - // C1: LK-reduce (UBO, SSBO, prev, curr) and curr-luma stabilization prewarp (UBO, src, dst). if (!buildSetLayout(dd_, dev, {B{1,U},B{16,SB},B{32,S},B{33,S}}, pGmReduce_.setLayout)) return false; if (!buildSetLayout(dd_, dev, {B{1,U},B{32,S},B{48,W}}, pGmPrewarp_.setLayout)) return false; - // C2: flow-reg (UBO, u_in, f0_obs, luma, out). if (!buildSetLayout(dd_, dev, {B{0,U},B{32,S},B{33,S},B{34,S},B{48,W}}, pFlowReg_.setLayout)) return false; std::vector unused; @@ -177,23 +164,19 @@ static VkExtent2D levelExtent(VkExtent2D e, int l) { return { e.width >> l ? e.width >> l : 1u, e.height >> l ? e.height >> l : 1u }; } -bool FrameGen::onResize(VkExtent2D extent, VkFormat /*colorFormat*/, bool force) { - // force skips the same-extent early-out so a perf_preset change (which alters - // flowFinest_ and therefore the per-size flow-image resolutions) rebuilds them. +bool FrameGen::onResize(VkExtent2D extent, VkFormat , bool force) { + + if (!force && ready_ && extent.width == extent_.width && extent.height == extent_.height) return true; - // tear down previous size for (auto& i : pyrA_) destroyImage(i); pyrA_.clear(); for (auto& i : pyrB_) destroyImage(i); pyrB_.clear(); for (auto& i : pyrAs_) destroyImage(i); pyrAs_.clear(); for (auto& i : flowLvl_) destroyImage(i); flowLvl_.clear(); destroyImage(flowExpA_); destroyImage(flowExpB_); - destroyImage(flowRegA_); destroyImage(flowRegB_); // C2 + destroyImage(flowRegA_); destroyImage(flowRegB_); extent_ = extent; ready_ = false; const VkImageUsageFlags lumaUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT; - // flowLvl_ is cleared every frame (per-frame reset) and C2 copies the cleaned - // finest level back into it - both need TRANSFER_DST. The C2 ping-pong images - // are the copy SOURCE (TRANSFER_SRC). const VkImageUsageFlags flowUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; const VkImageUsageFlags regUsage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; pyrA_.resize(kLevels); pyrB_.resize(kLevels); flowLvl_.resize(kLevels); @@ -203,13 +186,11 @@ bool FrameGen::onResize(VkExtent2D extent, VkFormat /*colorFormat*/, bool force) if (!makeImage(pyrB_[l], e, VK_FORMAT_R32_SFLOAT, lumaUsage)) return false; if (!makeImage(flowLvl_[l], e, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false; } - // C1: stabilized curr luma - only the levels the dense flow reads (finest..coarsest). pyrAs_.resize(kLevels); for (int l = flowFinest_; l < kLevels; ++l) if (!makeImage(pyrAs_[l], levelExtent(extent, l), VK_FORMAT_R32_SFLOAT, lumaUsage)) return false; if (!makeImage(flowExpA_, extent, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false; if (!makeImage(flowExpB_, extent, VK_FORMAT_R16G16B16A16_SFLOAT, flowUsage)) return false; - // C2: TV-L1 ping-pong scratch at the finest solved flow level (perf_preset-driven res). 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; @@ -229,13 +210,6 @@ void FrameGen::barrier(VkCommandBuffer cmd, VkImage img, VkImageLayout from, VkI VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &b); } -// NOTE: descriptor-set allocation/update per record() and the exact per-level -// flow wiring are implemented in record_impl.inc for readability; the graph is: -// luma(curr)->pyrA[0], luma(prev)->pyrB[0] -// downsample pyrA/pyrB to kLevels -// coarse->fine flow (per level) into flowLvl -// expand flowLvl[0] -> flowExpA/flowExpB -// synth(prev,curr,flowExpA,flowExpB, alpha) -> out void FrameGen::destroy() { if (!device_) return; @@ -245,11 +219,11 @@ void FrameGen::destroy() { for (auto& i : pyrAs_) destroyImage(i); pyrAs_.clear(); for (auto& i : flowLvl_) destroyImage(i); flowLvl_.clear(); destroyImage(flowExpA_); destroyImage(flowExpB_); - destroyImage(flowRegA_); destroyImage(flowRegB_); // C2 + destroyImage(flowRegA_); destroyImage(flowRegB_); destroyPipe(pLuma_); destroyPipe(pDown_); destroyPipe(pFlow_); destroyPipe(pFlowM4_); destroyPipe(pExpand_); destroyPipe(pExpandM4_); destroyPipe(pSynth_); destroyPipe(pGmReduce_); destroyPipe(pGmPrewarp_); - destroyPipe(pFlowReg_); // C2 + destroyPipe(pFlowReg_); for (int s = 0; s < kGmSlots; ++s) { if (gmSsboMem_[s]) dd_->UnmapMemory(device_, gmSsboMem_[s]); if (gmSsbo_[s]) dd_->DestroyBuffer(device_, gmSsbo_[s], nullptr); @@ -262,8 +236,6 @@ void FrameGen::destroy() { descPool_ = VK_NULL_HANDLE; linear_ = VK_NULL_HANDLE; device_ = VK_NULL_HANDLE; ready_ = false; } -} // namespace winfg +} -// record() + its scratch helpers reopen namespace winfg themselves, so include -// this OUTSIDE the namespace block above (keeps FrameGen:: members attached). #include "record_impl.inc" diff --git a/app/src/main/cpp/framegen/winfg/framegen.hpp b/app/src/main/cpp/framegen/winfg/framegen.hpp index 1980f1104..8b32fd0c0 100644 --- a/app/src/main/cpp/framegen/winfg/framegen.hpp +++ b/app/src/main/cpp/framegen/winfg/framegen.hpp @@ -1,8 +1,3 @@ -// win-fg - compute frame-generation engine. -// Owns the compute pipelines (our of3_* optical flow + wfg_synth) and the -// per-resolution intermediate images, and records the flow+synth graph that -// turns (prev, curr) into an interpolated frame. Written for win-fg; no code -// from bionic-fg / lsfg / GameScope. #pragma once #include "vk_dispatch.hpp" #include "config.hpp" @@ -14,70 +9,53 @@ class FrameGen { public: bool init(const DeviceDispatch* dd, const InstanceDispatch* id, VkPhysicalDevice phys, VkDevice dev, uint32_t queueFamily, VkQueue queue); - // Applies a new config. Recomputes the active finest flow level from - // cfg_.perfPreset; if it changed while resources are live, safely rebuilds the - // affected per-size flow images (like a resize) and resets the flow predictor, - // so a perf_preset change is FULLY LIVE - no FG toggle needed. Defined in - // record_impl.inc (needs the scratch-state teardown). See flowFinestForPreset. void configure(const Config& c); - // (Re)build per-resolution resources for a swapchain of the given size/format. - // force=true rebuilds even when the extent is unchanged (used by configure() - // when a perf_preset change alters the finest solved flow level). bool onResize(VkExtent2D extent, VkFormat colorFormat, bool force = false); - // Record the whole flow+synth graph into cmd. prevView/currView are SAMPLED - // views of the two real frames; outView is a STORAGE view of the target - // image the generated frame is written into. alpha in (0,1). - // gmSlot selects which C1 global-motion SSBO to reduce into / read back; the - // caller MUST pass the ring-slot index of the FrameCtx it just fence-waited - // (that guarantees this slot's PREVIOUS reduce has completed and is safe to - // read on the host - see the decoupled-LK note in record_impl.inc). void record(VkCommandBuffer cmd, VkImageView prevView, VkImageView currView, 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_; } - // public so the record_impl.inc free helpers can reference them struct Img { VkImage image = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; VkExtent2D extent{}; VkFormat fmt{}; }; struct Pipe { VkPipeline pipeline = VK_NULL_HANDLE; VkPipelineLayout layout = VK_NULL_HANDLE; VkDescriptorSetLayout setLayout = VK_NULL_HANDLE; VkShaderModule module = VK_NULL_HANDLE; }; - // ── C1 GLOBAL-MOTION PRE-WARP constants (public so record_impl.inc's free - // helpers can size/index the SSBO pool). See of3_gm_reduce/of3_gm_prewarp. ── - static constexpr int kGmSlots = 3; // >= present ring size; one SSBO per in-flight ctx - static constexpr int kGmLevel = 3; // 1/8-res luma pyramid level for the LK estimate - static constexpr int kGmThreads = 512; // fixed reduce threads (8 groups * local_size_x 64) - static constexpr int kGmStride = 32; // floats/thread: 21 H + 6 b + coverage + resid + pad - // Finest pyramid level the dense flow is solved at (0 = full res). This is now - // RUNTIME-SELECTABLE via conf.toml perf_preset (0 Quality=1 / 1 Balanced=2 / - // 2 Performance=3). kFlowFinestDefault is the Balanced value (today's behaviour); - // the ACTIVE level lives in flowFinest_ and is read through flowFinest(). onResize - // sizes the stabilized-curr pyramid + C2 scratch from it, and record_impl.inc - // wires the flow/expand inputs to it - both must agree, so they all read - // flowFinest()/flowFinest_ (never a hard-coded 2). (Was a static constexpr = 2.) - static constexpr int kFlowFinestDefault = 2; // Balanced (perf_preset=1) - // Map perf_preset -> finest solved level. Out-of-range -> Balanced. A higher - // level = coarser (cheaper) flow = smaller base-FPS drop. configure() clamps the - // result against kLevels so at least two solved levels always remain. + + + static constexpr int kGmSlots = 3; + static constexpr int kGmLevel = 3; + static constexpr int kGmThreads = 512; + static constexpr int kGmStride = 32; + + + + + + + + static constexpr int kFlowFinestDefault = 2; + + + static int flowFinestForPreset(int preset) { switch (preset) { - case 0: return 1; // Quality ~1/2-res - case 2: return 3; // Performance ~1/8-res - case 1: default: return kFlowFinestDefault; // Balanced ~1/4-res (today) + case 0: return 1; + case 2: return 3; + case 1: default: return kFlowFinestDefault; } } - // The ACTIVE finest solved flow level (set by configure() from perf_preset). int flowFinest() const { return flowFinest_; } private: uint32_t findMemType(uint32_t bits, VkMemoryPropertyFlags props) const; bool makeImage(Img& out, VkExtent2D ext, VkFormat fmt, VkImageUsageFlags usage); void destroyImage(Img& i); - bool makeGmBuffers(); // allocate the kGmSlots host-visible LK-reduce SSBOs - // Read back one reduce SSBO, solve the 6x6 inverse-compositional LK step, and - // update the running/applied global affine. Returns true if it engaged this - // frame. Implemented in record_impl.inc (has the ScratchState/UBO context). + bool makeGmBuffers(); + + + bool updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent); bool makePipe(Pipe& p, int shaderId, const std::vector& bindings); @@ -97,55 +75,36 @@ private: Config cfg_; VkExtent2D extent_{}; bool ready_ = false; - // ACTIVE finest solved flow level (perf_preset). Default = Balanced (today's 2) - // so an unconfigured engine and an unset conf.toml are byte-identical to before. int flowFinest_ = kFlowFinestDefault; - // pipelines Pipe pLuma_, pDown_, pFlow_, pFlowM4_, pExpand_, pExpandM4_, pSynth_; - // C1: LK normal-equation reduce + curr-luma stabilization pre-warp Pipe pGmReduce_, pGmPrewarp_; - // C2: TV-L1 flow-regularization iteration (one dispatch per iteration) Pipe pFlowReg_; - // per-resolution resources - // 7 levels (was 5): the coarsest levels set the max motion the solver can track. - // At 5 levels the reach was only ~±84px/frame at 720p, so fast camera motion - // (150-300px/frame at the periphery) saturated the search -> incoherent vectors - // -> oil-paint melt. Two more (tiny) coarse levels push the reach to ~±300px so - // large global motion is captured coherently instead of smeared. Near-zero cost: - // the extra levels are 40x22 and 20x11 images. static const int kLevels = 7; - std::vector pyrA_, pyrB_; // luma pyramids for curr / prev - std::vector pyrAs_; // C1: STABILIZED curr luma (flow levels only) - std::vector flowLvl_; // per-level flow scratch - Img flowExpA_, flowExpB_; // fwd / bwd expanded flow (+conf) - // C2: TV-L1 ping-pong scratch at kFlowFinest res. flowLvl_[kFlowFinest] holds - // the fixed observation f0 (never written during the iterations); u ping-pongs - // between these two; the final iterate is copied back into flowLvl_[kFlowFinest] - // so of3_expand consumes the cleaned field with no binding change (⇒ skipping - // C2 leaves flowLvl_ byte-identical to pre-C2). + std::vector pyrA_, pyrB_; + std::vector pyrAs_; + std::vector flowLvl_; + Img flowExpA_, flowExpB_; + + + + + Img flowRegA_, flowRegB_; - // ── C1 global-motion state ──────────────────────────────────────────────── - // Host-visible SSBOs the reduce shader writes per-thread partials into; the - // CPU sums + solves them. One per ring slot so a readback never races a live - // submission (the caller fence-waited its slot before calling record()). VkBuffer gmSsbo_[kGmSlots] {}; VkDeviceMemory gmSsboMem_[kGmSlots]{}; void* gmSsboPtr_[kGmSlots]{}; bool gmBuffersReady_ = false; - // running affine params p1..p6 in centered-UV space (see GMUBO in config.hpp); - // identity = all zero. gmLinAt_ records the linearization params in effect when - // each slot's reduce was dispatched (the correct IC-update base for that slot). double gmRun_[6] {}; double gmLinAt_[kGmSlots][6] {}; bool gmSlotValid_[kGmSlots]{}; - bool gmHaveEst_ = false; // at least one good solve so far - int gmStableCount_ = 0; // consecutive good solves (auto-engage threshold) - bool gmEngaged_ = false; // is the affine being APPLIED this frame - float gmLastTransPx_ = 0.0f; // estimated global translation magnitude (full-res px), for logging + bool gmHaveEst_ = false; + int gmStableCount_ = 0; + bool gmEngaged_ = false; + float gmLastTransPx_ = 0.0f; unsigned long long gmFrames_ = 0; }; -} // namespace winfg +} diff --git a/app/src/main/cpp/framegen/winfg/log.hpp b/app/src/main/cpp/framegen/winfg/log.hpp index 77c7b8270..056f136f4 100644 --- a/app/src/main/cpp/framegen/winfg/log.hpp +++ b/app/src/main/cpp/framegen/winfg/log.hpp @@ -1,5 +1,3 @@ -// win-fg - logging. On Android goes to logcat (tag "win-fg") so device bring-up -// can trace the layer; elsewhere falls back to stderr. #pragma once #if defined(__ANDROID__) #include @@ -11,10 +9,4 @@ #define WFG_LOGE(...) do { std::fprintf(stderr, "[win-fg][E] " __VA_ARGS__); std::fprintf(stderr, "\n"); } while(0) #endif -// Runtime-gated granular debug trace. The gate is a RUNTIME bool (Config::debug), -// not a compile-time switch - a shipping .so carries the full trail but pays only -// a predicted-not-taken bool test per step when debug is off (no logcat write, no -// arg evaluation of the message). Same "win-fg" tag so the app captures it under -// its own UID. Used to step the present path so a freeze leaves an obvious last -// line at the stage that hung. Usage: WFG_LOGD(dbg, "P#%llu step ...", n); #define WFG_LOGD(cond, ...) do { if (cond) WFG_LOGI(__VA_ARGS__); } while(0) diff --git a/app/src/main/cpp/framegen/winfg/record_impl.inc b/app/src/main/cpp/framegen/winfg/record_impl.inc index dcf0696ce..2e8fba009 100644 --- a/app/src/main/cpp/framegen/winfg/record_impl.inc +++ b/app/src/main/cpp/framegen/winfg/record_impl.inc @@ -1,57 +1,28 @@ -// win-fg - FrameGen::record() and its lazy per-size descriptor/UBO setup. -// Included from framegen.cpp (has access to FrameGen privates). Written for -// win-fg; standard Vulkan compute wiring, no third-party code. -// -// ── C1 GLOBAL-MOTION PRE-WARP (this file wires it end to end) ───────────────── -// The dense SAD flow search melts on fast camera pans because the global motion -// saturates its window. C1 estimates the per-frame camera affine with a cheap -// inverse-compositional Lucas-Kanade solve (of3_gm_reduce accumulates the 6x6 -// normal equations at 1/8 res → CPU 6x6 Cholesky here), then of3_gm_prewarp -// removes it from the curr luma pyramid BEFORE the search, so the search only -// sees small, coherent, object-only residual motion. of3_expand(_m4) composes -// the global affine back so wfg_synth is unchanged and the final warp is correct. -// -// DECOUPLED (stall-free) SCHEDULING: a true in-frame CPU solve would need a -// mid-frame GPU submit+readback stall - unacceptable on a GPU-bound title. So we -// read back the reduce SSBO the caller ALREADY fence-waited (its ring slot from a -// couple of presents ago) and run ONE Gauss-Newton step per frame, warm-started -// from the running estimate. Camera motion is temporally smooth, so a ~1-2 frame -// old affine is an excellent predictor; and because the SAME affine is removed -// (prewarp) and added back (expand), the result is EXACT for any affine we pick - -// an approximate/stale one only changes how much residual the search must absorb, -// never correctness. Identity affine ⇒ byte-identical to the pre-C1 pipeline, so -// the bail-to-identity safety path can never be worse than "no pre-warp". namespace { struct ScratchState { - // host-visible UBO buffers VkBuffer synthUbo = VK_NULL_HANDLE; VkDeviceMemory synthUboMem = VK_NULL_HANDLE; void* synthUboPtr = nullptr; std::vector flowUbo; std::vector flowUboMem; std::vector flowUboPtr; VkBuffer expandUbo = VK_NULL_HANDLE; VkDeviceMemory expandUboMem = VK_NULL_HANDLE; void* expandUboPtr = nullptr; - // C2: flow-regularization UBO (shared by all iterations; refreshed per frame). VkBuffer flowRegUbo = VK_NULL_HANDLE; VkDeviceMemory flowRegUboMem = VK_NULL_HANDLE; void* flowRegUboPtr = nullptr; - // C1: per-slot global-motion UBO (double-buffered by ring slot so a live GPU - // read is never overwritten by the next frame - see the header note). VkBuffer gmUbo[winfg::FrameGen::kGmSlots]{}; VkDeviceMemory gmUboMem[winfg::FrameGen::kGmSlots]{}; void* gmUboPtr[winfg::FrameGen::kGmSlots]{}; - // persistent descriptor sets VkDescriptorSet sLumaCurr = VK_NULL_HANDLE, sLumaPrev = VK_NULL_HANDLE; std::vector sDownA, sDownB, sFlow; - VkDescriptorSet sExpand[winfg::FrameGen::kGmSlots]{}; // per-slot (binds gmUbo[slot]) - // C2: three flow-reg sets covering every (u_in → u_out) pairing of the - // ping-pong. sFlowReg0: in=flowLvl_[F] (=f0), out=A. sFlowRegAB: in=A, out=B. - // sFlowRegBA: in=B, out=A. f0 (binding 33) = flowLvl_[F] in all three. + VkDescriptorSet sExpand[winfg::FrameGen::kGmSlots]{}; + + + VkDescriptorSet sFlowReg0 = VK_NULL_HANDLE, sFlowRegAB = VK_NULL_HANDLE, sFlowRegBA = VK_NULL_HANDLE; VkDescriptorSet sSynth = VK_NULL_HANDLE; - // C1: per-slot reduce set + per-slot per-level prewarp sets VkDescriptorSet sGmReduce[winfg::FrameGen::kGmSlots]{}; - std::vector sGmPrewarp[winfg::FrameGen::kGmSlots]; // sized kLevels; [flowFinest()..) used + std::vector sGmPrewarp[winfg::FrameGen::kGmSlots]; VkExtent2D builtFor{0,0}; bool built = false; - bool flowInited = false; // flow images laid out once, then persist (temporal predictor) + bool flowInited = false; }; -ScratchState g_scratch; // one swapchain in flight in this first-draft engine +ScratchState g_scratch; } static bool makeUbo(const winfg::DeviceDispatch* dd, VkDevice dev, @@ -77,7 +48,6 @@ static VkDescriptorSet allocSet(const winfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet s = VK_NULL_HANDLE; dd->AllocateDescriptorSets(dev, &ai, &s); return s; } -// write helpers 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; @@ -102,9 +72,6 @@ static void wSsbo(const winfg::DeviceDispatch* dd, VkDevice dev, VkDescriptorSet dd->UpdateDescriptorSets(dev, 1, &w, 0, nullptr); } -// ── C1 CPU-side linear algebra (clean-room, textbook) ──────────────────────── -// 6x6 symmetric positive-definite solve via Cholesky. Returns false if not PD -// (a bad/ill-conditioned frame → caller bails to identity). Damped by the caller. static bool cholSolve6(double A[6][6], const double b[6], double x[6]) { double L[6][6] = {{0}}; for (int i = 0; i < 6; ++i) { @@ -112,7 +79,7 @@ static bool cholSolve6(double A[6][6], const double b[6], double x[6]) { double s = A[i][j]; for (int k = 0; k < j; ++k) s -= L[i][k] * L[j][k]; if (i == j) { - if (s <= 1e-20) return false; // not positive-definite + if (s <= 1e-20) return false; L[i][j] = std::sqrt(s); } else { L[i][j] = s / L[j][j]; @@ -124,7 +91,6 @@ static bool cholSolve6(double A[6][6], const double b[6], double x[6]) { for (int i = 5; i >= 0; --i) { double s = y[i]; for (int k = i+1; k < 6; ++k) s -= L[k][i]*x[k]; x[i] = s / L[i][i]; } return true; } -// affine <-> params. params p=(p1,p2,p3,p4,p5,p6); M = I + [[p1,p3,p5],[p2,p4,p6]]. static void affFromParams(const double p[6], double M[3][3]) { M[0][0]=1.0+p[0]; M[0][1]=p[2]; M[0][2]=p[4]; M[1][0]=p[1]; M[1][1]=1.0+p[3]; M[1][2]=p[5]; @@ -153,8 +119,6 @@ static void mulAff(const double A[3][3], const double B[3][3], double out[3][3]) namespace winfg { -// Lazily allocate per-size UBOs + descriptor sets and wire the fixed (internal- -// resource) bindings once. External views (prev/curr/out) are wired per frame. static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, const VkPhysicalDeviceMemoryProperties& mp, VkDescriptorPool pool, VkSampler samp, VkExtent2D ext, @@ -168,11 +132,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, FrameGen::Img& flowRegA, FrameGen::Img& flowRegB, VkBuffer* gmSsbo, int kLevels, const Config& cfg); -// Tear down the lazily-built scratch (all UBO buffers + every descriptor set) so a -// live perf_preset rebuild neither leaks them nor exhausts the descriptor pool. -// The CALLER must have made the device idle first - no in-flight command buffer may -// still reference these. Resetting the pool frees every set FrameGen allocated (all -// sets come from this pool); the mapped UBO memory is unmapped + freed explicitly. static void destroyScratch(const DeviceDispatch* dd, VkDevice dev, VkDescriptorPool pool) { auto freeUbo = [&](VkBuffer& b, VkDeviceMemory& m, void*& p) { if (m) dd->UnmapMemory(dev, m); @@ -186,29 +145,20 @@ static void destroyScratch(const DeviceDispatch* dd, VkDevice dev, VkDescriptorP 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]); for (int s = 0; s < FrameGen::kGmSlots; ++s) freeUbo(S.gmUbo[s], S.gmUboMem[s], S.gmUboPtr[s]); - if (pool && dd->ResetDescriptorPool) dd->ResetDescriptorPool(dev, pool, 0); // reclaim all sets at once - S = ScratchState{}; // built=false -> next record() rebuilds via ensureScratch + if (pool && dd->ResetDescriptorPool) dd->ResetDescriptorPool(dev, pool, 0); + S = ScratchState{}; } -// ── perf_preset apply / live rebuild ───────────────────────────────────────── -// Sets the config, resolves the perf_preset -> finest-solved-level, and - when -// that level changed while resources are already live - safely rebuilds the -// affected per-size flow images (a resize) + scratch descriptor sets and resets -// the flow predictor. This makes a perf_preset change FULLY LIVE per-present (the -// conf.toml hot-reload path calls this), so no FG toggle / bg-fg cycle is needed. void FrameGen::configure(const Config& c) { cfg_ = c; cfg_.sanitize(); - // Map preset -> finest level, clamped so at least TWO solved levels always - // remain (coarse->fine needs a range). With kLevels=7 the presets (1/2/3) never - // hit the clamp; the guard is defensive for future level/preset tuning. int want = flowFinestForPreset(cfg_.perfPreset); - const int maxFinest = kLevels - 2; // keep >= 2 solved levels + const int maxFinest = kLevels - 2; if (want > maxFinest) { 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; } - if (want < 1) want = 1; // never solve full-res (level 0) + if (want < 1) want = 1; const int prevFinest = flowFinest_; flowFinest_ = want; @@ -216,35 +166,27 @@ void FrameGen::configure(const Config& c) { 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; // unchanged, or not yet built (initial configure before onResize) + if (!live) return; - // The per-size flow images (pyrAs_/flowReg*/flowLvl_ wiring) and the scratch - // descriptor sets were built for the OLD finest level. Rebuild them so record() - // never reads/writes mismatched-size images. - dd_->DeviceWaitIdle(device_); // no in-flight cmd buf may touch these - destroyScratch(dd_, device_, descPool_); // free old scratch (UBOs + all sets) - if (!onResize(extent_, VK_FORMAT_UNDEFINED, /*force=*/true)) + + + + dd_->DeviceWaitIdle(device_); + destroyScratch(dd_, device_, descPool_); + if (!onResize(extent_, VK_FORMAT_UNDEFINED, true)) WFG_LOGE("perf-preset live rebuild: onResize FAILED at %ux%u - FG disabled until next resize", extent_.width, extent_.height); - // Reset the persistent flow predictor so stale, differently-scaled state never - // feeds the new resolution (avoids a melt/garbage frame on switch). The per-frame - // flow field (flowLvl_) is already cleared every frame; the cross-frame state is - // the C1 global-motion estimate + its per-slot reduce partials (dispatched against - // the old pyramid) - drop both back to identity. for (int i = 0; i < 6; ++i) gmRun_[i] = 0.0; for (int s = 0; s < kGmSlots; ++s) { gmSlotValid_[s] = false; for (int i = 0; i < 6; ++i) gmLinAt_[s][i] = 0.0; } gmHaveEst_ = false; gmEngaged_ = false; gmStableCount_ = 0; gmLastTransPx_ = 0.0f; WFG_LOGI("perf-preset live rebuild DONE (kFlowFinest=%d, flow predictor reset)", flowFinest_); } -// ── C1: read back one reduce SSBO, run one inverse-compositional LK step ────── -// Updates the running/applied global affine. Returns whether GM engages this frame. bool FrameGen::updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent) { ++gmFrames_; - if (!gmBuffersReady_ || cfg_.gmMode == 2 /*off*/) { - // off: keep identity applied; keep estimating cheaply is pointless when off. + if (!gmBuffersReady_ || cfg_.gmMode == 2 ) { + gmEngaged_ = false; gmStableCount_ = 0; - // still refresh the linearization record so a live switch to auto starts clean for (int i = 0; i < 6; ++i) gmRun_[i] = 0.0; for (int i = 0; i < 6; ++i) gmLinAt_[gmSlot][i] = gmRun_[i]; gmLastTransPx_ = 0.0f; @@ -260,31 +202,25 @@ bool FrameGen::updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent) { for (int k = 0; k < kGmStride; ++k) sums[k] += (double)row[k]; } double coverage = sums[27]; - // require a few percent of the level's pixels as inliers double lvlpx = double((fullExtent.width >> kGmLevel) * (fullExtent.height >> kGmLevel)); double minCov = 0.03 * (lvlpx > 0 ? lvlpx : 1.0); if (coverage > minCov && coverage > 64.0) { double H[6][6]; int idx = 0; for (int r = 0; r < 6; ++r) for (int c = r; c < 6; ++c) { H[r][c] = H[c][r] = sums[idx]; ++idx; } double b[6]; for (int i = 0; i < 6; ++i) b[i] = sums[21 + i]; - // Levenberg damping for conditioning (relative + tiny absolute). for (int i = 0; i < 6; ++i) H[i][i] += 1e-3 * H[i][i] + 1e-9; double dp[6]; if (cholSolve6(H, b, dp)) { - // reject wild steps (something non-affine / a scene cut) bool bounded = std::fabs(dp[0]) < 0.5 && std::fabs(dp[1]) < 0.5 && std::fabs(dp[2]) < 0.5 && std::fabs(dp[3]) < 0.5 && std::fabs(dp[4]) < 0.25 && std::fabs(dp[5]) < 0.25; if (bounded) { - // IC update: M_new = M_lin ∘ W(dp)^-1 (compose onto the SLOT's - // linearization base, which is what this reduce linearized around). double Mlin[3][3], Md[3][3], Mdi[3][3], Mnew[3][3]; affFromParams(gmLinAt_[gmSlot], Mlin); affFromParams(dp, Md); if (invAff(Md, Mdi)) { mulAff(Mlin, Mdi, Mnew); double pnew[6]; paramsFromAff(Mnew, pnew); - // final sanity clamp on the composed estimate bool sane = std::fabs(pnew[0]) < 0.6 && std::fabs(pnew[1]) < 0.6 && std::fabs(pnew[2]) < 0.6 && std::fabs(pnew[3]) < 0.6 && std::fabs(pnew[4]) < 0.5 && std::fabs(pnew[5]) < 0.5; @@ -297,17 +233,14 @@ bool FrameGen::updateGlobalMotion(uint32_t gmSlot, VkExtent2D fullExtent) { if (good) ++gmStableCount_; else gmStableCount_ = 0; - // engage policy switch (cfg_.gmMode) { - case 1: gmEngaged_ = gmHaveEst_; break; // on - case 2: gmEngaged_ = false; break; // off (unreachable; handled above) - default: gmEngaged_ = (gmStableCount_ >= 3); break; // auto: 3 consecutive good solves + case 1: gmEngaged_ = gmHaveEst_; break; + case 2: gmEngaged_ = false; break; + default: gmEngaged_ = (gmStableCount_ >= 3); break; } - // record the linearization base for the reduce we are about to dispatch into - // this slot this frame (i.e. the current running estimate). for (int i = 0; i < 6; ++i) gmLinAt_[gmSlot][i] = gmRun_[i]; - gmSlotValid_[gmSlot] = true; // this frame's reduce will (re)write it + gmSlotValid_[gmSlot] = true; gmLastTransPx_ = (float)std::sqrt( (gmRun_[4]*fullExtent.width) * (gmRun_[4]*fullExtent.width) + @@ -330,9 +263,7 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur pFlowReg_, flowRegA_, flowRegB_, gmSsbo_, kLevels, cfg_); } - // ── C1: solve the global-motion affine (reads the SSBO the caller fence-waited) ── bool gmEngaged = updateGlobalMotion(gmSlot, extent_); - // Build this slot's GMUBO from the running / applied affine. 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]; @@ -340,10 +271,9 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur 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]; - } // else app/appT stay 0 -> identity -> prewarp/expand are pre-C1 no-ops + } std::memcpy(g_scratch.gmUboPtr[gmSlot], &g, sizeof(g)); } - // Log engage state + estimated global translation periodically (tag win-fg). { static int lastEng = -1; if (gmEngaged != (lastEng == 1) || (gmFrames_ % 300ull) == 0) { @@ -353,12 +283,10 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur } } - // update UBOs for this frame SynthUBO su{cfg_.flowScale, alpha, cfg_.beta, cfg_.lambda, cfg_.epsilon, cfg_.photoScale, 0, 0, cfg_.hudX0, cfg_.hudY0, cfg_.hudX1, cfg_.hudY1}; if (g_scratch.synthUboPtr) std::memcpy(g_scratch.synthUboPtr, &su, sizeof(su)); - // wire per-frame external views 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); @@ -372,23 +300,13 @@ 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); }; - // Internal images are created UNDEFINED and fully recomputed every frame, so - // discard-transition them all to GENERAL up front (all descriptors use GENERAL - // for these). This also fixes the first-frame layout (storage writes require - // GENERAL, not UNDEFINED). 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); - // C2: ping-pong scratch to GENERAL (storage writes require it; contents discarded). barrier(cmd, flowRegA_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL); barrier(cmd, flowRegB_.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL); - // Reset the flow field to zero every frame. The coarsest level uses it as its - // search predictor; carrying it across frames lets a spurious vector feed - // forward until it diverges and melts the image (the bg/fg pause was resetting - // it - that's why artifacts vanished on resume). Fresh zero every frame = - // stable, standard pyramid flow, no feedback loop. { VkClearColorValue zero{}; VkImageSubresourceRange rng{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; @@ -406,20 +324,14 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur } } - // 1) luma of curr/prev at full res disp(pLuma_, g_scratch.sLumaCurr, levelExtent(extent_, 0)); disp(pLuma_, g_scratch.sLumaPrev, levelExtent(extent_, 0)); bar(pyrA_[0]); bar(pyrB_[0]); - // 2) downsample pyramids 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]); } - // 2b) C1: LK normal-equation reduce (fixed grid, 1/8-res) → this slot's SSBO. - // Reads raw prev/curr luma at kGmLevel; writes per-thread partials. A - // compute→host barrier makes the write visible to the CPU readback that - // happens when this slot comes round again (~1-2 frames later). 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); @@ -429,28 +341,15 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &mb, 0, nullptr, 0, nullptr); } - // 2c) C1: pre-warp curr luma pyramid by the applied affine → stabilized pyramid - // (identity affine ⇒ exact copy, so the flow below is unchanged when - // disengaged). The dense flow reads pyrAs from here on. for (int l = flowFinest_; l < kLevels; ++l) { disp(pGmPrewarp_, g_scratch.sGmPrewarp[gmSlot][l], levelExtent(extent_, l)); bar(pyrAs_[l]); } - // 3) coarse->fine flow (only down to flowFinest_ - reduced res for perf; the - // finest level is perf_preset-selectable). Curr input is now the STABILIZED - // pyramid (pyrAs), wired in ensureScratch. for (int l = kLevels - 1; l >= flowFinest_; --l) { disp(flowP, g_scratch.sFlow[l], levelExtent(extent_, l)); bar(flowLvl_[l]); } - // 3b) ── C2: TV-L1 flow regularization on the finest residual field ────────── - // Run frIters semi-implicit TV-L1 denoising iterations on flowLvl_[flowFinest_] - // (the object-only residual left by C1). flowLvl_[F] is the fixed data term f0 - // (read, never written here); u ping-pongs A<->B; the final iterate is copied - // back into flowLvl_[F] so of3_expand consumes the cleaned field unchanged. - // Disengaged (frMode==off or frIters==0) ⇒ flowLvl_[F] untouched ⇒ byte-identical - // to pre-C2. Identity/zero iterations is therefore never worse than "no reg". const bool frEngaged = (cfg_.frMode != 2) && (cfg_.frIters > 0); if (frEngaged) { const int F = flowFinest_; @@ -471,9 +370,6 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur bar(*outImg); last = outImg; } - // Copy the cleaned finest flow back into flowLvl_[F] (GENERAL->GENERAL). - // Barrier: C2's f0 reads (SHADER_READ of flowLvl_[F]) and the last iterate - // write must complete before the transfer overwrites/reads. 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; @@ -491,7 +387,7 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur dd_->CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &mb2, 0, nullptr, 0, nullptr); } - { // periodic C2 status (tag win-fg), mirrors the C1 log cadence + { static int lastFr = -1; int fe = frEngaged ? 1 : 0; if (fe != lastFr || (gmFrames_ % 300ull) == 0) { @@ -501,14 +397,11 @@ void FrameGen::record(VkCommandBuffer cmd, VkImageView prevView, VkImageView cur } } - // 4) expand finest solved flow -> fwd/bwd (+conf), + C1 global-motion add-back. disp(expandP, g_scratch.sExpand[gmSlot], levelExtent(extent_, 0)); bar(flowExpA_); bar(flowExpB_); - // 5) synthesise interpolated frame disp(pSynth_, g_scratch.sSynth, levelExtent(extent_, 0)); } -// ---- lazy scratch builder ---- static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, const VkPhysicalDeviceMemoryProperties& mp, VkDescriptorPool pool, VkSampler samp, VkExtent2D ext, @@ -528,7 +421,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, 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)); } - // C2: flow-reg UBO (params refreshed each frame in record() for hot-reload). makeUbo(dd, dev, mp, sizeof(FlowRegUBO), S.flowRegUbo, S.flowRegUboMem, S.flowRegUboPtr); if (S.flowRegUboPtr) { FlowRegUBO fu{cfg.frDt, cfg.frLambda, cfg.frEdge, cfg.frEps}; std::memcpy(S.flowRegUboPtr, &fu, sizeof(fu)); } S.flowUbo.resize(kLevels); S.flowUboMem.resize(kLevels); S.flowUboPtr.resize(kLevels); @@ -536,19 +428,16 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, makeUbo(dd, dev, mp, sizeof(FlowUBO), S.flowUbo[l], S.flowUboMem[l], S.flowUboPtr[l]); if (S.flowUboPtr[l]) { FlowUBO u{cfg.flowScale, (uint32_t)l, 0.6f, 1.6f}; std::memcpy(S.flowUboPtr[l], &u, sizeof(u)); } } - // C1: per-slot GMUBO (initialised to identity) for (int s = 0; s < FrameGen::kGmSlots; ++s) { makeUbo(dd, dev, mp, sizeof(GMUBO), S.gmUbo[s], S.gmUboMem[s], S.gmUboPtr[s]); if (S.gmUboPtr[s]) { GMUBO g{}; std::memcpy(S.gmUboPtr[s], &g, sizeof(g)); } } - // luma sets (external src wired per frame; dst is pyramid[0]) S.sLumaCurr = allocSet(dd, dev, pool, pLuma.setLayout); S.sLumaPrev = allocSet(dd, dev, pool, pLuma.setLayout); wImg(dd, dev, S.sLumaCurr, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, pyrA[0].view, VK_NULL_HANDLE, GEN); wImg(dd, dev, S.sLumaPrev, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, pyrB[0].view, VK_NULL_HANDLE, GEN); - // downsample sets (fixed) S.sDownA.resize(kLevels - 1); S.sDownB.resize(kLevels - 1); for (int l = 1; l < kLevels; ++l) { S.sDownA[l-1] = allocSet(dd, dev, pool, pDown.setLayout); @@ -559,10 +448,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, wImg(dd, dev, S.sDownB[l-1], 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, pyrB[l].view, VK_NULL_HANDLE, GEN); } - // flow sets (fixed): predictor = coarser flow level, or self at coarsest. - // C1: the CURR input (binding 33) is the STABILIZED pyramid pyrAs for the - // solved flow levels; prev stays raw. Below kFlowFinest the level is never - // dispatched, so leaving those at raw curr is harmless. S.sFlow.resize(kLevels); for (int l = 0; l < kLevels; ++l) { S.sFlow[l] = allocSet(dd, dev, pool, pFlow.setLayout); @@ -575,19 +460,17 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, wImg(dd, dev, S.sFlow[l], 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowLvl[l].view, VK_NULL_HANDLE, GEN); } - // C1: reduce sets (one per slot) - prev/curr raw luma at kGmLevel + SSBO + GMUBO. if (gmSsbo) { for (int s = 0; s < FrameGen::kGmSlots; ++s) { if (!gmSsbo[s]) continue; S.sGmReduce[s] = allocSet(dd, dev, pool, pGmReduce.setLayout); wUbo (dd, dev, S.sGmReduce[s], 1, S.gmUbo[s], sizeof(GMUBO)); wSsbo(dd, dev, S.sGmReduce[s], 16, gmSsbo[s], VK_WHOLE_SIZE); - wImg (dd, dev, S.sGmReduce[s], 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[FrameGen::kGmLevel].view, samp, GEN); // prev = template - wImg (dd, dev, S.sGmReduce[s], 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrA[FrameGen::kGmLevel].view, samp, GEN); // curr = image + wImg (dd, dev, S.sGmReduce[s], 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[FrameGen::kGmLevel].view, samp, GEN); + wImg (dd, dev, S.sGmReduce[s], 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrA[FrameGen::kGmLevel].view, samp, GEN); } } - // C1: prewarp sets (one per slot, per solved flow level) - raw curr in, stabilized out. for (int s = 0; s < FrameGen::kGmSlots; ++s) { S.sGmPrewarp[s].assign(kLevels, VK_NULL_HANDLE); for (int l = fg->flowFinest(); l < kLevels; ++l) { @@ -598,7 +481,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, } } - // expand sets (one per slot): flow inputs fixed; binding 1 = that slot's GMUBO. for (int s = 0; s < FrameGen::kGmSlots; ++s) { S.sExpand[s] = allocSet(dd, dev, pool, pExpand.setLayout); wUbo(dd, dev, S.sExpand[s], 0, S.expandUbo, sizeof(FlowUBO)); @@ -608,9 +490,6 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, wImg(dd, dev, S.sExpand[s], 49, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, expB.view, VK_NULL_HANDLE, GEN); } - // C2: flow-reg sets. F = kFlowFinest. f0 (binding 33) = flowLvl_[F] in all; luma - // (binding 34) = prev luma at F (pyrB) for the edge-aware weight. u_in (32) and - // u_out (48) alternate to realize the ping-pong (see ScratchState comment). { const int F = fg->flowFinest(); S.sFlowReg0 = allocSet(dd, dev, pool, pFlowReg.setLayout); @@ -619,26 +498,19 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, VkDescriptorSet all3[3] = {S.sFlowReg0, S.sFlowRegAB, S.sFlowRegBA}; for (VkDescriptorSet s : all3) { wUbo(dd, dev, s, 0, S.flowRegUbo, sizeof(FlowRegUBO)); - wImg(dd, dev, s, 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowLvl[F].view, samp, GEN); // f0 - wImg(dd, dev, s, 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[F].view, samp, GEN); // luma + wImg(dd, dev, s, 33, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowLvl[F].view, samp, GEN); + wImg(dd, dev, s, 34, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pyrB[F].view, samp, GEN); } - // iter0: in=flowLvl_[F] (=f0), out=A wImg(dd, dev, S.sFlowReg0, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowLvl[F].view, samp, GEN); wImg(dd, dev, S.sFlowReg0, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowRegA.view, VK_NULL_HANDLE, GEN); - // odd iters: in=A, out=B wImg(dd, dev, S.sFlowRegAB, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowRegA.view, samp, GEN); wImg(dd, dev, S.sFlowRegAB, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowRegB.view, VK_NULL_HANDLE, GEN); - // even iters (>0): in=B, out=A wImg(dd, dev, S.sFlowRegBA, 32, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, flowRegB.view, samp, GEN); wImg(dd, dev, S.sFlowRegBA, 48, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, flowRegA.view, VK_NULL_HANDLE, GEN); } - // synth set: external prev/curr/out wired per frame; flow inputs fixed S.sSynth = allocSet(dd, dev, pool, pSynth.setLayout); wUbo(dd, dev, S.sSynth, 0, S.synthUbo, sizeof(SynthUBO)); - // GENERAL (not SR): these internal flow images stay in GENERAL across the whole - // record() (written as storage by expand, sampled here) - see the discard - // transition at the top of record(). 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); @@ -646,4 +518,4 @@ static bool ensureScratch(FrameGen* fg, const DeviceDispatch* dd, VkDevice dev, return true; } -} // namespace winfg +} diff --git a/app/src/main/cpp/framegen/winfg/vk_dispatch.hpp b/app/src/main/cpp/framegen/winfg/vk_dispatch.hpp index d1a6366ad..ef259250b 100644 --- a/app/src/main/cpp/framegen/winfg/vk_dispatch.hpp +++ b/app/src/main/cpp/framegen/winfg/vk_dispatch.hpp @@ -1,7 +1,3 @@ -// win-fg - minimal Vulkan layer dispatch tables. -// Structure follows the public Khronos loader/layer interface and the -// renderdoc "Vulkan layer guide" skeleton (learning references, Apache-2.0 / -// CC-BY). No code from bionic-fg / lsfg / GameScope. #pragma once #include #include @@ -9,53 +5,35 @@ namespace winfg { -// The loader sets the first pointer-sized word of a dispatchable handle to the -// dispatch table pointer; layers key their own maps on that value so that all -// handles sharing a chain resolve to the same table. static inline void* dispatch_key(void* handle) { return *reinterpret_cast(handle); } -// Instance-level functions we call from the layer. struct InstanceDispatch { PFN_vkGetInstanceProcAddr GetInstanceProcAddr = nullptr; PFN_vkDestroyInstance DestroyInstance = nullptr; PFN_vkGetPhysicalDeviceMemoryProperties GetPhysicalDeviceMemoryProperties = nullptr; PFN_vkGetPhysicalDeviceQueueFamilyProperties GetPhysicalDeviceQueueFamilyProperties = nullptr; - // Device-context logging (affected-device fingerprint). Properties2 is core 1.1; - // may be null on a 1.0 instance - always null-check before calling. PFN_vkGetPhysicalDeviceProperties GetPhysicalDeviceProperties = nullptr; PFN_vkGetPhysicalDeviceProperties2 GetPhysicalDeviceProperties2 = nullptr; - // Surface caps - used to clamp the extra-image-headroom request to the surface's - // maxImageCount so the bump never exceeds what the driver can grant. PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR GetPhysicalDeviceSurfaceCapabilitiesKHR = nullptr; }; -// Device-level functions we call. Populated by walking vkGetDeviceProcAddr on -// the next layer down at device-create time. struct DeviceDispatch { PFN_vkGetDeviceProcAddr GetDeviceProcAddr = nullptr; PFN_vkDestroyDevice DestroyDevice = nullptr; PFN_vkGetDeviceQueue GetDeviceQueue = nullptr; PFN_vkQueueSubmit QueueSubmit = nullptr; - // Synchronization2 submit path (core 1.3 + KHR alias). DXVK may use either the - // legacy QueueSubmit or QueueSubmit2/2KHR; both are hooked for the guest-submit - // diagnostic. May be null if the device did not enable them - always null-check. PFN_vkQueueSubmit2 QueueSubmit2 = nullptr; PFN_vkQueueSubmit2KHR QueueSubmit2KHR = nullptr; PFN_vkQueueWaitIdle QueueWaitIdle = nullptr; PFN_vkDeviceWaitIdle DeviceWaitIdle = nullptr; - // swapchain / present PFN_vkCreateSwapchainKHR CreateSwapchainKHR = nullptr; PFN_vkDestroySwapchainKHR DestroySwapchainKHR = nullptr; PFN_vkGetSwapchainImagesKHR GetSwapchainImagesKHR = nullptr; PFN_vkAcquireNextImageKHR AcquireNextImageKHR = nullptr; - // Alternate acquire entry (VK_KHR_swapchain 1.1+ / device-group). DXVK may use - // either AcquireNextImageKHR or AcquireNextImage2KHR - both hooked for the - // guest-acquire diagnostic. May be null - always null-check. PFN_vkAcquireNextImage2KHR AcquireNextImage2KHR = nullptr; PFN_vkQueuePresentKHR QueuePresentKHR = nullptr; - // resources PFN_vkCreateImage CreateImage = nullptr; PFN_vkDestroyImage DestroyImage = nullptr; PFN_vkCreateImageView CreateImageView = nullptr; @@ -73,7 +51,6 @@ struct DeviceDispatch { PFN_vkCreateSampler CreateSampler = nullptr; PFN_vkDestroySampler DestroySampler = nullptr; - // pipeline / descriptors PFN_vkCreateShaderModule CreateShaderModule = nullptr; PFN_vkDestroyShaderModule DestroyShaderModule = nullptr; PFN_vkCreateDescriptorSetLayout CreateDescriptorSetLayout = nullptr; @@ -84,15 +61,10 @@ struct DeviceDispatch { PFN_vkDestroyPipeline DestroyPipeline = nullptr; PFN_vkCreateDescriptorPool CreateDescriptorPool = nullptr; PFN_vkDestroyDescriptorPool DestroyDescriptorPool = nullptr; - // Frees every set in the pool at once - used by FrameGen::configure() to reclaim - // the old scratch descriptor sets when a perf_preset change forces a live rebuild - // (otherwise repeated preset switches would exhaust the pool). May be null in - // theory; the rebuild path null-checks before calling. PFN_vkResetDescriptorPool ResetDescriptorPool = nullptr; PFN_vkAllocateDescriptorSets AllocateDescriptorSets = nullptr; PFN_vkUpdateDescriptorSets UpdateDescriptorSets = nullptr; - // command recording PFN_vkCreateCommandPool CreateCommandPool = nullptr; PFN_vkDestroyCommandPool DestroyCommandPool = nullptr; PFN_vkAllocateCommandBuffers AllocateCommandBuffers = nullptr; @@ -104,7 +76,7 @@ struct DeviceDispatch { PFN_vkCmdDispatch CmdDispatch = nullptr; PFN_vkCmdPipelineBarrier CmdPipelineBarrier = nullptr; PFN_vkCmdCopyImage CmdCopyImage = nullptr; - PFN_vkCmdCopyImageToBuffer CmdCopyImageToBuffer = nullptr; // capture readback + PFN_vkCmdCopyImageToBuffer CmdCopyImageToBuffer = nullptr; PFN_vkCmdBlitImage CmdBlitImage = nullptr; PFN_vkCmdClearColorImage CmdClearColorImage = nullptr; PFN_vkCreateFence CreateFence = nullptr; @@ -120,4 +92,4 @@ static inline void load(PFN& slot, PFN_vkGetDeviceProcAddr gdpa, VkDevice dev, c slot = reinterpret_cast(gdpa(dev, name)); } -} // namespace winfg +} diff --git a/app/src/main/cpp/framegen/winfg/winfg_engine.cpp b/app/src/main/cpp/framegen/winfg/winfg_engine.cpp index c2737e261..965e15a15 100644 --- a/app/src/main/cpp/framegen/winfg/winfg_engine.cpp +++ b/app/src/main/cpp/framegen/winfg/winfg_engine.cpp @@ -1,4 +1,3 @@ -// See winfg_engine.h. #include "winfg_engine.h" #include "winfg_vkd.h" #include "log.hpp" @@ -8,11 +7,6 @@ namespace winfg { namespace { -// 2x: the guest layer synthesises at 0.35, not 0.5 - biased towards prev to -// cut the double-exposure ghost on HUD/text ("iterative bring-up knob" in -// layer.cpp). Kept for parity, since that is the tuning that was proven. -// 3x/4x: evenly spaced; never exercised by the guest layer, so this is the -// new ground native opens up and the first thing to judge by eye. float alphaFor(uint32_t k, uint32_t n) { if (n <= 1) return 0.35f; return (float)(k + 1) / (float)(n + 1); @@ -30,7 +24,7 @@ VkImageMemoryBarrier bar(VkImage img, VkImageLayout from, VkImageLayout to, return b; } -} // namespace +} Engine::Engine() = default; Engine::~Engine() { destroyRing(); fg_.destroy(); } @@ -65,8 +59,8 @@ bool Engine::prepare(uint32_t w, uint32_t h, VkFormat fmt) { if (unavailable_ || w == 0 || h == 0 || fmt == VK_FORMAT_UNDEFINED) return false; if (builtW_ == w && builtH_ == h && builtFmt_ == fmt && fg_.valid()) return true; - // onResize rebuilds the pyramids/scratch; our own input ring follows it. - if (!fg_.onResize(VkExtent2D{w, h}, fmt, /*force=*/false)) { + + if (!fg_.onResize(VkExtent2D{w, h}, fmt, false)) { WFG_LOGE("native: onResize %ux%u failed; frame generation unavailable", w, h); unavailable_ = true; return false; @@ -83,21 +77,17 @@ bool Engine::prepare(uint32_t w, uint32_t h, VkFormat fmt) { } uint32_t Engine::plan(uint32_t capacity) const { - if (!valid() || history_ < 2) return 0; // need prev AND curr + if (!valid() || history_ < 2) return 0; const uint32_t want = (uint32_t)std::max(cfg_.multiplier - 1, 0); return std::min({want, capacity, kMaxGenerations}); } void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t w, uint32_t h, - uint32_t /*generations*/) { + uint32_t ) { if (!valid()) return; - // Rotate: the slot we are about to overwrite is the OLD prev; after the - // copy it is the new curr and the other slot is prev. curr_ = 1 - curr_; Slot& dst = ring_[curr_]; - // source is GENERAL (composite render pass leaves it there); the slot may - // be in any layout from last use - UNDEFINED is legal as "discard". VkImageMemoryBarrier pre[2] = { bar(source, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT), @@ -115,8 +105,6 @@ void Engine::process(VkCommandBuffer cmd, VkImage source, uint32_t w, uint32_t h dd_.CmdCopyImage(cmd, source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst.img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); - // source back to GENERAL for the real-frame copy that follows; the new - // curr becomes SHADER_READ_ONLY, which is what the chain samples it as. VkImageMemoryBarrier post[2] = { bar(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT), @@ -138,17 +126,11 @@ void Engine::generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t cou const Slot& curr = ring_[curr_]; if (!prev.valid || !curr.valid) return; - // The chain writes the output through a STORAGE descriptor in GENERAL. - // The composite ring already keeps its targets in GENERAL; make the - // prior contents' writes visible and take ownership for the compute write. VkImageMemoryBarrier toWrite = bar(targetImage, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_WRITE_BIT); 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); - // gmSlot indexes the C1 global-motion SSBO read back on the CPU next - // frame; kGmSlots (3) >= our frames in flight (2), so rotating per source - // frame keeps a slot from being read while its reduce is still in flight. fg_.record(cmd, prev.view, curr.view, targetView, alphaFor(generation, count), gmSlot_); if (generation + 1 == count) gmSlot_ = (gmSlot_ + 1) % (uint32_t)FrameGen::kGmSlots; @@ -157,7 +139,6 @@ void Engine::generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t cou void Engine::reset() { history_ = 0; frames_ = 0; } -// ---- input ring -------------------------------------------------------------- uint32_t Engine::memType(uint32_t bits, VkMemoryPropertyFlags props) const { for (uint32_t i = 0; i < memProps_.memoryTypeCount; i++) @@ -201,4 +182,4 @@ void Engine::destroyRing() { history_ = 0; } -} // namespace winfg +} diff --git a/app/src/main/cpp/framegen/winfg/winfg_engine.h b/app/src/main/cpp/framegen/winfg/winfg_engine.h index 13675da99..9a607f531 100644 --- a/app/src/main/cpp/framegen/winfg/winfg_engine.h +++ b/app/src/main/cpp/framegen/winfg/winfg_engine.h @@ -1,28 +1,4 @@ #pragma once -// ============================================================================ -// winfg_engine - win-fg (Bannerlator's own frame generation) running inside -// the compositor, behind the same contract the LSFG engine uses: -// -// prepare(w, h, format) build/rebuild the chain for this size -// plan(capacity) how many frames to generate this time -// process(cmd, source, w, h, gens) take frame N in (becomes "curr") -// generateInto(cmd, k, n, ...) synthesise generated frame k of n -// -// Why this exists: as a guest-side Vulkan layer, win-fg's generated frames -// had to cross Wine -> DRI3 -> our X server -> the compositor, where the -// per-window latest-wins overwrite dropped them before they were ever -// presented - the HUD counted 2x, the panel got 1x. Here the chain runs in the -// compositor's own command stream and its frames are presented directly, on -// the same multi-present path that is device-proven for LSFG Native. -// -// Unlike LSFG this needs nothing from the user: the ten shaders are ours -// (MIT, see LICENSE.win-fg / NOTICE_FIDELITYFX_OPTICALFLOW.md) and embedded. -// -// The chain (framegen.cpp) is used unmodified. Its inputs are two sampled -// rgba8 views (prev, curr) and its output a GENERAL storage view; the guest -// layer transitions curr to SHADER_READ_ONLY before record() and the output -// UNDEFINED -> GENERAL, so we do exactly the same around the same call. -// ============================================================================ #include #include #include "framegen.hpp" @@ -32,8 +8,6 @@ struct VkTable; namespace winfg { -// Hard ceiling on generated frames per source frame (2x..4x -> 1..3), matching -// the LSFG engine and the drawer's buttons. constexpr uint32_t kMaxGenerations = 3; class Engine { @@ -48,22 +22,15 @@ public: bool valid() const { return fg_.valid() && !unavailable_; } bool unavailable() const { return unavailable_; } - // model 3/4, perf preset 0..2, flow scale - the same knobs the drawer - // already exposes for win-fg; they map straight onto winfg::Config. void configure(uint32_t multiplier, int model, int perfPreset, float flowScale); bool prepare(uint32_t width, uint32_t height, VkFormat format); - // 0 until there is a previous frame to interpolate from. uint32_t plan(uint32_t capacity) const; - // Copy the composited frame into the input ring as "curr" (the previous - // one becomes "prev"). `source` must be in GENERAL. void process(VkCommandBuffer cmd, VkImage source, uint32_t width, uint32_t height, uint32_t generations); - // Synthesise generated frame `generation` of `count` into the storage view. - // The target must be in GENERAL (the composite ring keeps it there). void generateInto(VkCommandBuffer cmd, uint32_t generation, uint32_t count, VkImage targetImage, VkImageView targetView, uint32_t width, uint32_t height); @@ -91,12 +58,12 @@ private: bool unavailable_ = false; Slot ring_[2]; - int curr_ = 0; // index of "curr" in ring_; prev is 1 - curr_ - uint32_t history_ = 0; // frames copied so far (need 2) - uint32_t gmSlot_ = 0; // rotates through FrameGen::kGmSlots + int curr_ = 0; + uint32_t history_ = 0; + uint32_t gmSlot_ = 0; uint32_t builtW_ = 0, builtH_ = 0; VkFormat builtFmt_ = VK_FORMAT_UNDEFINED; uint64_t frames_ = 0; }; -} // namespace winfg +} diff --git a/app/src/main/cpp/framegen/winfg/winfg_vkd.cpp b/app/src/main/cpp/framegen/winfg/winfg_vkd.cpp index d76f357c1..a0c2dd441 100644 --- a/app/src/main/cpp/framegen/winfg/winfg_vkd.cpp +++ b/app/src/main/cpp/framegen/winfg/winfg_vkd.cpp @@ -1,4 +1,3 @@ -// See winfg_vkd.h. #include "winfg_vkd.h" #include "../VulkanRendererContext.h" #include diff --git a/app/src/main/cpp/framegen/winfg/winfg_vkd.h b/app/src/main/cpp/framegen/winfg/winfg_vkd.h index 59d27bf2e..8faffc826 100644 --- a/app/src/main/cpp/framegen/winfg/winfg_vkd.h +++ b/app/src/main/cpp/framegen/winfg/winfg_vkd.h @@ -1,19 +1,4 @@ #pragma once -// ============================================================================ -// winfg_vkd - fill win-fg's own dispatch tables from the renderer's VkTable. -// -// win-fg's chain (framegen.cpp / record_impl.inc) was written as a Vulkan -// LAYER and calls through winfg::DeviceDispatch / InstanceDispatch. Inside the -// compositor there is no layer chain to resolve those from, so they are filled -// from the renderer's per-context table instead - the same bridge lsfg_vkd -// provides for the LSFG chain, for the same reason: an adrenotools driver -// shares no global symbols with the system loader. -// -// Only the entry points the chain actually calls are filled (36 device-level -// plus GetPhysicalDeviceMemoryProperties); everything else in the structs -// stays null and is never reached. winfgVkdInit reports a false if any of -// those failed to resolve, so a null is never called mid-frame. -// ============================================================================ #include "vk_dispatch.hpp" struct VkTable; bool winfgVkdInit(const VkTable& table, winfg::DeviceDispatch& dd, winfg::InstanceDispatch& id); diff --git a/app/src/main/cpp/scanout/ScanoutContext.h b/app/src/main/cpp/scanout/ScanoutContext.h index e14a82a9b..098136640 100644 --- a/app/src/main/cpp/scanout/ScanoutContext.h +++ b/app/src/main/cpp/scanout/ScanoutContext.h @@ -1,17 +1,5 @@ #pragma once -// Renderer-neutral direct-scanout implementation (Android SurfaceControl / -// SurfaceFlinger). Pure NDK: dlsym'd ASurfaceControl_* / ASurfaceTransaction_* -// plus AHardwareBuffer_*. It contains ZERO Vulkan/GL/EGL calls and ZERO -// render-thread coupling: the owning renderer drives it through the public -// surface below and decides WHEN the deferred cursor transaction is applied -// (the Vulkan path defers to its render loop; a GL path would apply inline). -// -// Extracted verbatim from VulkanRendererContext / VulkanRendererScanout.cpp -// (P0 of the GL direct-scanout plan). The only intended semantic change vs the -// old code is that the cursor render-thread signalling (needsRender / dirtyCV) -// stays in the owner: setCursorImage()/setCursorPos() return whether an apply -// became pending so the owner can signal its loop exactly as before. #include #include @@ -27,50 +15,34 @@ class ScanoutContext { public: - // --- API load / lifecycle --- bool loadApi(); - // Fallback path: creates the child SurfaceControls under the owner's own - // ANativeWindow. Passing a window updates the stored fallback window. void initFromWindow(ANativeWindow* win = nullptr); - // Primary path: wraps the two sibling game/cursor windows (handed in by the - // owner) as ASurfaceControls. Takes ownership and releases both windows. void initFromWindows(ANativeWindow* gameWin, ANativeWindow* cursorWin); void destroy(); - // --- per-frame game buffer (applies its transaction INLINE) --- void setBuffer(AHardwareBuffer* ahb, int x, int y, int w, int h, int fenceFd = -1); - // --- cursor: pure state-setters. They mutate state only and DO NOT signal - // any render thread. Each returns true if a deferred apply is now - // pending (i.e. the owner should arrange for applyPendingCursor()). bool setCursorImage(void* pixels, short w, short h, short stride); bool setCursorPos(short x, short y, short hotX, short hotY); - // Applies the pending cursor image/position transaction. Safe to call from - // any thread (a SurfaceControl transaction apply is thread-safe). The Vulkan - // owner calls this from its render loop; an inline owner may call it directly. void applyPendingCursor(); - // --- geometry / sizing fed by the owner --- void setDst(int x, int y, int w, int h); void setContainerSize(int w, int h) { containerWidth = w; containerHeight = h; } void setSurfaceSize(int w, int h) { surfaceWidth = w; surfaceHeight = h; } void setFallbackWindow(ANativeWindow* win) { window = win; } void setVerboseLog(bool v) { verboseLog = v; } - // --- queries --- bool isActive() const { return scanoutActive.load(); } bool isGameFrameDelivered() const { return gameFrameDelivered.load(std::memory_order_acquire); } void* debugGameSC() const { return scanoutGameSC; } - // Public so the owner can expose them through its existing ABI without a - // second source of truth (Vulkan binds references to these). std::atomic scanoutActive{false}; std::atomic gameFrameDelivered{false}; private: - ANativeWindow* window = nullptr; // owner's own window, for the fallback path + ANativeWindow* window = nullptr; bool verboseLog = true; int surfaceWidth = 0, surfaceHeight = 0, containerWidth = 0, containerHeight = 0; diff --git a/app/src/main/cpp/sdnative/sdnative.c b/app/src/main/cpp/sdnative/sdnative.c index aa928b856..c7ec580ab 100644 --- a/app/src/main/cpp/sdnative/sdnative.c +++ b/app/src/main/cpp/sdnative/sdnative.c @@ -1,11 +1,3 @@ -/* - * One function: the release fence the fake-input ring writer needs. - * - * FakeInputWriter publishes events into a shared-memory ring that libfakeinput.so reads from - * another process. Java's memory model says nothing about what a foreign reader sees, so the - * writer calls this between filling the events and advancing the sequence counter: everything - * written before it is visible to the reader before the counter moves. - */ #include #include diff --git a/app/src/main/cpp/shaders/cas_frag.h b/app/src/main/cpp/shaders/cas_frag.h index 21347e38d..103c2cce4 100644 --- a/app/src/main/cpp/shaders/cas_frag.h +++ b/app/src/main/cpp/shaders/cas_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t cas_code[] = { 0x07230203,0x00010000,0x0008000b,0x000000f3,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/color_frag.h b/app/src/main/cpp/shaders/color_frag.h index bde543ab4..7026c7c88 100644 --- a/app/src/main/cpp/shaders/color_frag.h +++ b/app/src/main/cpp/shaders/color_frag.h @@ -1,4 +1,3 @@ - // 1116.5.0 #pragma once const uint32_t color_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000059,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/crt_frag.h b/app/src/main/cpp/shaders/crt_frag.h index 2239f2274..e814ea7f7 100644 --- a/app/src/main/cpp/shaders/crt_frag.h +++ b/app/src/main/cpp/shaders/crt_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t crt_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000063,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/deband_frag.h b/app/src/main/cpp/shaders/deband_frag.h index 4a59f2135..dab0cba93 100644 --- a/app/src/main/cpp/shaders/deband_frag.h +++ b/app/src/main/cpp/shaders/deband_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t deband_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000053,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/fsr_easu_frag.h b/app/src/main/cpp/shaders/fsr_easu_frag.h index eb6d36af4..b2d125298 100644 --- a/app/src/main/cpp/shaders/fsr_easu_frag.h +++ b/app/src/main/cpp/shaders/fsr_easu_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t fsr_easu_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000046e,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/fsr_rcas_frag.h b/app/src/main/cpp/shaders/fsr_rcas_frag.h index 066d125dd..f241d7590 100644 --- a/app/src/main/cpp/shaders/fsr_rcas_frag.h +++ b/app/src/main/cpp/shaders/fsr_rcas_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t fsr_rcas_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000025e,0x00000000,0x00020011,0x00000001,0x00020011, diff --git a/app/src/main/cpp/shaders/fxaa_frag.h b/app/src/main/cpp/shaders/fxaa_frag.h index 6b7be374b..8f22155f7 100644 --- a/app/src/main/cpp/shaders/fxaa_frag.h +++ b/app/src/main/cpp/shaders/fxaa_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t fxaa_code[] = { 0x07230203,0x00010000,0x0008000b,0x000000e4,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/hdr_frag.h b/app/src/main/cpp/shaders/hdr_frag.h index 37e5783aa..4bcdfad3a 100644 --- a/app/src/main/cpp/shaders/hdr_frag.h +++ b/app/src/main/cpp/shaders/hdr_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t hdr_code[] = { 0x07230203,0x00010000,0x0008000b,0x000000f4,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/nis_frag.h b/app/src/main/cpp/shaders/nis_frag.h index 468283d7f..b20294eae 100644 --- a/app/src/main/cpp/shaders/nis_frag.h +++ b/app/src/main/cpp/shaders/nis_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t nis_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000644,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/ntsc_frag.h b/app/src/main/cpp/shaders/ntsc_frag.h index 9eb75f218..409c9d90f 100644 --- a/app/src/main/cpp/shaders/ntsc_frag.h +++ b/app/src/main/cpp/shaders/ntsc_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t ntsc_code[] = { 0x07230203,0x00010000,0x0008000b,0x000000d5,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/sgsr_frag.h b/app/src/main/cpp/shaders/sgsr_frag.h index 2452fb68f..11f19152e 100644 --- a/app/src/main/cpp/shaders/sgsr_frag.h +++ b/app/src/main/cpp/shaders/sgsr_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t sgsr_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000216,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/sgsr_quality_frag.h b/app/src/main/cpp/shaders/sgsr_quality_frag.h index ca44e6a6a..34b629f2d 100644 --- a/app/src/main/cpp/shaders/sgsr_quality_frag.h +++ b/app/src/main/cpp/shaders/sgsr_quality_frag.h @@ -1,4 +1,3 @@ - // 1116.5.0 #pragma once const uint32_t sgsr_quality_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000269,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/toon_frag.h b/app/src/main/cpp/shaders/toon_frag.h index aa54788ff..615ebdaee 100644 --- a/app/src/main/cpp/shaders/toon_frag.h +++ b/app/src/main/cpp/shaders/toon_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t toon_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000006d,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/shaders/upscale_vert.h b/app/src/main/cpp/shaders/upscale_vert.h index c64f91729..61f678288 100644 --- a/app/src/main/cpp/shaders/upscale_vert.h +++ b/app/src/main/cpp/shaders/upscale_vert.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t upscale_vert_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000004b,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/termux/termux.c b/app/src/main/cpp/termux/termux.c index 7d4f86e31..5365e95bc 100644 --- a/app/src/main/cpp/termux/termux.c +++ b/app/src/main/cpp/termux/termux.c @@ -1,4 +1,3 @@ -// Upstream: termux/termux-app v0.118.0, terminal-emulator/src/main/jni/termux.c. // Licensed under the repository's GPL-3.0 license (see LICENSE). #include #include @@ -51,14 +50,12 @@ static int create_subprocess(JNIEnv* env, return throw_runtime_exception(env, "Cannot grantpt()/unlockpt()/ptsname_r() on /dev/ptmx"); } - // Enable UTF-8 mode and disable flow control to prevent Ctrl+S from locking up the display. struct termios tios; tcgetattr(ptm, &tios); tios.c_iflag |= IUTF8; tios.c_iflag &= ~(IXON | IXOFF); tcsetattr(ptm, TCSANOW, &tios); - /** Set initial winsize. */ struct winsize sz = { .ws_row = (unsigned short) rows, .ws_col = (unsigned short) columns }; ioctl(ptm, TIOCSWINSZ, &sz); @@ -69,7 +66,6 @@ static int create_subprocess(JNIEnv* env, *pProcessId = (int) pid; return ptm; } else { - // Clear signals which the Android java process may have blocked: sigset_t signals_to_unblock; sigfillset(&signals_to_unblock); sigprocmask(SIG_UNBLOCK, &signals_to_unblock, 0); @@ -100,13 +96,11 @@ static int create_subprocess(JNIEnv* env, if (chdir(cwd) != 0) { char* error_message; - // No need to free asprintf()-allocated memory since doing execvp() or exit() below. if (asprintf(&error_message, "chdir(\"%s\")", cwd) == -1) error_message = "chdir()"; perror(error_message); fflush(stderr); } execvp(cmd, argv); - // Show terminal output about failing exec() call: char* error_message; if (asprintf(&error_message, "exec(\"%s\")", cmd) == -1) error_message = "exec()"; perror(error_message); @@ -207,7 +201,6 @@ JNIEXPORT jint JNICALL Java_com_termux_terminal_JNI_waitFor(JNIEnv* TERMUX_UNUSE } else if (WIFSIGNALED(status)) { return -WTERMSIG(status); } else { - // Should never happen - waitpid(2) says "One of the first three macros will evaluate to a non-zero (true) value". return 0; } } diff --git a/app/src/main/cpp/thirdparty/dxbc/.gitignore b/app/src/main/cpp/thirdparty/dxbc/.gitignore index 4522fe54d..bc9544853 100644 --- a/app/src/main/cpp/thirdparty/dxbc/.gitignore +++ b/app/src/main/cpp/thirdparty/dxbc/.gitignore @@ -1,3 +1,2 @@ -# cmake files .cache/ build/ diff --git a/app/src/main/cpp/thirdparty/dxbc/CMakeLists.txt b/app/src/main/cpp/thirdparty/dxbc/CMakeLists.txt index 2651d7b8d..43422465e 100644 --- a/app/src/main/cpp/thirdparty/dxbc/CMakeLists.txt +++ b/app/src/main/cpp/thirdparty/dxbc/CMakeLists.txt @@ -13,7 +13,6 @@ file(GLOB SOURCES add_library(dxbc STATIC ${SOURCES}) -# target set_target_properties(dxbc PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON) diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_analysis.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_analysis.h index 6c8b0325b..87113c1dc 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_analysis.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_analysis.h @@ -9,13 +9,6 @@ namespace dxvk { - /** - * \brief Info about unordered access views - * - * Stores whether an UAV is accessed with typed - * read or atomic instructions. This information - * will be used to generate image types. - */ struct DxbcUavInfo { bool accessTypedLoad = false; bool accessAtomicOp = false; @@ -25,27 +18,15 @@ namespace dxvk { VkAccessFlags accessFlags = 0; }; - /** - * \brief Info about shader resource views - * - * Stores whether an SRV is accessed with - * sparse feedback. Useful for buffers. - */ struct DxbcSrvInfo { bool sparseFeedback = false; }; - /** - * \brief Counts cull and clip distances - */ struct DxbcClipCullInfo { uint32_t numClipPlanes = 0; uint32_t numCullPlanes = 0; }; - /** - * \brief Shader analysis info - */ struct DxbcAnalysisInfo { std::array uavInfos; std::array srvInfos; @@ -60,13 +41,6 @@ namespace dxvk { bool usesKill = false; }; - /** - * \brief DXBC shader analysis pass - * - * Collects information about the shader itself - * and the resources used by the shader, which - * will later be used by the actual compiler. - */ class DxbcAnalyzer { public: @@ -81,10 +55,6 @@ namespace dxvk { ~DxbcAnalyzer(); - /** - * \brief Processes a single instruction - * \param [in] ins The instruction - */ void processInstruction( const DxbcShaderInstruction& ins); diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_isgn.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_isgn.h index 9b4b79186..da3c97d68 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_isgn.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_isgn.h @@ -9,12 +9,6 @@ namespace dxvk { - /** - * \brief Signature entry - * - * Stores the semantic name of an input or - * output and the corresponding register. - */ struct DxbcSgnEntry { std::string semanticName; uint32_t semanticIndex; @@ -26,12 +20,6 @@ namespace dxvk { uint32_t streamId; }; - /** - * \brief Input/Output signature chunk - * - * Stores information about the input and - * output registers used by the shader stage. - */ class DxbcIsgn : public RcObject { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_shex.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_shex.h index 8deecc867..02b3cd914 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_shex.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_chunk_shex.h @@ -6,13 +6,6 @@ namespace dxvk { - /** - * \brief Shader code chunk - * - * Stores the DXBC shader code itself, as well - * as some meta info about the shader, i.e. what - * type of shader this is. - */ class DxbcShex : public RcObject { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_common.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_common.h index d9d420767..4a382c8c4 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_common.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_common.h @@ -4,12 +4,6 @@ namespace dxvk { - /** - * \brief DXBC Program type - * - * Defines the shader stage that a DXBC - * module has been compiled form. - */ enum class DxbcProgramType : uint16_t { PixelShader = 0, VertexShader = 1, @@ -24,11 +18,6 @@ namespace dxvk { using DxbcProgramTypeFlags = Flags; - /** - * \brief DXBC shader info - * - * Stores the shader program type. - */ class DxbcProgramInfo { public: @@ -37,30 +26,12 @@ namespace dxvk { DxbcProgramInfo(DxbcProgramType type) : m_type(type) { } - /** - * \brief Program type - * \returns Program type - */ DxbcProgramType type() const { return m_type; } - /** - * \brief Vulkan shader stage - * - * The \c VkShaderStageFlagBits constant - * that corresponds to the program type. - * \returns Vulkan shaer stage - */ VkShaderStageFlagBits shaderStage() const; - /** - * \brief SPIR-V execution model - * - * The execution model that corresponds - * to the Vulkan shader stage. - * \returns SPIR-V execution model - */ spv::ExecutionModel executionModel() const; private: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_compiler.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_compiler.h index f63d1e11a..d5c699885 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_compiler.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_compiler.h @@ -16,27 +16,12 @@ namespace dxvk { - /** - * \brief Vector type - * - * Convenience struct that stores a scalar - * type and a component count. The compiler - * can use this to generate SPIR-V types. - */ struct DxbcVectorType { DxbcScalarType ctype; uint32_t ccount; }; - /** - * \brief Array type - * - * Convenience struct that stores a scalar type, a - * component count and an array size. An array of - * length 0 will be evaluated to a vector type. The - * compiler can use this to generate SPIR-V types. - */ struct DxbcArrayType { DxbcScalarType ctype; uint32_t ccount; @@ -44,39 +29,18 @@ namespace dxvk { }; - /** - * \brief Register info - * - * Stores the array type of a register and - * its storage class. The compiler can use - * this to generate SPIR-V pointer types. - */ struct DxbcRegisterInfo { DxbcArrayType type; spv::StorageClass sclass; }; - /** - * \brief Register value - * - * Stores a vector type and a SPIR-V ID that - * represents an intermediate value. This is - * used to track the type of such values. - */ struct DxbcRegisterValue { DxbcVectorType type; uint32_t id; }; - /** - * \brief Register pointer - * - * Stores a vector type and a SPIR-V ID that - * represents a pointer to such a vector. This - * can be used to load registers conveniently. - */ struct DxbcRegisterPointer { DxbcVectorType type; uint32_t id; @@ -98,12 +62,6 @@ namespace dxvk { }; - /** - * \brief Specialization constant properties - * - * Stores the name, data type and initial - * value of a specialization constant. - */ struct DxbcSpecConstant { DxbcScalarType ctype; uint32_t ccount; @@ -112,13 +70,6 @@ namespace dxvk { }; - /** - * \brief Helper struct for conditional execution - * - * Stores a set of labels required to implement either - * an if-then block or an if-then-else block. This is - * not used to implement control flow instructions. - */ struct DxbcConditional { uint32_t labelIf = 0; uint32_t labelElse = 0; @@ -144,9 +95,6 @@ namespace dxvk { }; - /** - * \brief Vertex shader-specific structure - */ struct DxbcCompilerVsPart { uint32_t functionId = 0; @@ -157,9 +105,6 @@ namespace dxvk { }; - /** - * \brief Geometry shader-specific structure - */ struct DxbcCompilerGsPart { DxbcPrimitive inputPrimitive = DxbcPrimitive::Undefined; DxbcPrimitiveTopology outputTopology = DxbcPrimitiveTopology::Undefined; @@ -175,9 +120,6 @@ namespace dxvk { }; - /** - * \brief Pixel shader-specific structure - */ struct DxbcCompilerPsPart { uint32_t functionId = 0; @@ -196,9 +138,6 @@ namespace dxvk { }; - /** - * \brief Compute shader-specific structure - */ struct DxbcCompilerCsPart { uint32_t functionId = 0; @@ -213,12 +152,6 @@ namespace dxvk { }; - /** - * \brief Hull shader fork/join phase - * - * Defines a function and built-in variables - * for a single fork or join phase sub-program. - */ struct DxbcCompilerHsForkJoinPhase { uint32_t functionId = 0; uint32_t instanceCount = 1; @@ -228,35 +161,20 @@ namespace dxvk { }; - /** - * \brief Hull shader control point phase - * - * Defines the function for the control - * point phase program of a hull shader. - */ struct DxbcCompilerHsControlPointPhase { uint32_t functionId = 0; }; - /** - * \brief Hull shader phase - * - * Used to identify the current - * phase and function ID. - */ enum class DxbcCompilerHsPhase : uint32_t { - None, ///< No active phase - Decl, ///< \c hs_decls - ControlPoint, ///< \c hs_control_point_phase - Fork, ///< \c hs_fork_phase - Join, ///< \c hs_join_phase + None, + Decl, + ControlPoint, + Fork, + Join, }; - /** - * \brief Hull shader-specific structure - */ struct DxbcCompilerHsPart { DxbcCompilerHsPhase currPhaseType = DxbcCompilerHsPhase::None; size_t currPhaseId = 0; @@ -284,9 +202,6 @@ namespace dxvk { }; - /** - * \brief Domain shader-specific structure - */ struct DxbcCompilerDsPart { uint32_t functionId = 0; @@ -362,13 +277,6 @@ namespace dxvk { }; - /** - * \brief DXBC to SPIR-V shader compiler - * - * Processes instructions from a DXBC shader and creates - * a DXVK shader object, which contains the SPIR-V module - * and information about the shader resource bindings. - */ class DxbcCompiler { public: @@ -383,35 +291,13 @@ namespace dxvk { const DxbcAnalysisInfo& analysis); ~DxbcCompiler(); - /** - * \brief Processes a single instruction - * \param [in] ins The instruction - */ void processInstruction( const DxbcShaderInstruction& ins); - /** - * \brief Emits transform feedback passthrough - * - * Writes all captured input variables to the - * corresponding xfb outputs, and sets up the - * geometry shader for point-to-point mode. - */ void processXfbPassthrough(); - /** - * \brief Finalizes the shader - * \returns The final shader object - */ SpirvCodeBuffer finalize(); - /** - * \brief Extracts immediate constant buffer data - * - * Only defined if the ICB needs to be backed by a - * uniform buffer. - * \returns Immediate constant buffer data - */ std::vector getIcbData() const { return std::move(m_icbData); } @@ -428,49 +314,27 @@ namespace dxvk { const DxbcAnalysisInfo* m_analysis; - /////////////////////////////////////////////////////// - // Resource slot description for the shader. This will - // be used to map D3D11 bindings to DXVK bindings. std::vector m_bindings; - //////////////////////////////////////////////// - // Temporary r# vector registers with immediate - // indexing, and x# vector array registers. std::vector m_rRegs; std::vector m_xRegs; - ///////////////////////////////////////////// - // Thread group shared memory (g#) registers std::vector m_gRegs; - /////////////////////////////////////////////////////////// - // v# registers as defined by the shader. The type of each - // of these inputs is either float4 or an array of float4. std::array< DxbcRegisterPointer, DxbcMaxInterfaceRegs> m_vRegs; std::vector m_vMappings; - ////////////////////////////////////////////////////////// - // o# registers as defined by the shader. In the fragment - // shader stage, these registers are typed by the signature, - // in all other stages, they are float4 registers or arrays. std::array< DxbcRegisterPointer, DxbcMaxInterfaceRegs> m_oRegs; std::vector m_oMappings; - ///////////////////////////////////////////// - // xfb output registers for geometry shaders std::vector m_xfbVars; - ///////////////////////////////////////////// - // Dynamically indexed input and output regs std::vector m_indexRanges = { }; - ////////////////////////////////////////////////////// - // Shader resource variables. These provide access to - // constant buffers, samplers, textures, and UAVs. std::array m_constantBuffers; std::array m_samplers; std::array m_textures; @@ -479,9 +343,6 @@ namespace dxvk { bool m_hasGloballyCoherentUav = false; bool m_hasRasterizerOrderedUav = false; - /////////////////////////////////////////////// - // Control flow information. Stores labels for - // currently active if-else blocks and loops. std::vector m_controlFlowBlocks; bool m_topLevelIsUniform = true; @@ -489,22 +350,13 @@ namespace dxvk { uint64_t m_uavRdMask = 0u; uint64_t m_uavWrMask = 0u; - ////////////////////////////////////////////// - // Function state tracking. Required in order - // to properly end functions in some cases. bool m_insideFunction = false; - /////////////////////////////////////////////////////////// - // Array of input values. Since v# registers are indexable - // in DXBC, we need to copy them into an array first. uint32_t m_vArrayLength = 0; uint32_t m_vArrayLengthId = 0; uint32_t m_vArray = 0; - //////////////////////////////////////////////////// - // Per-vertex input and output blocks. Depending on - // the shader stage, these may be declared as arrays. uint32_t m_positionIn = 0; uint32_t m_positionOut = 0; @@ -514,43 +366,25 @@ namespace dxvk { uint32_t m_primitiveIdIn = 0; uint32_t m_primitiveIdOut = 0; - ////////////////////////////////////////////////// - // Immediate constant buffer. If defined, this is - // an array of four-component uint32 vectors. uint32_t m_icbArray = 0; std::vector m_icbData; uint32_t m_icbComponents = 0u; uint32_t m_icbSize = 0u; - /////////////////////////////////////////////////// - // Sample pos array. If defined, this iis an array - // of 32 four-component float vectors. uint32_t m_samplePositions = 0; - //////////////////////////////////////////// - // Struct type used for UAV counter buffers uint32_t m_uavCtrStructType = 0; uint32_t m_uavCtrPointerType = 0; - //////////////////////////////// - // Function IDs for subroutines std::unordered_map m_subroutines; - /////////////////////////////////////////////////// - // Entry point description - we'll need to declare - // the function ID and all input/output variables. uint32_t m_entryPointId = 0; bool m_hasRawAccessChains = false; - //////////////////////////////////////////// - // Inter-stage shader interface slots. Also - // covers vertex input and fragment output. uint32_t m_inputMask = 0u; uint32_t m_outputMask = 0u; - /////////////////////////////////// - // Shader-specific data structures DxbcCompilerVsPart m_vs; DxbcCompilerHsPart m_hs; DxbcCompilerDsPart m_ds; @@ -558,8 +392,6 @@ namespace dxvk { DxbcCompilerPsPart m_ps; DxbcCompilerCsPart m_cs; - ////////////////////// - // Global state stuff bool m_precise = true; DxbcOpcode m_lastOp = DxbcOpcode::Nop; @@ -568,8 +400,6 @@ namespace dxvk { VkPrimitiveTopology m_inputTopology = VK_PRIMITIVE_TOPOLOGY_MAX_ENUM; VkPrimitiveTopology m_outputTopology = VK_PRIMITIVE_TOPOLOGY_MAX_ENUM; - ///////////////////////////////////////////////////// - // Shader interface and metadata declaration methods void emitDcl( const DxbcShaderInstruction& ins); @@ -662,8 +492,6 @@ namespace dxvk { uint32_t emitDclUavCounter( uint32_t regId); - //////////////////////// - // Custom data handlers void emitDclImmediateConstantBuffer( const DxbcShaderInstruction& ins); @@ -680,8 +508,6 @@ namespace dxvk { void emitCustomData( const DxbcShaderInstruction& ins); - ////////////////////////////// - // Instruction class handlers void emitVectorAlu( const DxbcShaderInstruction& ins); @@ -787,8 +613,6 @@ namespace dxvk { void emitTypedUavStore( const DxbcShaderInstruction& ins); - ///////////////////////////////////// - // Control flow instruction handlers void emitControlFlowIf( const DxbcShaderInstruction& ins); @@ -843,10 +667,6 @@ namespace dxvk { void emitControlFlow( const DxbcShaderInstruction& ins); - //////////////////////////////////////////////// - // Constant building methods. These are used to - // generate constant vectors that store the same - // value in each component. DxbcRegisterValue emitBuildConstVecf32( float x, float y, @@ -880,8 +700,6 @@ namespace dxvk { DxbcRegisterValue emitBuildZeroVector( DxbcVectorType type); - ///////////////////////////////////////// - // Generic register manipulation methods DxbcRegisterValue emitRegisterBitcast( DxbcRegisterValue srcValue, DxbcScalarType dstType); @@ -930,8 +748,6 @@ namespace dxvk { DxbcRegisterValue value, DxbcOpModifiers modifiers); - /////////////////////////// - // Sparse feedback methods uint32_t emitExtractSparseTexel( uint32_t texelTypeId, uint32_t resultId); @@ -940,22 +756,16 @@ namespace dxvk { const DxbcRegister& feedbackRegister, uint32_t resultId); - //////////////////////////////// - // Pointer manipulation methods DxbcRegisterPointer emitArrayAccess( DxbcRegisterPointer pointer, spv::StorageClass sclass, uint32_t index); - /////////////////////////////////////// - // Image register manipulation methods uint32_t emitLoadSampledImage( const DxbcShaderResource& textureResource, const DxbcSampler& samplerResource, bool isDepthCompare); - //////////////////////// - // Address load methods DxbcRegisterPointer emitGetTempPtr( const DxbcRegister& operand); @@ -981,8 +791,6 @@ namespace dxvk { const DxbcRegister& operand, const DxbcRegister& address); - ////////////////////////// - // Resource query methods DxbcRegisterValue emitQueryBufferSize( const DxbcRegister& resource); @@ -999,8 +807,6 @@ namespace dxvk { const DxbcRegister& resource, DxbcRegisterValue lod); - //////////////////////////////////// - // Buffer index calculation methods DxbcRegisterValue emitCalcBufferIndexStructured( DxbcRegisterValue structId, DxbcRegisterValue structOffset, @@ -1017,8 +823,6 @@ namespace dxvk { const DxbcRegister& coordReg, const DxbcImageInfo& imageInfo); - ////////////////////////////// - // Operand load/store methods DxbcRegisterValue emitIndexLoad( DxbcRegIndex index); @@ -1045,8 +849,6 @@ namespace dxvk { const DxbcRegister& reg, DxbcRegisterValue value); - //////////////////////////// - // Input/output preparation void emitInputSetup(); void emitInputSetup(uint32_t vertexCount); @@ -1055,8 +857,6 @@ namespace dxvk { void emitInitWorkgroupMemory(); - ////////////////////////////////////////// - // System value load methods (per shader) DxbcRegisterValue emitVsSystemValueLoad( DxbcSystemValue sv, DxbcRegMask mask); @@ -1070,8 +870,6 @@ namespace dxvk { DxbcSystemValue sv, DxbcRegMask mask); - /////////////////////////////////////////// - // System value store methods (per shader) void emitVsSystemValueStore( DxbcSystemValue sv, DxbcRegMask mask, @@ -1097,8 +895,6 @@ namespace dxvk { DxbcRegMask mask, const DxbcRegisterValue& value); - /////////////////////////////// - // Special system value stores void emitClipCullStore( DxbcSystemValue sv, uint32_t dstArray); @@ -1109,8 +905,6 @@ namespace dxvk { void emitPointSizeStore(); - ////////////////////////////////////// - // Common function definition methods void emitInit(); void emitFunctionBegin( @@ -1124,8 +918,6 @@ namespace dxvk { void emitMainFunctionBegin(); - ///////////////////////////////// - // Shader initialization methods void emitVsInit(); void emitHsInit(); void emitDsInit(); @@ -1133,8 +925,6 @@ namespace dxvk { void emitPsInit(); void emitCsInit(); - /////////////////////////////// - // Shader finalization methods void emitVsFinalize(); void emitHsFinalize(); void emitDsFinalize(); @@ -1142,16 +932,12 @@ namespace dxvk { void emitPsFinalize(); void emitCsFinalize(); - /////////////////////// - // Xfb related methods void emitXfbOutputDeclarations(); void emitXfbOutputSetup( uint32_t streamId, bool passthrough); - /////////////////////////////// - // Hull shader phase methods void emitHsControlPointPhase( const DxbcCompilerHsControlPointPhase& phase); @@ -1174,8 +960,6 @@ namespace dxvk { spv::StorageClass storageClass, uint32_t vertexCount); - ////////////// - // Misc stuff void emitDclInputArray( uint32_t vertexCount); @@ -1194,8 +978,6 @@ namespace dxvk { void emitFloatControl(); - /////////////////////////////// - // Variable definition methods uint32_t emitNewVariable( const DxbcRegisterInfo& info); @@ -1212,8 +994,6 @@ namespace dxvk { uint32_t emitPushConstants(); - //////////////// - // Misc methods DxbcCfgBlock* cfgFindBlock( const std::initializer_list& types); @@ -1265,8 +1045,6 @@ namespace dxvk { uint64_t readMask, uint64_t writeMask); - /////////////////////////// - // Type definition methods uint32_t getScalarTypeId( DxbcScalarType type); diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_decoder.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_decoder.h index 771837152..4ad3f82c8 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_decoder.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_decoder.h @@ -14,12 +14,6 @@ namespace dxvk { struct DxbcRegister; - /** - * \brief Source operand modifiers - * - * These are applied after loading - * an operand register. - */ enum class DxbcRegModifier : uint32_t { Neg = 0, Abs = 1, @@ -28,32 +22,17 @@ namespace dxvk { using DxbcRegModifiers = Flags; - /** - * \brief Constant buffer binding - * - * Stores information required to - * access a constant buffer. - */ struct DxbcConstantBuffer { uint32_t varId = 0; uint32_t size = 0; }; - /** - * \brief Sampler binding - * - * Stores a sampler variable that can be - * used together with a texture resource. - */ struct DxbcSampler { uint32_t varId = 0; uint32_t typeId = 0; }; - /** - * \brief Image type information - */ struct DxbcImageInfo { spv::Dim dim = spv::Dim1D; uint32_t array = 0; @@ -63,12 +42,6 @@ namespace dxvk { }; - /** - * \brief Shader resource binding - * - * Stores a resource variable - * and associated type IDs. - */ struct DxbcShaderResource { DxbcResourceType type = DxbcResourceType::Typed; DxbcImageInfo imageInfo; @@ -84,12 +57,6 @@ namespace dxvk { }; - /** - * \brief Unordered access binding - * - * Stores a resource variable that is provided - * by a UAV, as well as associated type IDs. - */ struct DxbcUav { DxbcResourceType type = DxbcResourceType::Typed; DxbcImageInfo imageInfo; @@ -105,12 +72,6 @@ namespace dxvk { }; - /** - * \brief Component swizzle - * - * Maps vector components to - * other vector components. - */ class DxbcRegSwizzle { public: @@ -133,12 +94,6 @@ namespace dxvk { }; - /** - * \brief Component mask - * - * Enables access to certain - * subset of vector components. - */ class DxbcRegMask { public: @@ -211,12 +166,6 @@ namespace dxvk { }; - /** - * \brief System value mapping - * - * Maps a system value to a given set of - * components of an input or output register. - */ struct DxbcSvMapping { uint32_t regId; DxbcRegMask regMask; @@ -230,9 +179,6 @@ namespace dxvk { }; - /** - * \brief Instruction operand - */ struct DxbcRegister { DxbcOperandType type; DxbcScalarType dataType; @@ -252,24 +198,12 @@ namespace dxvk { }; - /** - * \brief Instruction result modifiers - * - * Modifiers that are applied - * to all destination operands. - */ struct DxbcOpModifiers { bool saturate; bool precise; }; - /** - * \brief Opcode controls - * - * Instruction-specific controls. Usually, - * only one of the members will be valid. - */ class DxbcShaderOpcodeControls { public: @@ -355,24 +289,11 @@ namespace dxvk { }; - /** - * \brief Sample controls - * - * Constant texel offset with - * values raning from -8 to 7. - */ struct DxbcShaderSampleControls { int u, v, w; }; - /** - * \brief Immediate value - * - * Immediate argument represented either - * as a 32-bit or 64-bit unsigned integer, - * or a 32-bit or 32-bit floating point number. - */ union DxbcImmediate { float f32; double f64; @@ -381,17 +302,6 @@ namespace dxvk { }; - /** - * \brief Shader instruction - * - * Note that this structure may store pointer to - * external structures, such as the original code - * buffer. This is safe to use if and only if: - * - The \ref DxbcDecodeContext that created it - * still exists and was not moved - * - The code buffer that was being decoded - * still exists and was not moved. - */ struct DxbcShaderInstruction { DxbcOpcode op; DxbcInstClass opClass; @@ -413,12 +323,6 @@ namespace dxvk { }; - /** - * \brief DXBC code slice - * - * Convenient pointer pair that allows - * reading the code word stream safely. - */ class DxbcCodeSlice { public: @@ -448,34 +352,14 @@ namespace dxvk { }; - /** - * \brief Decode context - * - * Stores data that is required to decode a single - * instruction. This data is not persistent, so it - * should be forwarded to the compiler right away. - */ class DxbcDecodeContext { public: - /** - * \brief Retrieves current instruction - * - * This is only valid after a call to \ref decode. - * \returns Reference to last decoded instruction - */ const DxbcShaderInstruction& getInstruction() const { return m_instruction; } - /** - * \brief Decodes an instruction - * - * This also advances the given code slice by the - * number of dwords consumed by the instruction. - * \param [in] code Code slice - */ void decodeInstruction(DxbcCodeSlice& code); private: @@ -487,8 +371,6 @@ namespace dxvk { std::array m_immOperands; std::array m_indices; - // Index into the indices array. Used when decoding - // instruction operands with relative indexing. uint32_t m_indexId = 0; void decodeCustomData(DxbcCodeSlice code); diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_defs.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_defs.h index 8782481af..a0792aaa0 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_defs.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_defs.h @@ -7,98 +7,66 @@ namespace dxvk { constexpr size_t DxbcMaxInterfaceRegs = 32; constexpr size_t DxbcMaxOperandCount = 8; - /** - * \brief Operand kind - * - * In the instruction format definition, this specified - * whether an operand uses an actual operand token, or - * whether it is stored as an immediate value. - */ enum class DxbcOperandKind { - DstReg, ///< Destination register - SrcReg, ///< Source register - Imm32, ///< Constant number + DstReg, + SrcReg, + Imm32, }; - /** - * \brief Instruction class - * - * Instructions with a similar format are grouped into - * instruction classes in order to make implementing - * new instructions easier. - */ enum class DxbcInstClass { - Declaration, ///< Interface or resource declaration - CustomData, ///< Immediate constant buffer - ControlFlow, ///< Control flow instructions - GeometryEmit, ///< Special geometry shader instructions - Atomic, ///< Atomic operations - AtomicCounter, ///< Atomic counter operations - Barrier, ///< Execution or memory barrier - BitExtract, ///< Bit field extract operations - BitInsert, ///< Bit field insert operations - BitScan, ///< Bit scan operations - BufferQuery, ///< Buffer query instruction - BufferLoad, ///< Structured or raw buffer load - BufferStore, ///< Structured or raw buffer store - ConvertFloat16, ///< 16-bit float packing/unpacking - ConvertFloat64, ///< 64-bit float conversion - HullShaderPhase, ///< Hull shader phase declaration - HullShaderInstCnt, ///< Hull shader phase instance count - Interpolate, ///< Input attribute interpolation - NoOperation, ///< The most useful instruction class - SparseCheckAccess, ///< Verifies sparse resource access - TextureQuery, ///< Texture query instruction - TextureQueryLod, ///< Texture LOD query instruction - TextureQueryMs, ///< Multisample texture query - TextureQueryMsPos, ///< Sample position query - TextureFetch, ///< Texture fetch instruction - TextureGather, ///< Texture gather instruction - TextureSample, ///< Texture sampling instruction - TypedUavLoad, ///< Typed UAV load - TypedUavStore, ///< Typed UAV store - VectorAlu, ///< Component-wise vector instructions - VectorCmov, ///< Component-wise conditional move - VectorCmp, ///< Component-wise vector comparison - VectorDeriv, ///< Vector derivatives - VectorDot, ///< Dot product instruction - VectorIdiv, ///< Component-wise integer division - VectorImul, ///< Component-wise integer multiplication - VectorMsad, ///< Component-wise sum of absolute difference - VectorShift, ///< Bit shift operations on vectors - VectorSinCos, ///< Sine and Cosine instruction - Undefined, ///< Instruction code not defined + Declaration, + CustomData, + ControlFlow, + GeometryEmit, + Atomic, + AtomicCounter, + Barrier, + BitExtract, + BitInsert, + BitScan, + BufferQuery, + BufferLoad, + BufferStore, + ConvertFloat16, + ConvertFloat64, + HullShaderPhase, + HullShaderInstCnt, + Interpolate, + NoOperation, + SparseCheckAccess, + TextureQuery, + TextureQueryLod, + TextureQueryMs, + TextureQueryMsPos, + TextureFetch, + TextureGather, + TextureSample, + TypedUavLoad, + TypedUavStore, + VectorAlu, + VectorCmov, + VectorCmp, + VectorDeriv, + VectorDot, + VectorIdiv, + VectorImul, + VectorMsad, + VectorShift, + VectorSinCos, + Undefined, }; - /** - * \brief Instruction operand format - * - * Stores the kind and the expected data type - * of an operand. Used when parsing instructions. - */ struct DxbcInstOperandFormat { DxbcOperandKind kind; DxbcScalarType type; }; - /** - * \brief Instruction format - * - * Defines the instruction class as well as - * the format of the insttruction operands. - */ struct DxbcInstFormat { uint32_t operandCount = 0; DxbcInstClass instructionClass = DxbcInstClass::Undefined; DxbcInstOperandFormat operands[DxbcMaxOperandCount]; }; - /** - * \brief Retrieves instruction format info - * - * \param [in] opcode The opcode to retrieve - * \returns Instruction format info - */ DxbcInstFormat dxbcInstructionFormat(DxbcOpcode opcode); } \ No newline at end of file diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_enums.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_enums.h index f6d29a5b8..531a7afd2 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_enums.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_enums.h @@ -4,9 +4,6 @@ namespace dxvk { - /** - * \brief Instruction code listing - */ enum class DxbcOpcode : uint32_t { Add = 0, And = 1, @@ -246,9 +243,6 @@ namespace dxvk { }; - /** - * \brief Extended opcode - */ enum class DxbcExtOpcode : uint32_t { Empty = 0, SampleControls = 1, @@ -257,12 +251,6 @@ namespace dxvk { }; - /** - * \brief Operand type - * - * Selects the 'register file' from which - * to retrieve an operand's value. - */ enum class DxbcOperandType : uint32_t { Temp = 0, Input = 1, @@ -310,12 +298,6 @@ namespace dxvk { }; - /** - * \brief Number of components - * - * Used by operands to determine whether the - * operand has one, four or zero components. - */ enum class DxbcComponentCount : uint32_t { Component0 = 0, Component1 = 1, @@ -323,13 +305,6 @@ namespace dxvk { }; - /** - * \brief Component selection mode - * - * When an operand has four components, the - * component selection mode deterines which - * components are used for the operation. - */ enum class DxbcRegMode : uint32_t { Mask = 0, Swizzle = 1, @@ -337,12 +312,6 @@ namespace dxvk { }; - /** - * \brief Index representation - * - * Determines how an operand - * register index is stored. - */ enum class DxbcOperandIndexRepresentation : uint32_t { Imm32 = 0, Imm64 = 1, @@ -352,18 +321,11 @@ namespace dxvk { }; - /** - * \brief Extended operand type - */ enum class DxbcOperandExt : uint32_t { OperandModifier = 1, }; - /** - * \brief Resource dimension - * The type of a resource. - */ enum class DxbcResourceDim : uint32_t { Unknown = 0, Buffer = 1, @@ -381,27 +343,19 @@ namespace dxvk { }; - /** - * \brief Resource return type - * Data type for resource read ops. - */ enum class DxbcResourceReturnType : uint32_t { Unorm = 1, Snorm = 2, Sint = 3, Uint = 4, Float = 5, - Mixed = 6, /// ? + Mixed = 6, Double = 7, - Continued = 8, /// ? - Unused = 9, /// ? + Continued = 8, + Unused = 9, }; - /** - * \brief Register component type - * Data type of a register component. - */ enum class DxbcRegisterComponentType : uint32_t { Unknown = 0, Uint32 = 1, @@ -410,9 +364,6 @@ namespace dxvk { }; - /** - * \brief Instruction return type - */ enum class DxbcInstructionReturnType : uint32_t { Float = 0, Uint = 1, @@ -493,9 +444,6 @@ namespace dxvk { using DxbcSyncFlags = Flags; - /** - * \brief Geometry shader input primitive - */ enum class DxbcPrimitive : uint32_t { Undefined = 0, Point = 1, @@ -538,9 +486,6 @@ namespace dxvk { }; - /** - * \brief Geometry shader output topology - */ enum class DxbcPrimitiveTopology : uint32_t { Undefined = 0, PointList = 1, @@ -555,9 +500,6 @@ namespace dxvk { }; - /** - * \brief Sampler operation mode - */ enum class DxbcSamplerMode : uint32_t { Default = 0, Comparison = 1, @@ -565,13 +507,6 @@ namespace dxvk { }; - /** - * \brief Scalar value type - * - * Enumerates possible register component - * types. Scalar types are represented as - * a one-component vector type. - */ enum class DxbcScalarType : uint32_t { Uint32 = 0, Uint64 = 1, @@ -583,9 +518,6 @@ namespace dxvk { }; - /** - * \brief Tessellator domain - */ enum class DxbcTessDomain : uint32_t { Undefined = 0, Isolines = 1, @@ -593,9 +525,6 @@ namespace dxvk { Quads = 3, }; - /** - * \brief Tessellator partitioning - */ enum class DxbcTessPartitioning : uint32_t { Undefined = 0, Integer = 1, @@ -604,9 +533,6 @@ namespace dxvk { FractEven = 4, }; - /** - * \brief UAV definition flags - */ enum class DxbcUavFlag : uint32_t { GloballyCoherent = 0, RasterizerOrdered = 1, @@ -614,9 +540,6 @@ namespace dxvk { using DxbcUavFlags = Flags; - /** - * \brief Tessellator output primitive - */ enum class DxbcTessOutputPrimitive : uint32_t { Undefined = 0, Point = 1, @@ -625,12 +548,6 @@ namespace dxvk { TriangleCcw = 4, }; - /** - * \brief Custom data class - * - * Stores which type of custom data is - * referenced by the instruction. - */ enum class DxbcCustomDataClass : uint32_t { Comment = 0, DebugInfo = 1, diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_header.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_header.h index 8eca0d499..f9b33aa1f 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_header.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_header.h @@ -6,12 +6,6 @@ namespace dxvk { - /** - * \brief DXBC header - * - * Stores information about the shader file itself - * and the data chunks stored inside the file. - */ class DxbcHeader { public: @@ -19,22 +13,10 @@ namespace dxvk { DxbcHeader(DxbcReader& reader); ~DxbcHeader(); - /** - * \brief Number of chunks - * \returns Chunk count - */ uint32_t numChunks() const { return m_chunkOffsets.size(); } - /** - * \brief Chunk offset - * - * Retrieves the offset of a chunk, in - * bytes, from the start of the file. - * \param [in] chunkId Chunk index - * \returns Byte offset of that chunk - */ uint32_t chunkOffset(uint32_t chunkId) const { return m_chunkOffsets.at(chunkId); } diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_modinfo.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_modinfo.h index 13e733dde..d9ff25522 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_modinfo.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_modinfo.h @@ -4,22 +4,10 @@ namespace dxvk { - /** - * \brief Tessellation info - * - * Stores the maximum tessellation factor - * to export from tessellation shaders. - */ struct DxbcTessInfo { float maxTessFactor; }; - /** - * \brief Xfb capture entry - * - * Stores an output variable to capture, - * as well as the buffer to write it to. - */ struct DxbcXfbEntry { const char* semanticName; uint32_t semanticIndex; @@ -30,13 +18,6 @@ namespace dxvk { uint32_t offset; }; - /** - * \brief Xfb info - * - * Stores capture entries and output buffer - * strides. This structure must only be - * defined if \c entryCount is non-zero. - */ struct DxbcXfbInfo { uint32_t entryCount; DxbcXfbEntry entries[128]; @@ -44,12 +25,6 @@ namespace dxvk { int32_t rasterizedStream; }; - /** - * \brief Shader module info - * - * Stores information which may affect shader compilation. - * This data can be supplied by the client API implementation. - */ struct DxbcModuleInfo { DxbcOptions options; DxbcTessInfo* tess; diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_module.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_module.h index 3ef75cfe1..ef7617b95 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_module.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_module.h @@ -11,31 +11,18 @@ #include -// References used for figuring out DXBC: -// - https://github.com/tgjones/slimshader-cpp -// - Wine namespace dxvk { class DxbcAnalyzer; class DxbcCompiler; - /** - * \brief Immediate constant buffer properties - */ struct DxbcIcbInfo { size_t size = 0u; const void* data = nullptr; }; - /** - * \brief DXBC shader module - * - * Reads the DXBC byte code and extracts information - * about the resource bindings and the instruction - * stream. A module can then be compiled to SPIR-V. - */ class DxbcModule { public: @@ -43,10 +30,6 @@ namespace dxvk { DxbcModule(DxbcReader& reader); ~DxbcModule(); - /** - * \brief Shader type - * \returns Shader type - */ std::optional programInfo() const { if (m_shexChunk == nullptr) return std::nullopt; @@ -54,21 +37,10 @@ namespace dxvk { return m_shexChunk->programInfo(); } - /** - * \brief Queries shader binding mask - * - * Only valid after successfully compiling the shader. - */ std::optional bindings() const { return m_bindings; } - /** - * \brief Retrieves immediate constant buffer info - * - * Only valid after successfully compiling the shader. - * \returns Immediate constant buffer data - */ DxbcIcbInfo icbInfo() const { DxbcIcbInfo result = { }; result.size = m_icb.size() * sizeof(uint32_t); @@ -76,37 +48,13 @@ namespace dxvk { return result; } - /** - * \brief Input and output signature chunks - * - * Parts of the D3D11 API need access to the - * input or output signature of the shader. - */ Rc isgn() const { return m_isgnChunk; } Rc osgn() const { return m_osgnChunk; } - /** - * \brief Compiles DXBC shader to SPIR-V module - * - * \param [in] moduleInfo DXBC module info - * \param [in] fileName File name, will be added to - * the compiled SPIR-V for debugging purposes. - * \returns The compiled shader object - */ SpirvCodeBuffer compile( const DxbcModuleInfo& moduleInfo, const std::string& fileName); - /** - * \brief Compiles a pass-through geometry shader - * - * Applications can pass a vertex shader to create - * a geometry shader with stream output. In this - * case, we have to create a passthrough geometry - * shader, which operates in point to point mode. - * \param [in] moduleInfo DXBC module info - * \param [in] fileName SPIR-V shader name - */ SpirvCodeBuffer compilePassthroughShader( const DxbcModuleInfo& moduleInfo, const std::string& fileName) const; diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_names.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_names.h index 52235ae59..67e337491 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_names.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_names.h @@ -23,4 +23,4 @@ namespace dxvk { std::ostream& operator << (std::ostream& os, DxbcCustomDataClass e); std::ostream& operator << (std::ostream& os, DxbcScalarType e); -} // namespace dxvk +} diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_options.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_options.h index 0fe46a767..c8856026e 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_options.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_options.h @@ -20,54 +20,34 @@ namespace dxvk { struct DxbcOptions { DxbcOptions() {} - // Clamp oDepth in fragment shaders if the depth - // clip device feature is not supported bool useDepthClipWorkaround = false; - /// Determines whether format qualifiers - /// on typed UAV loads are required bool supportsTypedUavLoadR32 = false; - /// Determines whether raw access chains are supported bool supportsRawAccessChains = false; - /// Clear thread-group shared memory to zero bool zeroInitWorkgroupMemory = false; - /// Declare vertex positions as invariant bool invariantPosition = false; - /// Insert memory barriers after TGSM stoes bool forceVolatileTgsmAccess = false; - /// Try to detect hazards in UAV access and insert - /// barriers when we know control flow is uniform. bool forceComputeUavBarriers = false; - /// Replace ld_ms with ld bool disableMsaa = false; - /// Force sample rate shading by using sample - /// interpolation for fragment shader inputs bool forceSampleRateShading = false; - // Enable per-sample interlock if supported bool enableSampleShadingInterlock = false; - /// Use tightly packed arrays for immediate - /// constant buffers if possible bool supportsTightIcbPacking = false; - /// Whether exporting point size is required bool needsPointSizeExport = true; - /// Whether to enable sincos emulation bool sincosEmulation = false; - /// Float control flags DxbcFloatControlFlags floatControl; - /// Minimum storage buffer alignment VkDeviceSize minSsboAlignment = 0; }; diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_reader.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_reader.h index a16000179..36748327f 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_reader.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_reader.h @@ -7,12 +7,6 @@ namespace dxvk { - /** - * \brief DXBC bytecode reader - * - * Holds references to the shader byte code and - * provides methods to read - */ class DxbcReader { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_tag.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_tag.h index 2ba17509c..bd9a6ce3b 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_tag.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_tag.h @@ -4,12 +4,6 @@ namespace dxvk { - /** - * \brief Four-character tag - * - * Used to identify chunks in the - * compiled DXBC file by name. - */ class DxbcTag { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_util.h b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_util.h index 7d98af6ee..686905aaa 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_util.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxbc/dxbc_util.h @@ -5,17 +5,11 @@ namespace dxvk { - /** - * \brief Push constant struct - */ struct DxbcPushConstants { uint32_t rasterizerSampleCount; }; - /** - * \brief Binding numbers and properties - */ enum DxbcBindingProperties : uint32_t { DxbcConstBufBindingIndex = 0, DxbcConstBufBindingCount = 16, @@ -33,12 +27,6 @@ namespace dxvk { }; - /** - * \brief Shader binding mask - * - * Stores a bit masks of resource bindings - * that are accessed by any given shader. - */ struct DxbcBindingMask { uint32_t cbvMask = 0u; uint32_t samplerMask = 0u; @@ -70,94 +58,41 @@ namespace dxvk { }; - /** - * \brief Computes first binding index for a given stage - * - * \param [in] stage The shader stage - * \returns Index of first binding - */ inline uint32_t computeStageBindingOffset(DxbcProgramType stage) { return DxbcStageBindingCount * uint32_t(stage); } - /** - * \brief Computes first UAV binding index offset for a given stage - * - * \param [in] stage The shader stage - * \returns Index of first UAV binding - */ inline uint32_t computeStageUavBindingOffset(DxbcProgramType stage) { return DxbcUavBindingIndex + DxbcUavBindingCount * (stage == DxbcProgramType::ComputeShader ? 2 : 0); } - /** - * \brief Computes constant buffer binding index - * - * \param [in] stage Shader stage - * \param [in] index Constant buffer index - * \returns Binding index - */ inline uint32_t computeConstantBufferBinding(DxbcProgramType stage, uint32_t index) { return computeStageBindingOffset(stage) + DxbcConstBufBindingIndex + index; } - /** - * \brief Computes sampler binding index - * - * \param [in] stage Shader stage - * \param [in] index Sampler index - * \returns Binding index - */ inline uint32_t computeSamplerBinding(DxbcProgramType stage, uint32_t index) { return computeStageBindingOffset(stage) + DxbcSamplerBindingIndex + index; } - /** - * \brief Computes resource binding index - * - * \param [in] stage Shader stage - * \param [in] index Resource index - * \returns Binding index - */ inline uint32_t computeSrvBinding(DxbcProgramType stage, uint32_t index) { return computeStageBindingOffset(stage) + DxbcResourceBindingIndex + index; } - /** - * \brief Computes UAV binding offset - * - * \param [in] stage Shader stage - * \param [in] index UAV index - * \returns Binding index - */ inline uint32_t computeUavBinding(DxbcProgramType stage, uint32_t index) { return computeStageUavBindingOffset(stage) + index; } - /** - * \brief Computes UAV counter binding offset - * - * \param [in] stage Shader stage - * \param [in] index UAV index - * \returns Binding index - */ inline uint32_t computeUavCounterBinding(DxbcProgramType stage, uint32_t index) { return computeStageUavBindingOffset(stage) + DxbcUavBindingCount + index; } - /** - * \brief Primitive vertex count - * - * Calculates the number of vertices - * for a given primitive type. - */ uint32_t primitiveVertexCount( DxbcPrimitive primitive); diff --git a/app/src/main/cpp/thirdparty/dxbc/include/dxvk/dxvk_pipelayout.h b/app/src/main/cpp/thirdparty/dxbc/include/dxvk/dxvk_pipelayout.h index 9873719c7..190724bb5 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/dxvk/dxvk_pipelayout.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/dxvk/dxvk_pipelayout.h @@ -15,12 +15,6 @@ namespace dxvk { class DxvkDevice; class DxvkPipelineManager; - /** - * \brief Order-invariant atomic access operation - * - * Information used to optimize barriers when a resource - * is accessed exlusively via order-invariant stores. - */ struct DxvkAccessOp { enum OpType : uint16_t { None = 0x0u, @@ -56,50 +50,22 @@ namespace dxvk { static_assert(sizeof(DxvkAccessOp) == sizeof(uint16_t)); - /** - * \brief Binding info - * - * Stores metadata for a single binding in - * a given shader, or for the whole pipeline. - */ struct DxvkBindingInfo { - VkDescriptorType descriptorType = VK_DESCRIPTOR_TYPE_MAX_ENUM; ///< Vulkan descriptor type - uint32_t resourceBinding = 0u; ///< API binding slot for the resource - VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_MAX_ENUM; ///< Image view type - VkShaderStageFlagBits stage = VK_SHADER_STAGE_FLAG_BITS_MAX_ENUM; ///< Shader stage - VkAccessFlags access = 0u; ///< Access mask for the resource - DxvkAccessOp accessOp = DxvkAccessOp::None; ///< Order-invariant store type, if any - bool uboSet = false; ///< Whether to include this in the UBO set - bool isMultisampled = false; ///< Multisampled binding + VkDescriptorType descriptorType = VK_DESCRIPTOR_TYPE_MAX_ENUM; + uint32_t resourceBinding = 0u; + VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_MAX_ENUM; + VkShaderStageFlagBits stage = VK_SHADER_STAGE_FLAG_BITS_MAX_ENUM; + VkAccessFlags access = 0u; + DxvkAccessOp accessOp = DxvkAccessOp::None; + bool uboSet = false; + bool isMultisampled = false; - /** - * \brief Computes descriptor set index for the given binding - * - * This is determines based on the shader stages that use the binding. - * \returns Descriptor set index - */ uint32_t computeSetIndex() const; - /** - * \brief Numeric value of the binding - * - * Used when sorting bindings. - * \returns Numeric value - */ uint32_t value() const; - /** - * \brief Checks for equality - * - * \param [in] other Binding to compare to - * \returns \c true if both bindings are equal - */ bool eq(const DxvkBindingInfo& other) const; - /** - * \brief Hashes binding info - * \returns Binding hash - */ size_t hash() const; }; diff --git a/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_code_buffer.h b/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_code_buffer.h index 4e33d6dcb..ed8a2c36e 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_code_buffer.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_code_buffer.h @@ -8,13 +8,6 @@ namespace dxvk { - /** - * \brief SPIR-V code buffer - * - * Helper class for generating SPIR-V shaders. - * Stores arbitrary SPIR-V instructions in a - * format that can be read by Vulkan drivers. - */ class SpirvCodeBuffer { public: @@ -35,193 +28,62 @@ namespace dxvk { SpirvCodeBuffer &operator=(const SpirvCodeBuffer &) = default; SpirvCodeBuffer &operator=(SpirvCodeBuffer &&) = default; - /** - * \brief Code data - * \returns Code data - */ const uint32_t* data() const { return m_code.data(); } uint32_t* data() { return m_code.data(); } - /** - * \brief Code size, in dwords - * \returns Code size, in dwords - */ uint32_t dwords() const { return m_code.size(); } - /** - * \brief Code size, in bytes - * \returns Code size, in bytes - */ size_t size() const { return m_code.size() * sizeof(uint32_t); } - /** - * \brief Begin instruction iterator - * - * Points to the first instruction in the instruction - * block. The header, if any, will be skipped over. - * \returns Instruction iterator - */ SpirvInstructionIterator begin() { return SpirvInstructionIterator( m_code.data(), 0, m_code.size()); } - /** - * \brief End instruction iterator - * - * Points to the end of the instruction block. - * \returns Instruction iterator - */ SpirvInstructionIterator end() { return SpirvInstructionIterator(nullptr, 0, 0); } - /** - * \brief Allocates a new ID - * - * Returns a new valid ID and increments the - * maximum ID count stored in the header. - * \returns The new SPIR-V ID - */ uint32_t allocId(); - /** - * \brief Appends an instruction - * - * Slightly faster than individually adding words. - * \param [in] ins Instruction - */ void append(const SpirvInstruction& ins); - /** - * \brief Merges two code buffers - * - * This is useful to generate declarations or - * the SPIR-V header at the same time as the - * code when doing so in advance is impossible. - * \param [in] other Code buffer to append - */ void append(const SpirvCodeBuffer& other); - /** - * \brief Appends an 32-bit word to the buffer - * \param [in] word The word to append - */ void putWord(uint32_t word); - /** - * \brief Appends an instruction word to the buffer - * - * Adds a single word containing both the word count - * and the op code number for a single instruction. - * \param [in] opCode Operand code - * \param [in] wordCount Number of words - */ void putIns(spv::Op opCode, uint16_t wordCount); - /** - * \brief Appends a 32-bit integer to the buffer - * \param [in] value The number to add - */ void putInt32(uint32_t word); - /** - * \brief Appends a 64-bit integer to the buffer - * - * A 64-bit integer will take up two 32-bit words. - * \param [in] value 64-bit value to add - */ void putInt64(uint64_t value); - /** - * \brief Appends a 32-bit float to the buffer - * \param [in] value The number to add - */ void putFloat32(float value); - /** - * \brief Appends a 64-bit float to the buffer - * \param [in] value The number to add - */ void putFloat64(double value); - /** - * \brief Appends a literal string to the buffer - * \param [in] str String to append to the buffer - */ void putStr(const char* str); - /** - * \brief Adds the header to the buffer - * - * \param [in] version SPIR-V version - * \param [in] boundIds Number of bound IDs - */ void putHeader(uint32_t version, uint32_t boundIds); - /** - * \brief Erases given number of dwords - * - * Removes data from the code buffer, starting - * at the current insertion offset. - * \param [in] size Number of words to remove - */ void erase(size_t size); - /** - * \brief Computes length of a literal string - * - * \param [in] str The string to check - * \returns Number of words consumed by a string - */ uint32_t strLen(const char* str); - /** - * \brief Stores the SPIR-V module to a stream - * - * The ability to save modules to a file - * exists mostly for debugging purposes. - * \param [in] stream Output stream - */ void store(std::ostream& stream) const; - /** - * \brief Retrieves current insertion pointer - * - * Sometimes it may be necessay to insert code into the - * middle of the stream rather than appending it. This - * retrieves the current function pointer. Note that the - * pointer will become invalid if any code is inserted - * before the current pointer location. - * \returns Current instruction pointr - */ size_t getInsertionPtr() const { return m_ptr; } - /** - * \brief Sets insertion pointer to a specific value - * - * Sets the insertion pointer to a value that was - * previously retrieved by \ref getInsertionPtr. - * \returns Current instruction pointr - */ void beginInsertion(size_t ptr) { m_ptr = ptr; } - /** - * \brief Sets insertion pointer to the end - * - * After this call, new instructions will be - * appended to the stream. In other words, - * this will restore default behaviour. - * \returns Previous instruction pointer - */ size_t endInsertion() { return std::exchange(m_ptr, m_code.size()); } diff --git a/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_instruction.h b/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_instruction.h index 1f9314157..cfa932ea6 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_instruction.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_instruction.h @@ -4,13 +4,6 @@ namespace dxvk { - /** - * \brief SPIR-V instruction - * - * Helps parsing a single instruction, providing - * access to the op code, instruction length and - * instruction arguments. - */ class SpirvInstruction { public: @@ -19,64 +12,29 @@ namespace dxvk { SpirvInstruction(uint32_t* code, uint32_t offset, uint32_t length) : m_code(code), m_offset(offset), m_length(length) { } - /** - * \brief SPIR-V Op code - * \returns The op code - */ spv::Op opCode() const { return static_cast( this->arg(0) & spv::OpCodeMask); } - /** - * \brief Instruction length - * \returns Number of DWORDs - */ uint32_t length() const { return this->arg(0) >> spv::WordCountShift; } - /** - * \brief Instruction offset - * \returns Offset in DWORDs - */ uint32_t offset() const { return m_offset; } - /** - * \brief Argument value - * - * Retrieves an argument DWORD. Note that some instructions - * take 64-bit arguments which require more than one DWORD. - * Arguments start at index 1. Calling this method with an - * argument ID of 0 will return the opcode token. - * \param [in] idx Argument index, starting at 1 - * \returns The argument value - */ uint32_t arg(uint32_t idx) const { const uint32_t index = m_offset + idx; return index < m_length ? m_code[index] : 0; } - /** - * \brief Argument string - * - * Retrieves a pointer to a UTF-8-encoded string. - * \param [in] idx Argument index, starting at 1 - * \returns Pointer to the literal string - */ const char* chr(uint32_t idx) const { const uint32_t index = m_offset + idx; return index < m_length ? reinterpret_cast(&m_code[index]) : nullptr; } - /** - * \brief Changes the value of an argument - * - * \param [in] idx Argument index, starting at 1 - * \param [in] word New argument word - */ void setArg(uint32_t idx, uint32_t word) const { if (m_offset + idx < m_length) m_code[m_offset + idx] = word; @@ -91,12 +49,6 @@ namespace dxvk { }; - /** - * \brief SPIR-V instruction iterator - * - * Convenient iterator that can be used - * to process raw SPIR-V shader code. - */ class SpirvInstructionIterator { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_module.h b/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_module.h index 02b7b7cf7..6827d1b8b 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_module.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/spirv/spirv_module.h @@ -45,14 +45,6 @@ namespace dxvk { return (major << 16) | (minor << 8); } - /** - * \brief SPIR-V module - * - * This class generates a code buffer containing a full - * SPIR-V shader module. Ensures that the module layout - * is valid, as defined in the SPIR-V 1.0 specification, - * section 2.4 "Logical Layout of a Module". - */ class SpirvModule { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/GLSL.std.450.h b/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/GLSL.std.450.h index 0594f907a..6b862f16e 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/GLSL.std.450.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/GLSL.std.450.h @@ -13,7 +13,7 @@ ** ** MODIFICATIONS TO THIS FILE MAY MEAN IT NO LONGER ACCURATELY REFLECTS KHRONOS ** STANDARDS. THE UNMODIFIED, NORMATIVE VERSIONS OF KHRONOS SPECIFICATIONS AND -** HEADER INFORMATION ARE LOCATED AT https://www.khronos.org/registry/ +** HEADER INFORMATION ARE LOCATED AT https://www.khronos.org/registry/ ** ** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS ** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, @@ -31,7 +31,7 @@ static const int GLSLstd450Version = 100; static const int GLSLstd450Revision = 3; enum GLSLstd450 { - GLSLstd450Bad = 0, // Don't use + GLSLstd450Bad = 0, GLSLstd450Round = 1, GLSLstd450RoundEven = 2, @@ -71,8 +71,8 @@ enum GLSLstd450 { GLSLstd450Determinant = 33, GLSLstd450MatrixInverse = 34, - GLSLstd450Modf = 35, // second operand needs an OpVariable to write to - GLSLstd450ModfStruct = 36, // no OpVariable operand + GLSLstd450Modf = 35, + GLSLstd450ModfStruct = 36, GLSLstd450FMin = 37, GLSLstd450UMin = 38, GLSLstd450SMin = 39, @@ -83,13 +83,13 @@ enum GLSLstd450 { GLSLstd450UClamp = 44, GLSLstd450SClamp = 45, GLSLstd450FMix = 46, - GLSLstd450IMix = 47, // Reserved + GLSLstd450IMix = 47, GLSLstd450Step = 48, GLSLstd450SmoothStep = 49, GLSLstd450Fma = 50, - GLSLstd450Frexp = 51, // second operand needs an OpVariable to write to - GLSLstd450FrexpStruct = 52, // no OpVariable operand + GLSLstd450Frexp = 51, + GLSLstd450FrexpStruct = 52, GLSLstd450Ldexp = 53, GLSLstd450PackSnorm4x8 = 54, @@ -128,4 +128,4 @@ enum GLSLstd450 { GLSLstd450Count }; -#endif // #ifndef GLSLstd450_H +#endif diff --git a/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/spirv.hpp b/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/spirv.hpp index f439e472f..945e75a07 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/spirv.hpp +++ b/app/src/main/cpp/thirdparty/dxbc/include/spirv/thirdparty/spirv.hpp @@ -22,27 +22,7 @@ // FROM,OUT OF OR IN CONNECTION WITH THE MATERIALS OR THE USE OR OTHER DEALINGS // IN THE MATERIALS. -// This header is automatically generated by the same tool that creates -// the Binary Section of the SPIR-V specification. -// Enumeration tokens for SPIR-V, in various styles: -// C, C++, C++11, JSON, Lua, Python, C#, D, Beef -// -// - C will have tokens with a "Spv" prefix, e.g.: SpvSourceLanguageGLSL -// - C++ will have tokens in the "spv" name space, e.g.: spv::SourceLanguageGLSL -// - C++11 will use enum classes in the spv namespace, e.g.: spv::SourceLanguage::GLSL -// - Lua will use tables, e.g.: spv.SourceLanguage.GLSL -// - Python will use dictionaries, e.g.: spv['SourceLanguage']['GLSL'] -// - C# will use enum classes in the Specification class located in the "Spv" namespace, -// e.g.: Spv.Specification.SourceLanguage.GLSL -// - D will have tokens under the "spv" module, e.g: spv.SourceLanguage.GLSL -// - Beef will use enum classes in the Specification class located in the "Spv" namespace, -// e.g.: Spv.Specification.SourceLanguage.GLSL -// -// Some tokens act like mask values, which can be OR'd together, -// while others are mutually exclusive. The mask-like ones have -// "Mask" in their name, and a parallel enum that has the shift -// amount (1 << x) for each corresponding enumerant. #ifndef spirv_HPP #define spirv_HPP @@ -2398,7 +2378,7 @@ enum Op { inline void HasResultAndType(Op opcode, bool *hasResult, bool *hasResultType) { *hasResult = *hasResultType = false; switch (opcode) { - default: /* unknown opcode */ break; + default: break; case OpNop: *hasResult = false; *hasResultType = false; break; case OpUndef: *hasResult = true; *hasResultType = true; break; case OpSourceContinued: *hasResult = false; *hasResultType = false; break; @@ -5144,9 +5124,8 @@ inline const char* OpToString(Op value) { } } -#endif /* SPV_ENABLE_UTILITY_CODE */ +#endif -// Overload bitwise operators for mask bit combining inline ImageOperandsMask operator|(ImageOperandsMask a, ImageOperandsMask b) { return ImageOperandsMask(unsigned(a) | unsigned(b)); } inline ImageOperandsMask operator&(ImageOperandsMask a, ImageOperandsMask b) { return ImageOperandsMask(unsigned(a) & unsigned(b)); } @@ -5209,6 +5188,6 @@ inline RawAccessChainOperandsMask operator&(RawAccessChainOperandsMask a, RawAcc inline RawAccessChainOperandsMask operator^(RawAccessChainOperandsMask a, RawAccessChainOperandsMask b) { return RawAccessChainOperandsMask(unsigned(a) ^ unsigned(b)); } inline RawAccessChainOperandsMask operator~(RawAccessChainOperandsMask a) { return RawAccessChainOperandsMask(~unsigned(a)); } -} // end namespace spv +} -#endif // #ifndef spirv_HPP +#endif diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/log/log.h b/app/src/main/cpp/thirdparty/dxbc/include/util/log/log.h index b12428c08..58075723b 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/log/log.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/log/log.h @@ -14,12 +14,6 @@ namespace dxvk { None = 5, }; - /** - * \brief Logger - * - * Logger for one DLL. Creates a text file and - * writes all log messages to that file. - */ class Logger { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc.h b/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc.h index b92cf00b4..b84654c4f 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc.h @@ -6,25 +6,14 @@ namespace dxvk { - /** - * \brief Reference-counted object - */ class RcObject { public: - /** - * \brief Increments reference count - * \returns New reference count - */ force_inline uint32_t incRef() { return ++m_refCount; } - /** - * \brief Decrements reference count - * \returns New reference count - */ force_inline uint32_t decRef() { return --m_refCount; } diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc_ptr.h b/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc_ptr.h index ac465758c..c0d1e0778 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc_ptr.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/rc/util_rc_ptr.h @@ -8,13 +8,6 @@ namespace dxvk { - /** - * \brief Pointer for reference-counted objects - * - * This only requires the given type to implement \c incRef - * and \c decRef methods that adjust the reference count. - * \tparam T Object type - */ template class Rc { template @@ -108,36 +101,15 @@ namespace dxvk { return m_object != nullptr; } - /** - * \brief Sets pointer without acquiring a reference - * - * Must only be use when a reference has been taken via - * other means. - * \param [in] object Object pointer - */ void unsafeInsert(T* object) { this->decRef(); m_object = object; } - /** - * \brief Extracts raw pointer - * - * Sets the smart pointer to null without decrementing the - * reference count. Must only be used when the reference - * count is decremented in some other way. - * \returns Pointer to owned object - */ T* unsafeExtract() { return std::exchange(m_object, nullptr); } - /** - * \brief Creates smart pointer without taking reference - * - * Must only be used when a refernece has been obtained via other means. - * \param [in] object Pointer to object to take ownership of - */ static Rc unsafeCreate(T* object) { return Rc(object, false); } @@ -159,7 +131,6 @@ namespace dxvk { if constexpr (std::is_void_vdecRef())>) { m_object->decRef(); } else { - // Deprecated, objects should manage themselves now. if (!m_object->decRef()) delete m_object; } diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/util_bit.h b/app/src/main/cpp/thirdparty/dxbc/include/util/util_bit.h index 2a5302581..7df34d081 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/util_bit.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/util_bit.h @@ -72,11 +72,6 @@ namespace dxvk::bit { #elif defined(__BMI__) return __tzcnt_u32(n); #elif defined(DXVK_ARCH_X86) && (defined(__GNUC__) || defined(__clang__)) - // tzcnt is encoded as rep bsf, so we can use it on all - // processors, but the behaviour of zero inputs differs: - // - bsf: zf = 1, cf = ?, result = ? - // - tzcnt: zf = 0, cf = 1, result = 32 - // We'll have to handle this case manually. uint32_t res; uint32_t tmp; asm ( @@ -223,14 +218,6 @@ namespace dxvk::bit { } - /** - * \brief Clears cache lines of memory - * - * Uses non-temporal stores. The memory region offset - * and size are assumed to be aligned to 64 bytes. - * \param [in] mem Memory region to clear - * \param [in] size Number of bytes to clear - */ inline void bclear(void* mem, size_t size) { #if defined(DXVK_ARCH_X86) && (defined(__GNUC__) || defined(__clang__) || defined(_MSC_VER)) auto zero = _mm_setzero_si128(); @@ -253,13 +240,6 @@ namespace dxvk::bit { } - /** - * \brief Compares two aligned structs bit by bit - * - * \param [in] a First struct - * \param [in] b Second struct - * \returns \c true if the structs are equal - */ template bool bcmpeq(const T* a, const T* b) { static_assert(alignof(T) >= 16); @@ -319,7 +299,6 @@ namespace dxvk::bit { uint32_t dword = 0; uint32_t bit = idx; - // Compiler doesn't remove this otherwise. if constexpr (Dwords > 1) { dword = idx / 32; bit = idx % 32; @@ -332,7 +311,6 @@ namespace dxvk::bit { uint32_t dword = 0; uint32_t bit = idx; - // Compiler doesn't remove this otherwise. if constexpr (Dwords > 1) { dword = idx / 32; bit = idx % 32; @@ -354,7 +332,6 @@ namespace dxvk::bit { uint32_t dword = 0; uint32_t bit = idx; - // Compiler doesn't remove this otherwise. if constexpr (Dwords > 1) { dword = idx / 32; bit = idx % 32; @@ -594,17 +571,6 @@ namespace dxvk::bit { }; - /** - * \brief Encodes float as fixed point - * - * Rounds away from zero. If this is not suitable for - * certain use cases, implement round to nearest even. - * \tparam T Integer type, may be signed - * \tparam I Integer bits - * \tparam F Fractional bits - * \param n Float to encode - * \returns Encoded fixed-point value - */ template T encodeFixed(float n) { if (n != n) @@ -631,18 +597,8 @@ namespace dxvk::bit { } - /** - * \brief Decodes fixed-point integer to float - * - * \tparam T Integer type, may be signed - * \tparam I Integer bits - * \tparam F Fractional bits - * \param n Number to decode - * \returns Decoded number - */ template float decodeFixed(T n) { - // Sign-extend as necessary if constexpr (std::is_signed_v) n -= (n & (T(1u) << (I + F - 1u))) << 1u; @@ -650,9 +606,6 @@ namespace dxvk::bit { } - /** - * \brief Inserts one null bit after each bit - */ inline uint32_t split2(uint32_t c) { c = (c ^ (c << 8u)) & 0x00ff00ffu; c = (c ^ (c << 4u)) & 0x0f0f0f0fu; @@ -662,9 +615,6 @@ namespace dxvk::bit { } - /** - * \brief Inserts two null bits after each bit - */ inline uint64_t split3(uint64_t c) { c = (c | c << 32u) & 0x001f00000000ffffull; c = (c | c << 16u) & 0x001f0000ff0000ffull; @@ -675,32 +625,16 @@ namespace dxvk::bit { } - /** - * \brief Interleaves bits from two integers - * - * Both numbers must fit into 16 bits. - * \param [in] x X coordinate - * \param [in] y Y coordinate - * \returns Morton code of x and y - */ inline uint32_t interleave(uint16_t x, uint16_t y) { return split2(x) | (split2(y) << 1u); } - /** - * \brief Interleaves bits from three integers - * - * All three numbers must fit into 16 bits. - */ inline uint64_t interleave(uint16_t x, uint16_t y, uint16_t z) { return split3(x) | (split3(y) << 1u) | (split3(z) << 2u); } - /** - * \brief 48-bit integer storage type - */ struct uint48_t { explicit uint48_t(uint64_t n) : a(uint16_t(n)), b(uint16_t(n >> 16)), c(uint16_t(n >> 32)) { } @@ -710,7 +644,6 @@ namespace dxvk::bit { uint16_t c; explicit operator uint64_t () const { - // GCC generates worse code if we promote to uint64 directly uint32_t lo = uint32_t(a) | (uint32_t(b) << 16); return uint64_t(lo) | (uint64_t(c) << 32); } diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/util_error.h b/app/src/main/cpp/thirdparty/dxbc/include/util/util_error.h index 2cfd45ffd..5790952f6 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/util_error.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/util_error.h @@ -4,12 +4,6 @@ namespace dxvk { - /** - * \brief DXVK error - * - * A generic exception class that stores a - * message. Exceptions should be logged. - */ class DxvkError { public: diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/util_math.h b/app/src/main/cpp/thirdparty/dxbc/include/util/util_math.h index 9276d9337..c26bf7c87 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/util_math.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/util_math.h @@ -24,9 +24,6 @@ namespace dxvk { return (what / to) * to; } - // Equivalent of std::clamp for use with floating point numbers - // Handles (-){INFINITY,NAN} cases. - // Will return min in cases of NAN, etc. inline float fclamp(float value, float min, float max) { return std::fmin( std::fmax(value, min), max); diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/util_small_vector.h b/app/src/main/cpp/thirdparty/dxbc/include/util/util_small_vector.h index 861c9e0e0..b9091efd8 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/util_small_vector.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/util_small_vector.h @@ -193,7 +193,6 @@ namespace dxvk { } u; size_t pick_capacity(size_t n) { - // Pick next largest power of two for the new capacity return size_t(1u) << ((sizeof(n) * 8u) - bit::lzcnt(n - 1)); } diff --git a/app/src/main/cpp/thirdparty/dxbc/include/util/util_string.h b/app/src/main/cpp/thirdparty/dxbc/include/util/util_string.h index 5fe00fbf5..e8ca7f2d0 100644 --- a/app/src/main/cpp/thirdparty/dxbc/include/util/util_string.h +++ b/app/src/main/cpp/thirdparty/dxbc/include/util/util_string.h @@ -48,15 +48,6 @@ namespace dxvk::str { uint32_t* end, uint32_t ch); - /** - * \brief Decodes a single character - * - * Note that \c begin and \c end must not be equal. - * \param [in] begin Pointer to current position within the input string - * \param [in] end Pointer to the end of the input string - * \param [out] ch Pointer to the decoded character code - * \returns Pointer to next character in the input string - */ template const T* decodeChar( const T* begin, @@ -72,18 +63,6 @@ namespace dxvk::str { return reinterpret_cast(result); } - /** - * \brief Encodes a character - * - * Note that \c begin and \c end may be both be \c nullptr or equal, in - * which case only the length of the encoded character will be returned. - * \param [in] begin Pointer to current position within the output string - * \param [in] end Pointer to the end of the output string - * \param [in] ch Character to encode - * \returns If begin is \c nullptr , the number of units required to encode - * the character. Otherwise, the number of units written to the output. - * This may return \c 0 for characters that cannot be written or encoded. - */ template size_t encodeChar( T* begin, @@ -97,13 +76,6 @@ namespace dxvk::str { ch); } - /** - * \brief Computes length of a null-terminated string - * - * \param [in] begin Start of input string - * \returns Number of characters in input string, - * excluding the terminating null character - */ template size_t length(const S* string) { size_t result = 0; @@ -114,23 +86,6 @@ namespace dxvk::str { return result; } - /** - * \brief Converts string from one encoding to another - * - * The output string arguments may be \c nullptr. In that case, the - * total length of the transcoded string will be returned, in units - * of the output character type. The output string will only be - * null-terminated if the input string is also null-terminated. - * \tparam D Output character type - * \tparam S Input character type - * \param [in] dstBegin Start of output string - * \param [in] dstLength Length of output string - * \param [in] srcBegin Start of input string - * \param [in] srcLength Length of input string - * \returns If \c dstBegin is \c nullptr , the total number of output - * characters required to store the output string. Otherwise, the - * total number of characters written to the output string. - */ template size_t transcodeString( D* dstBegin, @@ -159,20 +114,8 @@ namespace dxvk::str { return totalLength; } - /** - * \brief Creates string object from wide char array - * - * \param [in] ws Null-terminated wide string - * \returns Regular string object - */ std::string fromws(const wchar_t* ws); - /** - * \brief Creates wide string object from char array - * - * \param [in] mbs Null-terminated string - * \returns Wide string object - */ std::wstring tows(const char* mbs); #ifdef _WIN32 @@ -211,25 +154,15 @@ namespace dxvk::str { } } - /** - * \brief Split string at one or more delimiters characters - * - * \param [in] string String to split - * \param [in] delims Delimiter characters - * \returns Vector of substring views - */ inline std::vector split(std::string_view string, std::string_view delims = " ") { std::vector tokens; for (size_t start = 0; start < string.size(); ) { - // Find first delimiter const auto end = string.find_first_of(delims, start); - // Add non-empty tokens if (start != end) tokens.emplace_back(string.substr(start, end-start)); - // Break at the end of string if (end == std::string_view::npos) break; diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_analysis.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_analysis.cpp index 6064e9ee5..138939e83 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_analysis.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_analysis.cpp @@ -13,9 +13,6 @@ namespace dxvk { m_osgn (osgn), m_psgn (psgn), m_analysis(&analysis) { - // Get number of clipping and culling planes from the - // input and output signatures. We will need this to - // declare the shader input and output interfaces. m_analysis->clipCullIn = getClipCullInfo(m_isgn); m_analysis->clipCullOut = getClipCullInfo(m_osgn); } @@ -36,7 +33,6 @@ namespace dxvk { m_analysis->uavInfos[registerId].accessAtomicOp = true; m_analysis->uavInfos[registerId].accessFlags |= VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; - // Check whether the atomic operation is order-invariant DxvkAccessOp op = DxvkAccessOp::None; switch (ins.op) { @@ -104,8 +100,6 @@ namespace dxvk { const uint32_t registerId = ins.dst[0].idx[0].offset; m_analysis->uavInfos[registerId].accessFlags |= VK_ACCESS_SHADER_WRITE_BIT; - // The UAV format may change between dispatches, so be conservative here - // and only allow this optimization when the app is writing zeroes. DxvkAccessOp storeOp = getStoreAccessOp(DxbcRegMask(0xf), ins.src[1u]); if (storeOp != DxvkAccessOp(DxvkAccessOp::OpType::StoreUi, 0u)) @@ -190,7 +184,6 @@ namespace dxvk { if (m_analysis->uavInfos[uav].accessOp == DxvkAccessOp::None) m_analysis->uavInfos[uav].accessOp = op; - // Maintain ordering if the UAV is accessed via other operations as well if (op == DxvkAccessOp::None || m_analysis->uavInfos[uav].accessOp != op) m_analysis->uavInfos[uav].nonInvariantAccess = true; } @@ -200,31 +193,26 @@ namespace dxvk { if (src.type != DxbcOperandType::Imm32) return DxvkAccessOp::None; - // Trivial case, same value is written to all components if (src.componentCount == DxbcComponentCount::Component1) return getConstantStoreOp(src.imm.u32_1); if (src.componentCount != DxbcComponentCount::Component4) return DxvkAccessOp::None; - // Otherwise, make sure that all written components are equal DxvkAccessOp op = DxvkAccessOp::None; for (uint32_t i = 0u; i < 4u; i++) { if (!writeMask[i]) continue; - // If the written value can't be represented, skip DxvkAccessOp scalarOp = getConstantStoreOp(src.imm.u32_4[i]); if (scalarOp == DxvkAccessOp::None) return DxvkAccessOp::None; - // First component written if (op == DxvkAccessOp::None) op = scalarOp; - // Conflicting store ops if (op != scalarOp) return DxvkAccessOp::None; } diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_isgn.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_isgn.cpp index 80e540f23..6291ac901 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_isgn.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_isgn.cpp @@ -11,7 +11,6 @@ namespace dxvk { DxbcScalarType::Sint32, DxbcScalarType::Float32, }; - // https://github.com/DarkStarSword/3d-fixes/blob/master/dx11shaderanalyse.py#L101 bool hasStream = (tag == "ISG1") || (tag == "OSG1") || (tag == "PSG1") || (tag == "OSG5"); bool hasPrecision = (tag == "ISG1") || (tag == "OSG1") || (tag == "PSG1"); diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_shex.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_shex.cpp index 552329b88..3e5d7d36f 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_shex.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_chunk_shex.cpp @@ -3,14 +3,10 @@ namespace dxvk { DxbcShex::DxbcShex(DxbcReader reader) { - // The shader version and type are stored in a 32-bit unit, - // where the first byte contains the major and minor version - // numbers, and the high word contains the program type. reader.skip(2); auto pType = reader.readEnum(); m_programInfo = DxbcProgramInfo(pType); - // Read the actual shader code as an array of DWORDs. auto codeLength = reader.readu32() - 2; m_code.resize(codeLength); reader.read(m_code.data(), codeLength * sizeof(uint32_t)); diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_compiler.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_compiler.cpp index 0b6286fe5..388d34868 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_compiler.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_compiler.cpp @@ -20,17 +20,13 @@ namespace dxvk { m_osgn (osgn), m_psgn (psgn), m_analysis (&analysis) { - // Declare an entry point ID. We'll need it during the - // initialization phase where the execution mode is set. m_entryPointId = m_module.allocateId(); - // Set the shader name so that we recognize it in renderdoc m_module.setDebugSource( spv::SourceLanguageUnknown, 0, m_module.addDebugString(fileName.c_str()), nullptr); - // Set the memory model. This is the same for all shaders. m_module.enableCapability( spv::CapabilityVulkanMemoryModel); @@ -38,7 +34,6 @@ namespace dxvk { spv::AddressingModelLogical, spv::MemoryModelVulkan); - // Make sure our interface registers are clear for (uint32_t i = 0; i < DxbcMaxInterfaceRegs; i++) { m_vRegs.at(i) = DxbcRegisterPointer { }; m_oRegs.at(i) = DxbcRegisterPointer { }; @@ -194,7 +189,6 @@ namespace dxvk { DxbcInterpolationMode::Undefined); } - // Figure out which streams to enable uint32_t streamMask = 0; for (size_t i = 0; i < m_xfbVars.size(); i++) @@ -205,18 +199,13 @@ namespace dxvk { m_module.opEmitVertex(m_module.constu32(streamId)); } - // End the main function emitFunctionEnd(); - // For pass-through we always assume points m_inputTopology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; } SpirvCodeBuffer DxbcCompiler::finalize() { - // Depending on the shader type, this will prepare - // input registers, call various shader functions - // and write back the output registers. switch (m_programInfo.type()) { case DxbcProgramType::VertexShader: this->emitVsFinalize(); break; case DxbcProgramType::HullShader: this->emitHsFinalize(); break; @@ -227,11 +216,8 @@ namespace dxvk { default: throw DxvkError("Invalid shader stage"); } - // Emit float control mode if the extension is supported this->emitFloatControl(); - // Declare the entry point, we now have all the - // information we need, including the interfaces m_module.addEntryPoint(m_entryPointId, m_programInfo.executionModel(), "main"); m_module.setDebugName(m_entryPointId, "main"); @@ -341,9 +327,6 @@ namespace dxvk { void DxbcCompiler::emitDclIndexRange(const DxbcShaderInstruction& ins) { - // dcl_index_range has one operand: - // (0) Range start, either an input or output register - // (1) Range end uint32_t index = ins.dst[0].idxDim - 1u; DxbcIndexRange range = { }; @@ -356,19 +339,11 @@ namespace dxvk { void DxbcCompiler::emitDclTemps(const DxbcShaderInstruction& ins) { - // dcl_temps has one operand: - // (imm0) Number of temp registers - // Ignore this and declare temps on demand. } void DxbcCompiler::emitDclIndexableTemp(const DxbcShaderInstruction& ins) { - // dcl_indexable_temps has three operands: - // (imm0) Array register index (x#) - // (imm1) Number of vectors stored in the array - // (imm2) Component count of each individual vector. This is - // always 4 in fxc-generated binaries and therefore useless. const uint32_t regId = ins.imm[0].u32; DxbcRegisterInfo info; @@ -398,21 +373,9 @@ namespace dxvk { case DxbcOperandType::Input: case DxbcOperandType::Output: { - // dcl_input and dcl_output instructions - // have the following operands: - // (dst0) The register to declare - // (imm0) The system value (optional) uint32_t regDim = 0; uint32_t regIdx = 0; - // In the vertex and fragment shader stage, the - // operand indices will have the following format: - // (0) Register index - // - // In other stages, the input and output registers - // may be declared as arrays of a fixed size: - // (0) Array length - // (1) Register index if (ins.dst[0].idxDim == 2) { regDim = ins.dst[0].idx[0].offset; regIdx = ins.dst[0].idx[1].offset; @@ -425,9 +388,6 @@ namespace dxvk { return; } - // This declaration may map an output register to a system - // value. If that is the case, the system value type will - // be stored in the second operand. const bool hasSv = ins.op == DxbcOpcode::DclInputSgv || ins.op == DxbcOpcode::DclInputSiv @@ -441,8 +401,6 @@ namespace dxvk { if (hasSv) sv = static_cast(ins.imm[0].u32); - // In the pixel shader, inputs are declared with an - // interpolation mode that is part of the op token. const bool hasInterpolationMode = ins.op == DxbcOpcode::DclInputPs || ins.op == DxbcOpcode::DclInputPsSiv; @@ -452,7 +410,6 @@ namespace dxvk { if (hasInterpolationMode) im = ins.controls.interpolation(); - // Declare the actual input/output variable switch (ins.op) { case DxbcOpcode::DclInput: case DxbcOpcode::DclInputSgv: @@ -599,14 +556,10 @@ namespace dxvk { } break; case DxbcOperandType::OutputControlPointId: { - // This system value map to the invocation - // ID, which has been declared already. } break; case DxbcOperandType::InputPatchConstant: case DxbcOperandType::OutputControlPoint: { - // These have been declared as global input and - // output arrays, so there's nothing left to do. } break; case DxbcOperandType::InputGsInstanceId: { @@ -621,8 +574,6 @@ namespace dxvk { m_module.enableExtension("SPV_EXT_fragment_fully_covered"); m_module.enableCapability(spv::CapabilityFragmentFullyCoveredEXT); - // This is bool in SPIR-V but uint32 in DXBC. A bool value of - // false must be 0, and bit 1 must be set to represent true. uint32_t builtinId = emitNewBuiltinVariable({ { DxbcScalarType::Bool, 1, 0 }, spv::StorageClassInput }, @@ -660,9 +611,6 @@ namespace dxvk { DxbcRegMask regMask, DxbcSystemValue sv, DxbcInterpolationMode im) { - // Avoid declaring the same variable multiple times. - // This may happen when multiple system values are - // mapped to different parts of the same register. if (m_vRegs.at(regIdx).id == 0 && sv == DxbcSystemValue::None) { const DxbcVectorType regType = getInputRegType(regIdx); @@ -679,7 +627,6 @@ namespace dxvk { m_vRegs.at(regIdx) = { regType, varId }; - // Interpolation mode, used in pixel shaders if (im == DxbcInterpolationMode::Constant) m_module.decorate(varId, spv::DecorationFlat); @@ -706,11 +653,9 @@ namespace dxvk { } } - // Declare the input slot as defined m_inputMask |= 1u << regIdx; m_vArrayLength = std::max(m_vArrayLength, regIdx + 1); } else if (sv != DxbcSystemValue::None) { - // Add a new system value mapping if needed bool skipSv = sv == DxbcSystemValue::ClipDistance || sv == DxbcSystemValue::CullDistance; @@ -726,21 +671,15 @@ namespace dxvk { DxbcRegMask regMask, DxbcSystemValue sv, DxbcInterpolationMode im) { - // Add a new system value mapping if needed. Clip - // and cull distances are handled separately. if (sv != DxbcSystemValue::None && sv != DxbcSystemValue::ClipDistance && sv != DxbcSystemValue::CullDistance) m_oMappings.push_back({ regIdx, regMask, sv }); if (m_programInfo.type() == DxbcProgramType::HullShader) { - // Hull shaders don't use standard outputs if (getCurrentHsForkJoinPhase() != nullptr) m_hs.outputPerPatchMask |= 1 << regIdx; } else if (m_oRegs.at(regIdx).id == 0) { - // Avoid declaring the same variable multiple times. - // This may happen when multiple system values are - // mapped to different parts of the same register. const DxbcVectorType regType = getOutputRegType(regIdx); DxbcRegisterInfo info; @@ -749,14 +688,9 @@ namespace dxvk { info.type.alength = regDim; info.sclass = spv::StorageClassOutput; - // In xfb mode, we set up the actual - // output vars when emitting a vertex if (m_moduleInfo.xfb != nullptr) info.sclass = spv::StorageClassPrivate; - // In geometry shaders, don't duplicate system value outputs - // to stay within device limits. The pixel shader will read - // all GS system value outputs as system value inputs. if (m_programInfo.type() == DxbcProgramType::GeometryShader && sv != DxbcSystemValue::None) info.sclass = spv::StorageClassPrivate; @@ -766,33 +700,24 @@ namespace dxvk { if (info.sclass == spv::StorageClassOutput) { m_module.decorateLocation(varId, regIdx); - // Add index decoration for potential dual-source blending if (m_programInfo.type() == DxbcProgramType::PixelShader) m_module.decorateIndex(varId, 0); - // Declare vertex positions in all stages as invariant, even if - // this is not the last stage, to help with potential Z fighting. if (sv == DxbcSystemValue::Position && m_moduleInfo.options.invariantPosition) m_module.decorate(varId, spv::DecorationInvariant); } m_oRegs.at(regIdx) = { regType, varId }; - // Declare the output slot as defined m_outputMask |= 1u << regIdx; } } void DxbcCompiler::emitDclConstantBuffer(const DxbcShaderInstruction& ins) { - // dcl_constant_buffer has one operand with two indices: - // (0) Constant buffer register ID (cb#) - // (1) Number of constants in the buffer uint32_t bufferId = ins.dst[0].idx[0].offset; uint32_t elementCount = ins.dst[0].idx[1].offset; - // With dynamic indexing, games will often index constant buffers - // out of bounds. Declare an upper bound to stay within spec. if (ins.controls.accessType() == DxbcConstantBufferAccessType::DynamicallyIndexed) elementCount = 4096; @@ -806,15 +731,11 @@ namespace dxvk { uint32_t numConstants, uint32_t numComponents, const char* name) { - // Uniform buffer data is stored as a fixed-size array - // of 4x32-bit vectors. SPIR-V requires explicit strides. const uint32_t arrayType = m_module.defArrayTypeUnique( getVectorTypeId({ DxbcScalarType::Float32, numComponents }), m_module.constu32(numConstants)); m_module.decorateArrayStride(arrayType, sizeof(uint32_t) * numComponents); - // SPIR-V requires us to put that array into a - // struct and decorate that struct as a block. const uint32_t structType = m_module.defStructTypeUnique(1, &arrayType); m_module.decorate(structType, spv::DecorationBlock); @@ -823,15 +744,12 @@ namespace dxvk { m_module.setDebugName (structType, str::format(name, "_t").c_str()); m_module.setDebugMemberName (structType, 0, "m"); - // Variable that we'll use to access the buffer const uint32_t varId = m_module.newVar( m_module.defPointerType(structType, spv::StorageClassUniform), spv::StorageClassUniform); m_module.setDebugName(varId, name); - // Compute the DXVK binding slot index for the buffer. - // D3D11 needs to bind the actual buffers to this slot. uint32_t bindingId = computeConstantBufferBinding( m_programInfo.type(), regIdx); @@ -843,7 +761,6 @@ namespace dxvk { buf.size = numConstants; m_constantBuffers.at(regIdx) = buf; - // Store descriptor info for the shader interface DxvkBindingInfo binding = { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER }; binding.viewType = VK_IMAGE_VIEW_TYPE_MAX_ENUM; binding.access = VK_ACCESS_UNIFORM_READ_BIT; @@ -854,17 +771,12 @@ namespace dxvk { void DxbcCompiler::emitDclSampler(const DxbcShaderInstruction& ins) { - // dclSampler takes one operand: - // (dst0) The sampler register to declare const uint32_t samplerId = ins.dst[0].idx[0].offset; - // The sampler type is opaque, but we still have to - // define a pointer and a variable in oder to use it const uint32_t samplerType = m_module.defSamplerType(); const uint32_t samplerPtrType = m_module.defPointerType( samplerType, spv::StorageClassUniformConstant); - // Define the sampler variable const uint32_t varId = m_module.newVar(samplerPtrType, spv::StorageClassUniformConstant); m_module.setDebugName(varId, @@ -873,14 +785,12 @@ namespace dxvk { m_samplers.at(samplerId).varId = varId; m_samplers.at(samplerId).typeId = samplerType; - // Compute binding slot index for the sampler uint32_t bindingId = computeSamplerBinding( m_programInfo.type(), samplerId); m_module.decorateDescriptorSet(varId, 0); m_module.decorateBinding(varId, bindingId); - // Store descriptor info for the shader interface DxvkBindingInfo binding = { VK_DESCRIPTOR_TYPE_SAMPLER }; binding.viewType = VK_IMAGE_VIEW_TYPE_MAX_ENUM; binding.resourceBinding = bindingId; @@ -895,12 +805,8 @@ namespace dxvk { void DxbcCompiler::emitDclResourceTyped(const DxbcShaderInstruction& ins) { - // dclResource takes two operands: - // (dst0) The resource register ID - // (imm0) The resource return type const uint32_t registerId = ins.dst[0].idx[0].offset; - // We also handle unordered access views here const bool isUav = ins.op == DxbcOpcode::DclUavTyped; if (isUav) { @@ -909,12 +815,8 @@ namespace dxvk { m_module.enableCapability(spv::CapabilityStorageImageWriteWithoutFormat); } - // Defines the type of the resource (texture2D, ...) const DxbcResourceDim resourceType = ins.controls.resourceDim(); - // Defines the type of a read operation. DXBC has the ability - // to define four different types whereas SPIR-V only allows - // one, but in practice this should not be much of a problem. auto xType = static_cast( bit::extract(ins.imm[0].u32, 0, 3)); auto yType = static_cast( @@ -927,12 +829,9 @@ namespace dxvk { if ((xType != yType) || (xType != zType) || (xType != wType)) Logger::warn("DxbcCompiler: dcl_resource: Ignoring resource return types"); - // Declare the actual sampled type const DxbcScalarType sampledType = [xType] { switch (xType) { - // FIXME is this correct? There's no documentation about it case DxbcResourceReturnType::Mixed: return DxbcScalarType::Uint32; - // FIXME do we have to manually clamp writes to SNORM/UNORM resources? case DxbcResourceReturnType::Snorm: return DxbcScalarType::Float32; case DxbcResourceReturnType::Unorm: return DxbcScalarType::Float32; case DxbcResourceReturnType::Float: return DxbcScalarType::Float32; @@ -942,11 +841,9 @@ namespace dxvk { } }(); - // Declare the resource type const uint32_t sampledTypeId = getScalarTypeId(sampledType); const DxbcImageInfo typeInfo = getResourceType(resourceType, isUav); - // Declare additional capabilities if necessary switch (resourceType) { case DxbcResourceDim::Buffer: m_module.enableCapability(isUav @@ -967,13 +864,9 @@ namespace dxvk { break; default: - // No additional capabilities required break; } - // If the read-without-format capability is not set and this - // image is access via a typed load, or if atomic operations - // are used,, we must define the image format explicitly. spv::ImageFormat imageFormat = spv::ImageFormatUnknown; if (isUav) { @@ -983,15 +876,10 @@ namespace dxvk { imageFormat = getScalarImageFormat(sampledType); } - // We do not know whether the image is going to be used as - // a color image or a depth image yet, but we can pick the - // correct type when creating a sampled image object. const uint32_t imageTypeId = m_module.defImageType(sampledTypeId, typeInfo.dim, 0, typeInfo.array, typeInfo.ms, typeInfo.sampled, imageFormat); - // We'll declare the texture variable with the color type - // and decide which one to use when the texture is sampled. const uint32_t resourcePtrType = m_module.defPointerType( imageTypeId, spv::StorageClassUniformConstant); @@ -1001,8 +889,6 @@ namespace dxvk { m_module.setDebugName(varId, str::format(isUav ? "u" : "t", registerId).c_str()); - // Compute the DXVK binding slot index for the resource. - // D3D11 needs to bind the actual resource to this slot. uint32_t bindingId = isUav ? computeUavBinding(m_programInfo.type(), registerId) : computeSrvBinding(m_programInfo.type(), registerId); @@ -1010,8 +896,6 @@ namespace dxvk { m_module.decorateDescriptorSet(varId, 0); m_module.decorateBinding(varId, bindingId); - // Declare a specialization constant which will - // store whether or not the resource is bound. if (isUav) { DxbcUav uav; uav.type = DxbcResourceType::Typed; @@ -1053,7 +937,6 @@ namespace dxvk { m_textures.at(registerId) = res; } - // Store descriptor info for the shader interface DxvkBindingInfo binding = { }; binding.viewType = typeInfo.vtype; binding.resourceBinding = bindingId; @@ -1084,11 +967,6 @@ namespace dxvk { void DxbcCompiler::emitDclResourceRawStructured(const DxbcShaderInstruction& ins) { - // dcl_resource_raw and dcl_uav_raw take one argument: - // (dst0) The resource register ID - // dcl_resource_structured and dcl_uav_structured take two arguments: - // (dst0) The resource register ID - // (imm0) Structure stride, in bytes const uint32_t registerId = ins.dst[0].idx[0].offset; const bool isUav = ins.op == DxbcOpcode::DclUavRaw @@ -1102,11 +980,9 @@ namespace dxvk { const DxbcImageInfo typeInfo = { spv::DimBuffer, 0, 0, isUav ? 2u : 1u, VK_IMAGE_VIEW_TYPE_MAX_ENUM }; - // Declare the resource type uint32_t resTypeId = 0; uint32_t varId = 0; - // Write back resource info DxbcResourceType resType = isStructured ? DxbcResourceType::Structured : DxbcResourceType::Raw; @@ -1119,12 +995,10 @@ namespace dxvk { ? (resStride & -resStride) : 16; - // Compute the DXVK binding slot index for the resource. uint32_t bindingId = isUav ? computeUavBinding(m_programInfo.type(), registerId) : computeSrvBinding(m_programInfo.type(), registerId); - // Test whether we should use a raw SSBO for this resource bool hasSparseFeedback = isUav ? m_analysis->uavInfos[registerId].sparseFeedback : m_analysis->srvInfos[registerId].sparseFeedback; @@ -1148,8 +1022,6 @@ namespace dxvk { str::format(isUav ? "u" : "t", registerId, "_t").c_str()); m_module.setDebugMemberName(structType, 0, "m"); } else { - // Structured and raw buffers are represented as - // texel buffers consisting of 32-bit integers. m_module.enableCapability(isUav ? spv::CapabilityImageBuffer : spv::CapabilitySampledBuffer); @@ -1197,7 +1069,6 @@ namespace dxvk { m_textures.at(registerId) = res; } - // Store descriptor info for the shader interface DxvkBindingInfo binding = { }; binding.descriptorType = useRawSsbo ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER @@ -1222,7 +1093,6 @@ namespace dxvk { m_bindings.push_back(binding); - // If supported, we'll be using raw access chains to access this if (!m_hasRawAccessChains && m_moduleInfo.options.supportsRawAccessChains) { m_module.enableExtension("SPV_NV_raw_access_chains"); m_module.enableCapability(spv::CapabilityRawAccessChainsNV); @@ -1233,13 +1103,6 @@ namespace dxvk { void DxbcCompiler::emitDclThreadGroupSharedMemory(const DxbcShaderInstruction& ins) { - // dcl_tgsm_raw takes two arguments: - // (dst0) The resource register ID - // (imm0) Block size, in bytes - // dcl_tgsm_structured takes three arguments: - // (dst0) The resource register ID - // (imm0) Structure stride, in bytes - // (imm1) Structure count const bool isStructured = ins.op == DxbcOpcode::DclThreadGroupSharedMemoryStructured; const uint32_t regId = ins.dst[0].idx[0].offset; @@ -1271,9 +1134,6 @@ namespace dxvk { void DxbcCompiler::emitDclGsInputPrimitive(const DxbcShaderInstruction& ins) { - // The input primitive type is stored within in the - // control bits of the opcode token. In SPIR-V, we - // have to define an execution mode. const auto mode = [&] { switch (ins.controls.primitive()) { case DxbcPrimitive::Point: return std::make_pair(VK_PRIMITIVE_TOPOLOGY_POINT_LIST, spv::ExecutionModeInputPoints); @@ -1294,9 +1154,6 @@ namespace dxvk { void DxbcCompiler::emitDclGsOutputTopology(const DxbcShaderInstruction& ins) { - // The input primitive topology is stored within in the - // control bits of the opcode token. In SPIR-V, we have - // to define an execution mode. auto mode = [&] { switch (ins.controls.primitiveTopology()) { case DxbcPrimitiveTopology::PointList: return std::make_pair(VK_PRIMITIVE_TOPOLOGY_POINT_LIST, spv::ExecutionModeOutputPoints); @@ -1312,8 +1169,6 @@ namespace dxvk { void DxbcCompiler::emitDclMaxOutputVertexCount(const DxbcShaderInstruction& ins) { - // dcl_max_output_vertex_count has one operand: - // (imm0) The maximum number of vertices m_gs.outputVertexCount = ins.imm[0].u32; m_module.setOutputVertices(m_entryPointId, m_gs.outputVertexCount); @@ -1321,8 +1176,6 @@ namespace dxvk { void DxbcCompiler::emitDclInputControlPointCount(const DxbcShaderInstruction& ins) { - // dcl_input_control_points has the control point - // count embedded within the opcode token. if (m_programInfo.type() == DxbcProgramType::HullShader) { m_hs.vertexCountIn = ins.controls.controlPointCount(); @@ -1337,8 +1190,6 @@ namespace dxvk { void DxbcCompiler::emitDclOutputControlPointCount(const DxbcShaderInstruction& ins) { - // dcl_output_control_points has the control point - // count embedded within the opcode token. m_hs.vertexCountOut = ins.controls.controlPointCount(); m_hs.outputPerPatch = emitTessInterfacePerPatch(spv::StorageClassPrivate); @@ -1407,10 +1258,6 @@ namespace dxvk { void DxbcCompiler::emitDclThreadGroup(const DxbcShaderInstruction& ins) { - // dcl_thread_group has three operands: - // (imm0) Number of threads in X dimension - // (imm1) Number of threads in Y dimension - // (imm2) Number of threads in Z dimension m_cs.workgroupSizeX = ins.imm[0].u32; m_cs.workgroupSizeY = ins.imm[1].u32; m_cs.workgroupSizeZ = ins.imm[2].u32; @@ -1421,15 +1268,12 @@ namespace dxvk { void DxbcCompiler::emitDclGsInstanceCount(const DxbcShaderInstruction& ins) { - // dcl_gs_instance_count has one operand: - // (imm0) Number of geometry shader invocations m_module.setInvocations(m_entryPointId, ins.imm[0].u32); m_gs.invocationCount = ins.imm[0].u32; } uint32_t DxbcCompiler::emitDclUavCounter(uint32_t regId) { - // Declare a structure type which holds the UAV counter if (m_uavCtrStructType == 0) { const uint32_t t_u32 = m_module.defIntType(32, 0); const uint32_t t_struct = m_module.defStructTypeUnique(1, &t_u32); @@ -1445,7 +1289,6 @@ namespace dxvk { t_struct, spv::StorageClassStorageBuffer); } - // Declare the buffer variable const uint32_t varId = m_module.newVar( m_uavCtrPointerType, spv::StorageClassStorageBuffer); @@ -1458,7 +1301,6 @@ namespace dxvk { m_module.decorateDescriptorSet(varId, 0); m_module.decorateBinding(varId, bindingId); - // Declare the storage buffer binding DxvkBindingInfo binding = { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER }; binding.resourceBinding = bindingId; binding.viewType = VK_IMAGE_VIEW_TYPE_MAX_ENUM; @@ -1476,8 +1318,6 @@ namespace dxvk { if ((ins.customDataSize & 0x3) != 0) throw DxvkError("DxbcCompiler: Immediate constant buffer size not a multiple of four DWORDs"); - // A lot of the time we'll be dealing with a scalar or vec2 - // array here, there's no reason to emit all those zeroes. uint32_t componentCount = 1u; for (uint32_t i = 0; i < ins.customDataSize; i += 4u) { @@ -1507,7 +1347,6 @@ namespace dxvk { uint32_t dwordCount, const uint32_t* dwordArray, uint32_t componentCount) { - // Declare individual vector constants as 4x32-bit vectors small_vector vectorIds; DxbcVectorType vecType; @@ -1530,11 +1369,8 @@ namespace dxvk { vectorIds.push_back(id); } - // Pad array with one entry of zeroes so that we can - // handle out-of-bounds accesses more conveniently. vectorIds.push_back(emitBuildZeroVector(vecType).id); - // Declare the array that contains all the vectors DxbcArrayType arrInfo; arrInfo.ctype = DxbcScalarType::Uint32; arrInfo.ccount = componentCount; @@ -1544,8 +1380,6 @@ namespace dxvk { uint32_t arrayId = m_module.constComposite( arrayTypeId, vectorIds.size(), vectorIds.data()); - // Declare the variable that will hold the constant - // data and initialize it with the constant array. uint32_t pointerTypeId = m_module.defPointerType( arrayTypeId, spv::StorageClassPrivate); @@ -1567,15 +1401,11 @@ namespace dxvk { uint32_t componentCount) { uint32_t vectorCount = dwordCount / 4u; - // Tightly pack vec2 or scalar arrays if possible. Don't bother with - // vec3 since we'd rather have properly vectorized loads in that case. if (m_moduleInfo.options.supportsTightIcbPacking && componentCount <= 2u) m_icbComponents = componentCount; else m_icbComponents = 4u; - // Immediate constant buffer can be read out of bounds, declare - // it with the maximum possible size and rely on robustness. this->emitDclConstantBufferVar(Icb_BindingSlotId, 4096u, m_icbComponents, "icb"); m_icbData.reserve(vectorCount * componentCount); @@ -1618,15 +1448,11 @@ namespace dxvk { const uint32_t typeId = getVectorTypeId(dst.type); switch (ins.op) { - ///////////////////// - // Move instructions case DxbcOpcode::Mov: case DxbcOpcode::DMov: dst.id = src.at(0).id; break; - ///////////////////////////////////// - // ALU operations on float32 numbers case DxbcOpcode::Add: case DxbcOpcode::DAdd: dst.id = m_module.opFAdd(typeId, @@ -1657,8 +1483,6 @@ namespace dxvk { case DxbcOpcode::Mad: case DxbcOpcode::DFma: if (ins.controls.precise()) { - // FXC only emits precise mad if the shader explicitly uses - // the HLSL mad()/fma() intrinsics, let's preserve that. dst.id = m_module.opFFma(typeId, src.at(0).id, src.at(1).id, src.at(2).id); } else { @@ -1730,8 +1554,6 @@ namespace dxvk { typeId, src.at(0).id); break; - ///////////////////////////////////// - // ALU operations on signed integers case DxbcOpcode::IAdd: dst.id = m_module.opIAdd(typeId, src.at(0).id, src.at(1).id); @@ -1760,8 +1582,6 @@ namespace dxvk { typeId, src.at(0).id); break; - /////////////////////////////////////// - // ALU operations on unsigned integers case DxbcOpcode::UMax: dst.id = m_module.opUMax(typeId, src.at(0).id, src.at(1).id); @@ -1772,8 +1592,6 @@ namespace dxvk { src.at(0).id, src.at(1).id); break; - /////////////////////////////////////// - // Bit operations on unsigned integers case DxbcOpcode::And: dst.id = m_module.opBitwiseAnd(typeId, src.at(0).id, src.at(1).id); @@ -1804,8 +1622,6 @@ namespace dxvk { typeId, src.at(0).id); break; - /////////////////////////// - // Conversion instructions case DxbcOpcode::ItoF: dst.id = m_module.opConvertStoF( typeId, src.at(0).id); @@ -1836,19 +1652,12 @@ namespace dxvk { if (ins.controls.precise() || m_precise) m_module.decorate(dst.id, spv::DecorationNoContraction); - // Store computed value dst = emitDstOperandModifiers(dst, ins.modifiers); emitRegisterStore(ins.dst[0], dst); } void DxbcCompiler::emitVectorCmov(const DxbcShaderInstruction& ins) { - // movc and swapc have the following operands: - // (dst0) The first destination register - // (dst1) The second destination register (swapc only) - // (src0) The condition vector - // (src1) Vector to select from if the condition is not 0 - // (src2) Vector to select from if the condition is 0 DxbcRegMask condMask = ins.dst[0].mask; if (ins.dst[0].dataType == DxbcScalarType::Float64) { @@ -1864,8 +1673,6 @@ namespace dxvk { uint32_t componentCount = condMask.popCount(); - // We'll compare against a vector of zeroes to generate a - // boolean vector, which in turn will be used by OpSelect uint32_t zeroType = m_module.defIntType(32, 0); uint32_t boolType = m_module.defBoolType(); @@ -1879,8 +1686,6 @@ namespace dxvk { zero = m_module.constComposite(zeroType, componentCount, zeroVec.data()); } - // In case of swapc, the second destination operand receives - // the output that a cmov instruction would normally get const uint32_t trueIndex = ins.op == DxbcOpcode::Swapc ? 1 : 0; for (uint32_t i = 0; i < ins.dstCount; i++) { @@ -1899,14 +1704,8 @@ namespace dxvk { } void DxbcCompiler::emitVectorCmp(const DxbcShaderInstruction& ins) { - // Compare instructions have three operands: - // (dst0) The destination register - // (src0) The first vector to compare - // (src1) The second vector to compare uint32_t componentCount = ins.dst[0].mask.popCount(); - // For 64-bit operations, we'll return a 32-bit - // vector, so we have to adjust the read mask DxbcRegMask srcMask = ins.dst[0].mask; if (isDoubleType(ins.src[0].dataType)) { @@ -1920,8 +1719,6 @@ namespace dxvk { emitRegisterLoad(ins.src[1], srcMask), }; - // Condition, which is a boolean vector used - // to select between the ~0u and 0u vectors. uint32_t condition = 0; uint32_t conditionType = m_module.defBoolType(); @@ -1991,7 +1788,6 @@ namespace dxvk { return; } - // Generate constant vectors for selection uint32_t sFalse = m_module.constu32( 0u); uint32_t sTrue = m_module.constu32(~0u); @@ -2012,8 +1808,6 @@ namespace dxvk { if (invert) std::swap(sFalse, sTrue); - // Perform component-wise mask selection - // based on the condition evaluated above. result.id = m_module.opSelect( typeId, condition, sTrue, sFalse); @@ -2022,9 +1816,6 @@ namespace dxvk { void DxbcCompiler::emitVectorDeriv(const DxbcShaderInstruction& ins) { - // Derivative instructions have two operands: - // (dst0) Destination register for the derivative - // (src0) The operand to compute the derivative of DxbcRegisterValue value = emitRegisterLoad(ins.src[0], ins.dst[0].mask); const uint32_t typeId = getVectorTypeId(value.type); @@ -2096,9 +1887,6 @@ namespace dxvk { m_module.opCompositeExtract(componentType, src.at(1).id, 1, &i)); } - // Unconditionally mark as precise since the exact order of operation - // matters for some games, even if the instruction itself is not marked - // as precise. m_module.decorate(dst.id, spv::DecorationNoContraction); } @@ -2108,16 +1896,10 @@ namespace dxvk { void DxbcCompiler::emitVectorIdiv(const DxbcShaderInstruction& ins) { - // udiv has four operands: - // (dst0) Quotient destination register - // (dst1) Remainder destination register - // (src0) The first vector to compare - // (src1) The second vector to compare if (ins.dst[0].type == DxbcOperandType::Null && ins.dst[1].type == DxbcOperandType::Null) return; - // FIXME support this if applications require it if (ins.dst[0].type != DxbcOperandType::Null && ins.dst[1].type != DxbcOperandType::Null && ins.dst[0].mask != ins.dst[1].mask) { @@ -2125,8 +1907,6 @@ namespace dxvk { return; } - // Load source operands as integers with the - // mask of one non-NULL destination operand const DxbcRegMask srcMask = ins.dst[0].type != DxbcOperandType::Null ? ins.dst[0].mask @@ -2137,7 +1917,6 @@ namespace dxvk { emitRegisterLoad(ins.src[1], srcMask), }; - // Division by zero will return 0xffffffff for both results auto bvecId = getVectorTypeId({ DxbcScalarType::Bool, srcMask.popCount() }); DxbcRegisterValue const0 = emitBuildConstVecu32( 0u, 0u, 0u, 0u, srcMask); @@ -2145,8 +1924,6 @@ namespace dxvk { uint32_t cmpValue = m_module.opINotEqual(bvecId, src.at(1).id, const0.id); - // Compute results only if the destination - // operands are not NULL. if (ins.dst[0].type != DxbcOperandType::Null) { DxbcRegisterValue quotient; quotient.type.ctype = ins.dst[0].dataType; @@ -2184,17 +1961,10 @@ namespace dxvk { void DxbcCompiler::emitVectorImul(const DxbcShaderInstruction& ins) { - // imul and umul have four operands: - // (dst0) High destination register - // (dst1) Low destination register - // (src0) The first vector to compare - // (src1) The second vector to compare if (ins.dst[0].type == DxbcOperandType::Null) { if (ins.dst[1].type == DxbcOperandType::Null) return; - // If dst0 is NULL, this instruction behaves just - // like any other three-operand ALU instruction const std::array src = { emitRegisterLoad(ins.src[0], ins.dst[1].mask), emitRegisterLoad(ins.src[1], ins.dst[1].mask), @@ -2210,18 +1980,12 @@ namespace dxvk { result = emitDstOperandModifiers(result, ins.modifiers); emitRegisterStore(ins.dst[1], result); } else { - // TODO implement this Logger::warn("DxbcCompiler: Extended Imul not yet supported"); } } void DxbcCompiler::emitVectorMsad(const DxbcShaderInstruction& ins) { - // msad has four operands: - // (dst0) Destination - // (src0) Reference (packed uint8) - // (src1) Source (packed uint8) - // (src2) Accumulator DxbcRegisterValue refReg = emitRegisterLoad(ins.src[0], ins.dst[0].mask); DxbcRegisterValue srcReg = emitRegisterLoad(ins.src[1], ins.dst[0].mask); DxbcRegisterValue result = emitRegisterLoad(ins.src[2], ins.dst[0].mask); @@ -2249,10 +2013,6 @@ namespace dxvk { void DxbcCompiler::emitVectorShift(const DxbcShaderInstruction& ins) { - // Shift operations have three operands: - // (dst0) The destination register - // (src0) The register to shift - // (src1) The shift amount (scalar) DxbcRegisterValue shiftReg = emitRegisterLoad(ins.src[0], ins.dst[0].mask); DxbcRegisterValue countReg = emitRegisterLoad(ins.src[1], ins.dst[0].mask); @@ -2298,12 +2058,7 @@ namespace dxvk { void DxbcCompiler::emitVectorSinCos(const DxbcShaderInstruction& ins) { - // sincos has three operands: - // (dst0) Destination register for sin(x) - // (dst1) Destination register for cos(x) - // (src0) Source operand x - // Load source operand as 32-bit float vector. const DxbcRegisterValue srcValue = emitRegisterLoad( ins.src[0], DxbcRegMask(true, true, true, true)); @@ -2315,7 +2070,6 @@ namespace dxvk { DxbcRegisterValue cosVector = { }; cosVector.type.ctype = DxbcScalarType::Float32; - // Only compute sincos for enabled components std::array sinIds = { }; std::array cosIds = { }; @@ -2353,8 +2107,6 @@ namespace dxvk { void DxbcCompiler::emitGeometryEmit(const DxbcShaderInstruction& ins) { - // In xfb mode we might have multiple streams, so - // we have to figure out which stream to write to uint32_t streamId = 0; uint32_t streamVar = 0; @@ -2363,7 +2115,6 @@ namespace dxvk { streamVar = m_module.constu32(streamId); } - // Checking the negation is easier for EmitThenCut/EmitThenCutStream bool doEmit = ins.op != DxbcOpcode::Cut && ins.op != DxbcOpcode::CutStream; bool doCut = ins.op != DxbcOpcode::Emit && ins.op != DxbcOpcode::EmitStream; @@ -2382,26 +2133,15 @@ namespace dxvk { void DxbcCompiler::emitAtomic(const DxbcShaderInstruction& ins) { - // atomic_* operations have the following operands: - // (dst0) Destination u# or g# register - // (src0) Index into the texture or buffer - // (src1) The source value for the operation - // (src2) Second source operand (optional) - // imm_atomic_* operations have the following operands: - // (dst0) Register that receives the result - // (dst1) Destination u# or g# register - // (srcX) As above const DxbcBufferInfo bufferInfo = getBufferInfo(ins.dst[ins.dstCount - 1]); bool isImm = ins.dstCount == 2; bool isUav = ins.dst[ins.dstCount - 1].type == DxbcOperandType::UnorderedAccessView; bool isSsbo = bufferInfo.isSsbo; - // Retrieve destination pointer for the atomic operation> const DxbcRegisterPointer pointer = emitGetAtomicPointer( ins.dst[ins.dstCount - 1], ins.src[0]); - // Load source values std::array src; for (uint32_t i = 1; i < ins.srcCount; i++) { @@ -2410,7 +2150,6 @@ namespace dxvk { pointer.type.ctype); } - // Define memory scope and semantics based on the operands uint32_t scope = 0; uint32_t semantics = 0; @@ -2430,12 +2169,10 @@ namespace dxvk { const uint32_t scopeId = m_module.constu32(scope); const uint32_t semanticsId = m_module.constu32(semantics); - // Perform the atomic operation on the given pointer DxbcRegisterValue value; value.type = pointer.type; value.id = 0; - // The result type, which is a scalar integer const uint32_t typeId = getVectorTypeId(value.type); switch (ins.op) { @@ -2516,23 +2253,17 @@ namespace dxvk { return; } - // Write back the result to the destination - // register if this is an imm_atomic_* opcode. if (isImm) emitRegisterStore(ins.dst[0], value); } void DxbcCompiler::emitAtomicCounter(const DxbcShaderInstruction& ins) { - // imm_atomic_alloc and imm_atomic_consume have the following operands: - // (dst0) The register that will hold the old counter value - // (dst1) The UAV whose counter is going to be modified const uint32_t registerId = ins.dst[1].idx[0].offset; if (m_uavs.at(registerId).ctrId == 0) m_uavs.at(registerId).ctrId = emitDclUavCounter(registerId); - // Get a pointer to the atomic counter in question DxbcRegisterInfo ptrType; ptrType.type.ctype = DxbcScalarType::Uint32; ptrType.type.ccount = 1; @@ -2545,7 +2276,6 @@ namespace dxvk { m_uavs.at(registerId).ctrId, 1, &zeroId); - // Define memory scope and semantics based on the operands uint32_t scope = spv::ScopeQueueFamily; uint32_t semantics = spv::MemorySemanticsUniformMemoryMask | spv::MemorySemanticsAcquireReleaseMask; @@ -2553,7 +2283,6 @@ namespace dxvk { uint32_t scopeId = m_module.constu32(scope); uint32_t semanticsId = m_module.constu32(semantics); - // Compute the result value DxbcRegisterValue value; value.type.ctype = DxbcScalarType::Uint32; value.type.ccount = 1; @@ -2580,14 +2309,11 @@ namespace dxvk { return; } - // Store the result emitRegisterStore(ins.dst[0], value); } void DxbcCompiler::emitBarrier(const DxbcShaderInstruction& ins) { - // sync takes no operands. Instead, the synchronization - // scope is defined by the operand control bits. const DxbcSyncFlags flags = ins.controls.syncFlags(); uint32_t executionScope = spv::ScopeInvocation; @@ -2643,11 +2369,6 @@ namespace dxvk { void DxbcCompiler::emitBitExtract(const DxbcShaderInstruction& ins) { - // ibfe and ubfe take the following arguments: - // (dst0) The destination register - // (src0) Number of bits to extact - // (src1) Offset of the bits to extract - // (src2) Register to extract bits from const bool isSigned = ins.op == DxbcOpcode::IBfe; DxbcRegisterValue bitCnt = emitRegisterLoad(ins.src[0], ins.dst[0].mask); @@ -2688,12 +2409,6 @@ namespace dxvk { void DxbcCompiler::emitBitInsert(const DxbcShaderInstruction& ins) { - // ibfe and ubfe take the following arguments: - // (dst0) The destination register - // (src0) Number of bits to extact - // (src1) Offset of the bits to extract - // (src2) Register to take bits from - // (src3) Register to replace bits in DxbcRegisterValue bitCnt = emitRegisterLoad(ins.src[0], ins.dst[0].mask); DxbcRegisterValue bitOfs = emitRegisterLoad(ins.src[1], ins.dst[0].mask); @@ -2733,16 +2448,12 @@ namespace dxvk { void DxbcCompiler::emitBitScan(const DxbcShaderInstruction& ins) { - // firstbit(lo|hi|shi) have two operands: - // (dst0) The destination operant - // (src0) Source operand to scan DxbcRegisterValue src = emitRegisterLoad(ins.src[0], ins.dst[0].mask); DxbcRegisterValue dst; dst.type.ctype = ins.dst[0].dataType; dst.type.ccount = ins.dst[0].mask.popCount(); - // Result type, should be an unsigned integer const uint32_t typeId = getVectorTypeId(dst.type); switch (ins.op) { @@ -2752,8 +2463,6 @@ namespace dxvk { default: Logger::warn(str::format("DxbcCompiler: Unhandled instruction: ", ins.op)); return; } - // The 'Hi' variants are counted from the MSB in DXBC - // rather than the LSB, so we have to invert the number if (ins.op == DxbcOpcode::FirstBitHi || ins.op == DxbcOpcode::FirstBitShi) { uint32_t boolTypeId = m_module.defBoolType(); @@ -2769,28 +2478,20 @@ namespace dxvk { constff.id); } - // No modifiers are supported emitRegisterStore(ins.dst[0], dst); } void DxbcCompiler::emitBufferQuery(const DxbcShaderInstruction& ins) { - // bufinfo takes two arguments - // (dst0) The destination register - // (src0) The buffer register to query const DxbcBufferInfo bufferInfo = getBufferInfo(ins.src[0]); bool isSsbo = bufferInfo.isSsbo; - // We'll store this as a scalar unsigned integer DxbcRegisterValue result = isSsbo ? emitQueryBufferSize(ins.src[0]) : emitQueryTexelBufferSize(ins.src[0]); uint32_t typeId = getVectorTypeId(result.type); - // Adjust returned size if this is a raw or structured - // buffer, as emitQueryTexelBufferSize only returns the - // number of typed elements in the buffer. if (bufferInfo.type == DxbcResourceType::Raw) { result.id = m_module.opIMul(typeId, result.id, m_module.constu32(4)); @@ -2799,56 +2500,34 @@ namespace dxvk { m_module.constu32(bufferInfo.stride / 4)); } - // Store the result. The scalar will be extended to a - // vector if the write mask consists of more than one - // component, which is the desired behaviour. emitRegisterStore(ins.dst[0], result); } void DxbcCompiler::emitBufferLoad(const DxbcShaderInstruction& ins) { - // ld_raw takes three arguments: - // (dst0) Destination register - // (src0) Byte offset - // (src1) Source register - // ld_structured takes four arguments: - // (dst0) Destination register - // (src0) Structure index - // (src1) Byte offset - // (src2) Source register const bool isStructured = ins.op == DxbcOpcode::LdStructured || ins.op == DxbcOpcode::LdStructuredS; - // Source register. The exact way we access - // the data depends on the register type. const DxbcRegister& dstReg = ins.dst[0]; const DxbcRegister& srcReg = isStructured ? ins.src[2] : ins.src[1]; if (dstReg.type == DxbcOperandType::UnorderedAccessView) emitUavBarrier(uint64_t(1u) << srcReg.idx[0].offset, 0u); - // Retrieve common info about the buffer const DxbcBufferInfo bufferInfo = getBufferInfo(srcReg); - // Shared memory is the only type of buffer that - // is not accessed through a texel buffer view bool isTgsm = srcReg.type == DxbcOperandType::ThreadGroupSharedMemory; bool isSsbo = bufferInfo.isSsbo; - // Common types and IDs used while loading the data uint32_t bufferId = isTgsm || isSsbo ? 0 : m_module.opLoad(bufferInfo.typeId, bufferInfo.varId); uint32_t vectorTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 4 }); uint32_t scalarTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 1 }); - // Since all data is represented as a sequence of 32-bit - // integers, we have to load each component individually. std::array ccomps = { 0, 0, 0, 0 }; std::array scomps = { 0, 0, 0, 0 }; uint32_t scount = 0; - // The sparse feedback ID will be non-zero for sparse - // instructions on input. We need to reset it to 0. SpirvMemoryOperands memoryOperands; SpirvImageOperands imageOperands; imageOperands.sparse = ins.dstCount == 2; @@ -2987,8 +2666,6 @@ namespace dxvk { throw DxvkError("DxbcCompiler: Invalid operand type for strucured/raw load"); } - // Only read sparse feedback once. This may be somewhat inaccurate - // for reads that straddle pages, but we can't easily emulate this. if (imageOperands.sparse) { imageOperands.sparse = false; sparseFeedbackId = resultId; @@ -3029,19 +2706,8 @@ namespace dxvk { void DxbcCompiler::emitBufferStore(const DxbcShaderInstruction& ins) { - // store_raw takes three arguments: - // (dst0) Destination register - // (src0) Byte offset - // (src1) Source register - // store_structured takes four arguments: - // (dst0) Destination register - // (src0) Structure index - // (src1) Byte offset - // (src2) Source register const bool isStructured = ins.op == DxbcOpcode::StoreStructured; - // Source register. The exact way we access - // the data depends on the register type. const DxbcRegister& dstReg = ins.dst[0]; const DxbcRegister& srcReg = isStructured ? ins.src[2] : ins.src[1]; @@ -3051,10 +2717,8 @@ namespace dxvk { DxbcRegisterValue value = emitRegisterLoad(srcReg, dstReg.mask); value = emitRegisterBitcast(value, DxbcScalarType::Uint32); - // Retrieve common info about the buffer const DxbcBufferInfo bufferInfo = getBufferInfo(dstReg); - // Thread Group Shared Memory is not accessed through a texel buffer view bool isTgsm = dstReg.type == DxbcOperandType::ThreadGroupSharedMemory; bool isSsbo = bufferInfo.isSsbo; @@ -3063,7 +2727,6 @@ namespace dxvk { uint32_t scalarTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 1 }); uint32_t vectorTypeId = getVectorTypeId({ DxbcScalarType::Uint32, 4 }); - // Set memory operands according to resource properties SpirvMemoryOperands memoryOperands; SpirvImageOperands imageOperands; @@ -3087,7 +2750,6 @@ namespace dxvk { } } - // Compute flat element index as necessary bool useRawAccessChains = isSsbo && m_hasRawAccessChains; DxbcRegisterValue index = emitRegisterLoad(ins.src[0], DxbcRegMask(true, false, false, false)); @@ -3206,42 +2868,31 @@ namespace dxvk { void DxbcCompiler::emitConvertFloat16(const DxbcShaderInstruction& ins) { - // f32tof16 takes two operands: - // (dst0) Destination register as a uint32 vector - // (src0) Source register as a float32 vector - // f16tof32 takes two operands: - // (dst0) Destination register as a float32 vector - // (src0) Source register as a uint32 vector const DxbcRegisterValue src = emitRegisterLoad(ins.src[0], ins.dst[0].mask); - // We handle both packing and unpacking here const bool isPack = ins.op == DxbcOpcode::F32toF16; - // The conversion instructions do not map very well to the - // SPIR-V pack instructions, which operate on 2D vectors. std::array scalarIds = {{ 0, 0, 0, 0 }}; const uint32_t componentCount = src.type.ccount; - // These types are used in both pack and unpack operations const uint32_t t_u32 = getVectorTypeId({ DxbcScalarType::Uint32, 1 }); const uint32_t t_f32 = getVectorTypeId({ DxbcScalarType::Float32, 1 }); const uint32_t t_f32v2 = getVectorTypeId({ DxbcScalarType::Float32, 2 }); - // Constant zero-bit pattern, used for packing const uint32_t zerof32 = isPack ? m_module.constf32(0.0f) : 0; for (uint32_t i = 0; i < componentCount; i++) { const DxbcRegisterValue componentValue = emitRegisterExtract(src, DxbcRegMask::select(i)); - if (isPack) { // f32tof16 + if (isPack) { const std::array packIds = {{ componentValue.id, zerof32 }}; scalarIds[i] = m_module.opPackHalf2x16(t_u32, m_module.opCompositeConstruct(t_f32v2, packIds.size(), packIds.data())); - } else { // f16tof32 + } else { const uint32_t zeroIndex = 0; scalarIds[i] = m_module.opCompositeExtract(t_f32, @@ -3261,10 +2912,6 @@ namespace dxvk { : scalarIds[0]; if (isPack) { - // Some drivers return infinity if the input value is above a certain - // threshold, but D3D wants us to return infinity only if the input is - // actually infinite. Fix this up to return the maximum representable - // 16-bit floating point number instead, but preserve input infinity. uint32_t t_bvec = getVectorTypeId({ DxbcScalarType::Bool, componentCount }); uint32_t f16Infinity = m_module.constuReplicant(0x7C00, componentCount); uint32_t f16Unsigned = m_module.constuReplicant(0x7FFF, componentCount); @@ -3279,22 +2926,17 @@ namespace dxvk { result.id); } - // Store result in the destination register emitRegisterStore(ins.dst[0], result); } void DxbcCompiler::emitConvertFloat64(const DxbcShaderInstruction& ins) { - // ftod and dtof take the following operands: - // (dst0) Destination operand - // (src0) Number to convert uint32_t dstBits = ins.dst[0].mask.popCount(); DxbcRegMask srcMask = isDoubleType(ins.dst[0].dataType) ? DxbcRegMask(dstBits >= 2, dstBits >= 4, false, false) : DxbcRegMask(dstBits >= 1, dstBits >= 1, dstBits >= 2, dstBits >= 2); - // Perform actual conversion, destination modifiers are not applied DxbcRegisterValue val = emitRegisterLoad(ins.src[0], srcMask); DxbcRegisterValue result; @@ -3393,8 +3035,6 @@ namespace dxvk { void DxbcCompiler::emitInterpolate(const DxbcShaderInstruction& ins) { m_module.enableCapability(spv::CapabilityInterpolationFunction); - // The SPIR-V instructions operate on input variable pointers, - // which are all declared as four-component float vectors. uint32_t registerId = ins.src[0].idx[0].offset; DxbcRegisterValue result; @@ -3418,7 +3058,6 @@ namespace dxvk { } break; case DxbcOpcode::EvalSnapped: { - // The offset is encoded as a 4-bit fixed point value DxbcRegisterValue offset = emitRegisterLoad( ins.src[1], DxbcRegMask(true, true, false, false)); offset.id = m_module.opBitFieldSExtract( @@ -3454,9 +3093,6 @@ namespace dxvk { void DxbcCompiler::emitSparseCheckAccess( const DxbcShaderInstruction& ins) { - // check_access_mapped has two operands: - // (dst0) The destination register - // (src0) The residency code m_module.enableCapability(spv::CapabilitySparseResidency); DxbcRegisterValue srcValue = emitRegisterLoad(ins.src[0], ins.dst[0].mask); @@ -3474,34 +3110,22 @@ namespace dxvk { void DxbcCompiler::emitTextureQuery(const DxbcShaderInstruction& ins) { - // resinfo has three operands: - // (dst0) The destination register - // (src0) Resource LOD to query - // (src1) Resource to query const DxbcBufferInfo resourceInfo = getBufferInfo(ins.src[1]); const DxbcResinfoType resinfoType = ins.controls.resinfoType(); - // Read the exact LOD for the image query const DxbcRegisterValue mipLod = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); const DxbcScalarType returnType = resinfoType == DxbcResinfoType::Uint ? DxbcScalarType::Uint32 : DxbcScalarType::Float32; - // Query the size of the selected mip level, as well as the - // total number of mip levels. We will have to combine the - // result into a four-component vector later. DxbcRegisterValue imageSize = emitQueryTextureSize(ins.src[1], mipLod); DxbcRegisterValue imageLevels = emitQueryTextureLods(ins.src[1]); - // If the mip level is out of bounds, D3D requires us to return - // zero before applying modifiers, whereas SPIR-V is undefined, - // so we need to fix it up manually here. imageSize.id = m_module.opSelect(getVectorTypeId(imageSize.type), m_module.opULessThan(m_module.defBoolType(), mipLod.id, imageLevels.id), imageSize.id, emitBuildZeroVector(imageSize.type).id); - // Convert intermediates to the requested type if (returnType == DxbcScalarType::Float32) { imageSize.type.ctype = DxbcScalarType::Float32; imageSize.id = m_module.opConvertUtoF( @@ -3514,9 +3138,6 @@ namespace dxvk { imageLevels.id); } - // If the selected return type is rcpFloat, we need - // to compute the reciprocal of the image dimensions, - // but not the array size, so we need to separate it. const uint32_t imageCoordDim = imageSize.type.ccount; DxbcRegisterValue imageLayers; @@ -3536,8 +3157,6 @@ namespace dxvk { imageSize.id); } - // Concatenate result vectors and scalars to form a - // 4D vector. Unused components will be set to zero. std::array vectorIds = { imageSize.id, 0, 0, 0 }; uint32_t numVectorIds = 1; @@ -3555,7 +3174,6 @@ namespace dxvk { vectorIds[numVectorIds++] = imageLevels.id; - // Create the actual result vector DxbcRegisterValue result; result.type.ctype = returnType; result.type.ccount = 4; @@ -3563,8 +3181,6 @@ namespace dxvk { getVectorTypeId(result.type), numVectorIds, vectorIds.data()); - // Swizzle components using the resource swizzle - // and the destination operand's write mask result = emitRegisterSwizzle(result, ins.src[1].swizzle, ins.dst[0].mask); emitRegisterStore(ins.dst[0], result); @@ -3572,32 +3188,21 @@ namespace dxvk { void DxbcCompiler::emitTextureQueryLod(const DxbcShaderInstruction& ins) { - // All sample instructions have at least these operands: - // (dst0) The destination register - // (src0) Texture coordinates - // (src1) The texture itself - // (src2) The sampler object const DxbcRegister& texCoordReg = ins.src[0]; const DxbcRegister& textureReg = ins.src[1]; const DxbcRegister& samplerReg = ins.src[2]; - // Texture and sampler register IDs const auto& texture = m_textures.at(textureReg.idx[0].offset); const auto& sampler = m_samplers.at(samplerReg.idx[0].offset); - // Load texture coordinates const DxbcRegisterValue coord = emitRegisterLoad(texCoordReg, DxbcRegMask::firstN(getTexLayerDim(texture.imageInfo))); - // Query the LOD. The result is a two-dimensional float32 - // vector containing the mip level and virtual LOD numbers. const uint32_t sampledImageId = emitLoadSampledImage(texture, sampler, false); const uint32_t queriedLodId = m_module.opImageQueryLod( getVectorTypeId({ DxbcScalarType::Float32, 2 }), sampledImageId, coord.id); - // Build the result array vector by filling up - // the remaining two components with zeroes. const uint32_t zero = m_module.constf32(0.0f); const std::array resultIds = {{ queriedLodId, zero, zero }}; @@ -3614,9 +3219,6 @@ namespace dxvk { void DxbcCompiler::emitTextureQueryMs(const DxbcShaderInstruction& ins) { - // sampleinfo has two operands: - // (dst0) The destination register - // (src0) Resource to query DxbcRegisterValue sampleCount = emitQueryTextureSamples(ins.src[0]); if (ins.controls.returnType() != DxbcInstructionReturnType::Uint) { @@ -3631,15 +3233,9 @@ namespace dxvk { void DxbcCompiler::emitTextureQueryMsPos(const DxbcShaderInstruction& ins) { - // samplepos has three operands: - // (dst0) The destination register - // (src0) Resource to query - // (src1) Sample index if (m_samplePositions == 0) m_samplePositions = emitSamplePosArray(); - // The lookup index is qual to the sample count plus the - // sample index, or 0 if the resource cannot be queried. DxbcRegisterValue sampleCount = emitQueryTextureSamples(ins.src[0]); DxbcRegisterValue sampleIndex = emitRegisterLoad( ins.src[1], DxbcRegMask(true, false, false, false)); @@ -3648,7 +3244,6 @@ namespace dxvk { getVectorTypeId(sampleCount.type), sampleCount.id, sampleIndex.id); - // Validate the parameters uint32_t sampleCountValid = m_module.opULessThanEqual( m_module.defBoolType(), sampleCount.id, @@ -3659,8 +3254,6 @@ namespace dxvk { sampleIndex.id, sampleCount.id); - // If the lookup cannot be performed, set the lookup - // index to zero, which will return a zero vector. lookupIndex = m_module.opSelect( getVectorTypeId(sampleCount.type), m_module.opLogicalAnd( @@ -3670,8 +3263,6 @@ namespace dxvk { lookupIndex, m_module.constu32(0)); - // Load sample pos vector and write the masked - // components to the destination register. DxbcRegisterPointer samplePos; samplePos.type.ctype = DxbcScalarType::Float32; samplePos.type.ccount = 2; @@ -3681,7 +3272,6 @@ namespace dxvk { spv::StorageClassPrivate), m_samplePositions, 1, &lookupIndex); - // Expand to vec4 by appending zeroes DxbcRegisterValue result = emitValueLoad(samplePos); DxbcRegisterValue zero; @@ -3699,28 +3289,15 @@ namespace dxvk { void DxbcCompiler::emitTextureFetch(const DxbcShaderInstruction& ins) { - // ld has three operands: - // (dst0) The destination register - // (src0) Source address - // (src1) Source texture - // ld2dms has four operands: - // (dst0) The destination register - // (src0) Source address - // (src1) Source texture - // (src2) Sample number const auto& texture = m_textures.at(ins.src[1].idx[0].offset); const uint32_t imageLayerDim = getTexLayerDim(texture.imageInfo); bool isMultisampled = ins.op == DxbcOpcode::LdMs || ins.op == DxbcOpcode::LdMsS; - // Load the texture coordinates. The last component - // contains the LOD if the resource is an image. const DxbcRegisterValue address = emitRegisterLoad( ins.src[0], DxbcRegMask(true, true, true, true)); - // Additional image operands. This will store - // the LOD and the address offset if present. SpirvImageOperands imageOperands; imageOperands.sparse = ins.dstCount == 2; @@ -3741,8 +3318,6 @@ namespace dxvk { } } - // The LOD is not present when reading from - // a buffer or from a multisample texture. if (texture.imageInfo.dim != spv::DimBuffer && texture.imageInfo.ms == 0) { DxbcRegisterValue imageLod; @@ -3750,7 +3325,6 @@ namespace dxvk { imageLod = emitRegisterExtract( address, DxbcRegMask(false, false, false, true)); } else { - // If we force-disabled MSAA, fetch from LOD 0 imageLod.type = { DxbcScalarType::Uint32, 1 }; imageLod.id = m_module.constu32(0); } @@ -3759,8 +3333,6 @@ namespace dxvk { imageOperands.sLod = imageLod.id; } - // The ld2dms instruction has a sample index, but we - // are only allowed to set it for multisample views if (isMultisampled && texture.imageInfo.ms == 1) { DxbcRegisterValue sampleId = emitRegisterLoad( ins.src[2], DxbcRegMask(true, false, false, false)); @@ -3769,11 +3341,8 @@ namespace dxvk { imageOperands.sSampleId = sampleId.id; } - // Extract coordinates from address const DxbcRegisterValue coord = emitCalcTexCoord(address, texture.imageInfo); - // Reading a typed image or buffer view - // always returns a four-component vector. const uint32_t imageId = m_module.opLoad(texture.imageTypeId, texture.varId); DxbcVectorType texelType; @@ -3796,8 +3365,6 @@ namespace dxvk { ? emitExtractSparseTexel(texelTypeId, resultId) : resultId; - // Swizzle components using the texture swizzle - // and the destination operand's write mask result = emitRegisterSwizzle(result, ins.src[1].swizzle, ins.dst[0].mask); @@ -3809,18 +3376,6 @@ namespace dxvk { void DxbcCompiler::emitTextureGather(const DxbcShaderInstruction& ins) { - // Gather4 takes the following operands: - // (dst0) The destination register - // (dst1) The residency code for sparse ops - // (src0) Texture coordinates - // (src1) The texture itself - // (src2) The sampler, with a component selector - // Gather4C takes the following additional operand: - // (src3) The depth reference value - // The Gather4Po variants take an additional operand - // which defines an extended constant offset. - // TODO reduce code duplication by moving some common code - // in both sample() and gather() into separate methods const bool isExtendedGather = ins.op == DxbcOpcode::Gather4Po || ins.op == DxbcOpcode::Gather4PoC || ins.op == DxbcOpcode::Gather4PoS @@ -3830,18 +3385,13 @@ namespace dxvk { const DxbcRegister& textureReg = ins.src[1 + isExtendedGather]; const DxbcRegister& samplerReg = ins.src[2 + isExtendedGather]; - // Texture and sampler register IDs const auto& texture = m_textures.at(textureReg.idx[0].offset); const auto& sampler = m_samplers.at(samplerReg.idx[0].offset); - // Image type, which stores the image dimensions etc. const uint32_t imageLayerDim = getTexLayerDim(texture.imageInfo); - // Load the texture coordinates. SPIR-V allows these - // to be float4 even if not all components are used. DxbcRegisterValue coord = emitLoadTexCoord(texCoordReg, texture.imageInfo); - // Load reference value for depth-compare operations const bool isDepthCompare = ins.op == DxbcOpcode::Gather4C || ins.op == DxbcOpcode::Gather4PoC || ins.op == DxbcOpcode::Gather4CS @@ -3852,7 +3402,6 @@ namespace dxvk { DxbcRegMask(true, false, false, false)) : DxbcRegisterValue(); - // Accumulate additional image operands. SpirvImageOperands imageOperands; imageOperands.sparse = ins.dstCount == 2; @@ -3881,8 +3430,6 @@ namespace dxvk { } } - // Gathering texels always returns a four-component - // vector, even for the depth-compare variants. uint32_t sampledImageId = emitLoadSampledImage(texture, sampler, isDepthCompare); DxbcVectorType texelType; @@ -3898,7 +3445,6 @@ namespace dxvk { if (sampledImageId) { switch (ins.op) { - // Simple image gather operation case DxbcOpcode::Gather4: case DxbcOpcode::Gather4S: case DxbcOpcode::Gather4Po: @@ -3909,7 +3455,6 @@ namespace dxvk { imageOperands); } break; - // Depth-compare operation case DxbcOpcode::Gather4C: case DxbcOpcode::Gather4CS: case DxbcOpcode::Gather4PoC: @@ -3930,15 +3475,12 @@ namespace dxvk { resultId = m_module.constNull(resultTypeId); } - // If necessary, deal with the sparse result DxbcRegisterValue result; result.type = texelType; result.id = imageOperands.sparse ? emitExtractSparseTexel(texelTypeId, resultId) : resultId; - // Swizzle components using the texture swizzle - // and the destination operand's write mask result = emitRegisterSwizzle(result, textureReg.swizzle, ins.dst[0].mask); @@ -3950,25 +3492,16 @@ namespace dxvk { void DxbcCompiler::emitTextureSample(const DxbcShaderInstruction& ins) { - // All sample instructions have at least these operands: - // (dst0) The destination register - // (src0) Texture coordinates - // (src1) The texture itself - // (src2) The sampler object const DxbcRegister& texCoordReg = ins.src[0]; const DxbcRegister& textureReg = ins.src[1]; const DxbcRegister& samplerReg = ins.src[2]; - // Texture and sampler register IDs const auto& texture = m_textures.at(textureReg.idx[0].offset); const auto& sampler = m_samplers.at(samplerReg.idx[0].offset); const uint32_t imageLayerDim = getTexLayerDim(texture.imageInfo); - // Load the texture coordinates. SPIR-V allows these - // to be float4 even if not all components are used. DxbcRegisterValue coord = emitLoadTexCoord(texCoordReg, texture.imageInfo); - // Load reference value for depth-compare operations const bool isDepthCompare = ins.op == DxbcOpcode::SampleC || ins.op == DxbcOpcode::SampleClz || ins.op == DxbcOpcode::SampleCClampS @@ -3978,7 +3511,6 @@ namespace dxvk { ? emitRegisterLoad(ins.src[3], DxbcRegMask(true, false, false, false)) : DxbcRegisterValue(); - // Load explicit gradients for sample operations that require them const bool hasExplicitGradients = ins.op == DxbcOpcode::SampleD || ins.op == DxbcOpcode::SampleDClampS; @@ -3990,7 +3522,6 @@ namespace dxvk { ? emitRegisterLoad(ins.src[4], DxbcRegMask::firstN(imageLayerDim)) : DxbcRegisterValue(); - // LOD for certain sample operations const bool hasLod = ins.op == DxbcOpcode::SampleL || ins.op == DxbcOpcode::SampleLS || ins.op == DxbcOpcode::SampleB @@ -4000,7 +3531,6 @@ namespace dxvk { ? emitRegisterLoad(ins.src[3], DxbcRegMask(true, false, false, false)) : DxbcRegisterValue(); - // Min LOD for certain sparse operations const bool hasMinLod = ins.op == DxbcOpcode::SampleClampS || ins.op == DxbcOpcode::SampleBClampS || ins.op == DxbcOpcode::SampleDClampS @@ -4010,8 +3540,6 @@ namespace dxvk { ? emitRegisterLoad(ins.src[ins.srcCount - 1], DxbcRegMask(true, false, false, false)) : DxbcRegisterValue(); - // Accumulate additional image operands. These are - // not part of the actual operand token in SPIR-V. SpirvImageOperands imageOperands; imageOperands.sparse = ins.dstCount == 2; @@ -4039,11 +3567,8 @@ namespace dxvk { imageOperands.sMinLod = minLod.id; } - // Combine the texture and the sampler into a sampled image uint32_t sampledImageId = emitLoadSampledImage(texture, sampler, isDepthCompare); - // Sampling an image always returns a four-component - // vector, whereas depth-compare ops return a scalar. DxbcVectorType texelType; texelType.ctype = texture.sampledType; texelType.ccount = isDepthCompare ? 1 : 4; @@ -4057,7 +3582,6 @@ namespace dxvk { if (sampledImageId) { switch (ins.op) { - // Simple image sample operation case DxbcOpcode::Sample: case DxbcOpcode::SampleClampS: { resultId = m_module.opImageSampleImplicitLod( @@ -4065,7 +3589,6 @@ namespace dxvk { imageOperands); } break; - // Depth-compare operation case DxbcOpcode::SampleC: case DxbcOpcode::SampleCClampS: { resultId = m_module.opImageSampleDrefImplicitLod( @@ -4073,7 +3596,6 @@ namespace dxvk { referenceValue.id, imageOperands); } break; - // Depth-compare operation on mip level zero case DxbcOpcode::SampleClz: case DxbcOpcode::SampleClzS: { imageOperands.flags |= spv::ImageOperandsLodMask; @@ -4084,7 +3606,6 @@ namespace dxvk { referenceValue.id, imageOperands); } break; - // Sample operation with explicit gradients case DxbcOpcode::SampleD: case DxbcOpcode::SampleDClampS: { imageOperands.flags |= spv::ImageOperandsGradMask; @@ -4096,7 +3617,6 @@ namespace dxvk { imageOperands); } break; - // Sample operation with explicit LOD case DxbcOpcode::SampleL: case DxbcOpcode::SampleLS: { imageOperands.flags |= spv::ImageOperandsLodMask; @@ -4107,7 +3627,6 @@ namespace dxvk { imageOperands); } break; - // Sample operation with LOD bias case DxbcOpcode::SampleB: case DxbcOpcode::SampleBClampS: { imageOperands.flags |= spv::ImageOperandsBiasMask; @@ -4135,8 +3654,6 @@ namespace dxvk { ? emitExtractSparseTexel(texelTypeId, resultId) : resultId; - // Swizzle components using the texture swizzle - // and the destination operand's write mask if (result.type.ccount != 1) { result = emitRegisterSwizzle(result, textureReg.swizzle, ins.dst[0].mask); @@ -4150,16 +3667,11 @@ namespace dxvk { void DxbcCompiler::emitTypedUavLoad(const DxbcShaderInstruction& ins) { - // load_uav_typed has three operands: - // (dst0) The destination register - // (src0) The texture or buffer coordinates - // (src1) The UAV to load from const uint32_t registerId = ins.src[1].idx[0].offset; const DxbcUav uavInfo = m_uavs.at(registerId); emitUavBarrier(uint64_t(1u) << registerId, 0u); - // Load texture coordinates DxbcRegisterValue texCoord = emitLoadTexCoord( ins.src[0], uavInfo.imageInfo); @@ -4183,12 +3695,10 @@ namespace dxvk { if (imageOperands.sparse) resultTypeId = getSparseResultTypeId(texelTypeId); - // Load source value from the UAV resultId = m_module.opImageRead(resultTypeId, m_module.opLoad(uavInfo.imageTypeId, uavInfo.varId), texCoord.id, imageOperands); - // Apply component swizzle and mask DxbcRegisterValue uavValue; uavValue.type = texelType; uavValue.id = imageOperands.sparse @@ -4206,14 +3716,9 @@ namespace dxvk { void DxbcCompiler::emitTypedUavStore(const DxbcShaderInstruction& ins) { - // store_uav_typed has three operands: - // (dst0) The destination UAV - // (src0) The texture or buffer coordinates - // (src1) The value to store const DxbcBufferInfo uavInfo = getBufferInfo(ins.dst[0]); emitUavBarrier(0u, uint64_t(1u) << ins.dst[0].idx[0].offset); - // Set image operands for coherent access if necessary SpirvImageOperands imageOperands; if (uavInfo.coherence) { @@ -4222,16 +3727,12 @@ namespace dxvk { imageOperands.makeAvailable = m_module.constu32(uavInfo.coherence); } - // Load texture coordinates DxbcRegisterValue texCoord = emitLoadTexCoord(ins.src[0], uavInfo.image); - // Load the value that will be written to the image. We'll - // have to cast it to the component type of the image. const DxbcRegisterValue texValue = emitRegisterBitcast( emitRegisterLoad(ins.src[1], DxbcRegMask(true, true, true, true)), uavInfo.stype); - // Write the given value to the image m_module.opImageWrite( m_module.opLoad(uavInfo.typeId, uavInfo.varId), texCoord.id, texValue.id, imageOperands); @@ -4239,14 +3740,9 @@ namespace dxvk { void DxbcCompiler::emitControlFlowIf(const DxbcShaderInstruction& ins) { - // Load the first component of the condition - // operand and perform a zero test on it. const DxbcRegisterValue condition = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); - // Declare the 'if' block. We do not know if there - // will be an 'else' block or not, so we'll assume - // that there is one and leave it empty otherwise. DxbcCfgBlock block; block.type = DxbcCfgBlockType::If; block.b_if.ztestId = emitRegisterZeroTest(condition, ins.controls.zeroTest()).id; @@ -4256,9 +3752,6 @@ namespace dxvk { block.b_if.headerPtr = m_module.getInsertionPtr(); m_controlFlowBlocks.push_back(block); - // We'll insert the branch instruction when closing - // the block, since we don't know whether or not an - // else block is needed right now. m_module.opLabel(block.b_if.labelIf); } @@ -4269,13 +3762,9 @@ namespace dxvk { || m_controlFlowBlocks.back().b_if.labelElse != 0) throw DxvkError("DxbcCompiler: 'Else' without 'If' found"); - // Set the 'Else' flag so that we do - // not insert a dummy block on 'EndIf' DxbcCfgBlock& block = m_controlFlowBlocks.back(); block.b_if.labelElse = m_module.allocateId(); - // Close the 'If' block by branching to - // the merge block we declared earlier m_module.opBranch(block.b_if.labelEnd); m_module.opLabel (block.b_if.labelElse); } @@ -4286,11 +3775,9 @@ namespace dxvk { || m_controlFlowBlocks.back().type != DxbcCfgBlockType::If) throw DxvkError("DxbcCompiler: 'EndIf' without 'If' found"); - // Remove the block from the stack, it's closed DxbcCfgBlock block = m_controlFlowBlocks.back(); m_controlFlowBlocks.pop_back(); - // Write out the 'if' header m_module.beginInsertion(block.b_if.headerPtr); m_module.opSelectionMerge( @@ -4306,20 +3793,15 @@ namespace dxvk { m_module.endInsertion(); - // End the active 'if' or 'else' block m_module.opBranch(block.b_if.labelEnd); m_module.opLabel (block.b_if.labelEnd); } void DxbcCompiler::emitControlFlowSwitch(const DxbcShaderInstruction& ins) { - // Load the selector as a scalar unsigned integer const DxbcRegisterValue selector = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); - // Declare switch block. We cannot insert the switch - // instruction itself yet because the number of case - // statements and blocks is unknown at this point. DxbcCfgBlock block; block.type = DxbcCfgBlockType::Switch; block.b_switch.insertPtr = m_module.getInsertionPtr(); @@ -4330,7 +3812,6 @@ namespace dxvk { block.b_switch.labelCases = nullptr; m_controlFlowBlocks.push_back(block); - // Define the first 'case' label m_module.opLabel(block.b_switch.labelCase); } @@ -4340,11 +3821,9 @@ namespace dxvk { || m_controlFlowBlocks.back().type != DxbcCfgBlockType::Switch) throw DxvkError("DxbcCompiler: 'Case' without 'Switch' found"); - // The source operand must be a 32-bit immediate. if (ins.src[0].type != DxbcOperandType::Imm32) throw DxvkError("DxbcCompiler: Invalid operand type for 'Case'"); - // Use the last label allocated for 'case'. DxbcCfgBlockSwitch* block = &m_controlFlowBlocks.back().b_switch; if (caseBlockIsFallthrough()) { @@ -4376,7 +3855,6 @@ namespace dxvk { m_module.opLabel (block->labelCase); } - // Set the last label allocated for 'case' as the default label. block->labelDefault = block->labelCase; } @@ -4386,7 +3864,6 @@ namespace dxvk { || m_controlFlowBlocks.back().type != DxbcCfgBlockType::Switch) throw DxvkError("DxbcCompiler: 'EndSwitch' without 'Switch' found"); - // Remove the block from the stack, it's closed DxbcCfgBlock block = m_controlFlowBlocks.back(); m_controlFlowBlocks.pop_back(); @@ -4396,17 +3873,13 @@ namespace dxvk { : block.b_switch.labelCase; } - // Close the current 'case' block m_module.opBranch(block.b_switch.labelBreak); - // Insert the 'switch' statement. For that, we need to - // gather all the literal-label pairs for the construct. m_module.beginInsertion(block.b_switch.insertPtr); m_module.opSelectionMerge( block.b_switch.labelBreak, spv::SelectionControlMaskNone); - // We'll restore the original order of the case labels here std::vector jumpTargets; for (auto i = block.b_switch.labelCases; i != nullptr; i = i->next) jumpTargets.insert(jumpTargets.begin(), i->desc); @@ -4418,20 +3891,16 @@ namespace dxvk { jumpTargets.data()); m_module.endInsertion(); - // Destroy the list of case labels - // FIXME we're leaking memory if compilation fails. DxbcSwitchLabel* caseLabel = block.b_switch.labelCases; while (caseLabel != nullptr) delete std::exchange(caseLabel, caseLabel->next); - // Begin new block after switch blocks m_module.opLabel(block.b_switch.labelBreak); } void DxbcCompiler::emitControlFlowLoop(const DxbcShaderInstruction& ins) { - // Declare the 'loop' block DxbcCfgBlock block; block.type = DxbcCfgBlockType::Loop; block.b_loop.labelHeader = m_module.allocateId(); @@ -4458,15 +3927,12 @@ namespace dxvk { || m_controlFlowBlocks.back().type != DxbcCfgBlockType::Loop) throw DxvkError("DxbcCompiler: 'EndLoop' without 'Loop' found"); - // Remove the block from the stack, it's closed const DxbcCfgBlock block = m_controlFlowBlocks.back(); m_controlFlowBlocks.pop_back(); - // Declare the continue block m_module.opBranch(block.b_loop.labelContinue); m_module.opLabel (block.b_loop.labelContinue); - // Declare the merge block m_module.opBranch(block.b_loop.labelHeader); m_module.opLabel (block.b_loop.labelBreak); } @@ -4486,17 +3952,13 @@ namespace dxvk { m_module.opBranch(isBreak ? cfgBlock->b_loop.labelBreak : cfgBlock->b_loop.labelContinue); - } else /* if (cfgBlock->type == DxbcCfgBlockType::Switch) */ { + } else { m_module.opBranch(cfgBlock->b_switch.labelBreak); } - // Subsequent instructions assume that there is an open block const uint32_t labelId = m_module.allocateId(); m_module.opLabel(labelId); - // If this is on the same level as a switch-case construct, - // rather than being nested inside an 'if' statement, close - // the current 'case' block. if (m_controlFlowBlocks.back().type == DxbcCfgBlockType::Switch) cfgBlock->b_switch.labelCase = labelId; } @@ -4512,14 +3974,12 @@ namespace dxvk { if (cfgBlock == nullptr) throw DxvkError("DxbcCompiler: 'Breakc' or 'Continuec' outside 'Loop' or 'Switch' found"); - // Perform zero test on the first component of the condition const DxbcRegisterValue condition = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); const DxbcRegisterValue zeroTest = emitRegisterZeroTest( condition, ins.controls.zeroTest()); - // We basically have to wrap this into an 'if' block const uint32_t breakBlock = m_module.allocateId(); const uint32_t mergeBlock = m_module.allocateId(); @@ -4535,7 +3995,7 @@ namespace dxvk { m_module.opBranch(isBreak ? cfgBlock->b_loop.labelBreak : cfgBlock->b_loop.labelContinue); - } else /* if (cfgBlock->type == DxbcCfgBlockType::Switch) */ { + } else { m_module.opBranch(cfgBlock->b_switch.labelBreak); } @@ -4550,27 +4010,23 @@ namespace dxvk { m_module.opReturn(); m_module.opLabel(labelId); - // return can be used in place of break to terminate a case block if (m_controlFlowBlocks.back().type == DxbcCfgBlockType::Switch) m_controlFlowBlocks.back().b_switch.labelCase = labelId; m_topLevelIsUniform = false; } else { - // Last instruction in the current function this->emitFunctionEnd(); } } void DxbcCompiler::emitControlFlowRetc(const DxbcShaderInstruction& ins) { - // Perform zero test on the first component of the condition const DxbcRegisterValue condition = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); const DxbcRegisterValue zeroTest = emitRegisterZeroTest( condition, ins.controls.zeroTest()); - // We basically have to wrap this into an 'if' block const uint32_t returnLabel = m_module.allocateId(); const uint32_t continueLabel = m_module.allocateId(); @@ -4585,14 +4041,11 @@ namespace dxvk { m_module.opLabel(continueLabel); - // The return condition may be non-uniform m_topLevelIsUniform = false; } void DxbcCompiler::emitControlFlowDiscard(const DxbcShaderInstruction& ins) { - // Discard actually has an operand that determines - // whether or not the fragment should be discarded const DxbcRegisterValue condition = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); @@ -4614,7 +4067,6 @@ namespace dxvk { m_module.enableCapability(spv::CapabilityDemoteToHelperInvocation); - // Discard is just retc in a trenchcoat m_topLevelIsUniform = false; } @@ -4634,8 +4086,6 @@ namespace dxvk { m_insideFunction = true; - // We have to assume that this function gets - // called from non-uniform control flow m_topLevelIsUniform = false; } @@ -4654,14 +4104,12 @@ namespace dxvk { uint32_t functionNr = ins.src[1].idx[0].offset; uint32_t functionId = getFunctionId(functionNr); - // Perform zero test on the first component of the condition const DxbcRegisterValue condition = emitRegisterLoad( ins.src[0], DxbcRegMask(true, false, false, false)); const DxbcRegisterValue zeroTest = emitRegisterZeroTest( condition, ins.controls.zeroTest()); - // We basically have to wrap this into an 'if' block const uint32_t callLabel = m_module.allocateId(); const uint32_t skipLabel = m_module.allocateId(); @@ -4778,7 +4226,6 @@ namespace dxvk { float z, float w, const DxbcRegMask& writeMask) { - // TODO refactor these functions into one single template std::array ids = { 0, 0, 0, 0 }; uint32_t componentIndex = 0; @@ -4947,8 +4394,6 @@ namespace dxvk { indices[dstIndex++] = swizzle[i]; } - // If the swizzle combined with the mask can be reduced - // to a no-op, we don't need to insert any instructions. bool isIdentitySwizzle = dstIndex == value.type.ccount; for (uint32_t i = 0; i < dstIndex && isIdentitySwizzle; i++) @@ -4957,8 +4402,6 @@ namespace dxvk { if (isIdentitySwizzle) return value; - // Use OpCompositeExtract if the resulting vector contains - // only one component, and OpVectorShuffle if it is a vector. DxbcRegisterValue result; result.type.ctype = value.type.ctype; result.type.ccount = dstIndex; @@ -4996,24 +4439,15 @@ namespace dxvk { const uint32_t typeId = getVectorTypeId(result.type); if (srcMask.popCount() == 0) { - // Nothing to do if the insertion mask is empty result.id = dstValue.id; } else if (dstValue.type.ccount == 1) { - // Both values are scalar, so the first component - // of the write mask decides which one to take. result.id = srcMask[0] ? srcValue.id : dstValue.id; } else if (srcValue.type.ccount == 1) { - // The source value is scalar. Since OpVectorShuffle - // requires both arguments to be vectors, we have to - // use OpCompositeInsert to modify the vector instead. const uint32_t componentId = srcMask.firstSet(); result.id = m_module.opCompositeInsert(typeId, srcValue.id, dstValue.id, 1, &componentId); } else { - // Both arguments are vectors. We can determine which - // components to take from which vector and use the - // OpVectorShuffle instruction. std::array components; uint32_t srcComponentId = dstValue.type.ccount; @@ -5232,8 +4666,6 @@ namespace dxvk { DxbcRegisterPointer DxbcCompiler::emitGetTempPtr( const DxbcRegister& operand) { - // r# regs are indexed as follows: - // (0) register index (immediate) uint32_t regIdx = operand.idx[0].offset; if (regIdx >= m_rRegs.size()) @@ -5269,14 +4701,6 @@ namespace dxvk { DxbcRegisterPointer DxbcCompiler::emitGetInputPtr( const DxbcRegister& operand) { - // In the vertex and pixel stages, - // v# regs are indexed as follows: - // (0) register index (relative) - // - // In the tessellation and geometry - // stages, the index has two dimensions: - // (0) vertex index (relative) - // (1) register index (relative) DxbcRegisterPointer result; result.type.ctype = DxbcScalarType::Float32; result.type.ccount = 4; @@ -5286,8 +4710,6 @@ namespace dxvk { for (uint32_t i = 0; i < operand.idxDim; i++) indices.at(i) = emitIndexLoad(operand.idx[i]).id; - // Pick the input array depending on - // the program type and operand type struct InputArray { uint32_t id; spv::StorageClass sclass; @@ -5327,9 +4749,6 @@ namespace dxvk { DxbcRegisterPointer DxbcCompiler::emitGetOutputPtr( const DxbcRegister& operand) { if (m_programInfo.type() == DxbcProgramType::HullShader) { - // Hull shaders are special in that they have two sets of - // output registers, one for per-patch values and one for - // per-vertex values. DxbcRegisterPointer result; result.type.ctype = DxbcScalarType::Float32; result.type.ccount = 4; @@ -5361,8 +4780,6 @@ namespace dxvk { return result; } else { - // Regular shaders have their output - // registers set up at declaration time return m_oRegs.at(operand.idx[0].offset); } } @@ -5373,8 +4790,6 @@ namespace dxvk { DxbcRegisterValue constId = emitIndexLoad(operand.idx[0]); if (m_icbArray) { - // We pad the icb array with an extra zero vector, so we can - // clamp the index and get correct robustness behaviour. constId.id = m_module.opUMin(getVectorTypeId(constId.type), constId.id, m_module.constu32(m_icbSize)); @@ -5542,16 +4957,12 @@ namespace dxvk { DxbcRegisterPointer DxbcCompiler::emitGetAtomicPointer( const DxbcRegister& operand, const DxbcRegister& address) { - // Query information about the resource itself const uint32_t registerId = operand.idx[0].offset; const DxbcBufferInfo resourceInfo = getBufferInfo(operand); - // For UAVs and shared memory, different methods - // of obtaining the final pointer are used. bool isTgsm = operand.type == DxbcOperandType::ThreadGroupSharedMemory; bool isSsbo = resourceInfo.isSsbo; - // Compute the actual address into the resource const DxbcRegisterValue addressValue = [&] { switch (resourceInfo.type) { case DxbcResourceType::Raw: @@ -5581,7 +4992,6 @@ namespace dxvk { } }(); - // Compute the actual pointer DxbcRegisterPointer result; result.type.ctype = resourceInfo.stype; result.type.ccount = 1; @@ -5620,14 +5030,11 @@ namespace dxvk { DxbcRegisterValue DxbcCompiler::emitQueryTexelBufferSize( const DxbcRegister& resource) { - // Load the texel buffer object. This cannot be used with - // constant buffers or any other type of resource. const DxbcBufferInfo bufferInfo = getBufferInfo(resource); const uint32_t bufferId = m_module.opLoad( bufferInfo.typeId, bufferInfo.varId); - // We'll store this as a scalar unsigned integer DxbcRegisterValue result; result.type.ctype = DxbcScalarType::Uint32; result.type.ccount = 1; @@ -5651,7 +5058,6 @@ namespace dxvk { getVectorTypeId(result.type), m_module.opLoad(info.typeId, info.varId)); } else { - // Report one LOD in case of UAVs or multisampled images result.id = m_module.constu32(1); } @@ -5662,8 +5068,6 @@ namespace dxvk { DxbcRegisterValue DxbcCompiler::emitQueryTextureSamples( const DxbcRegister& resource) { if (resource.type == DxbcOperandType::Rasterizer) { - // SPIR-V has no gl_NumSamples equivalent, so we - // have to work around it using a push constant if (!m_ps.pushConstantId) m_ps.pushConstantId = emitPushConstants(); @@ -5689,7 +5093,6 @@ namespace dxvk { getVectorTypeId(result.type), m_module.opLoad(info.typeId, info.varId)); } else { - // OpImageQuerySamples requires MSAA images result.id = m_module.constu32(1); } @@ -5812,22 +5215,15 @@ namespace dxvk { DxbcRegisterPointer ptr, DxbcRegisterValue value, DxbcRegMask writeMask) { - // If the component types are not compatible, - // we need to bit-cast the source variable. if (value.type.ctype != ptr.type.ctype) value = emitRegisterBitcast(value, ptr.type.ctype); - // If the source value consists of only one component, - // it is stored in all components of the destination. if (value.type.ccount == 1) value = emitRegisterExtend(value, writeMask.popCount()); if (ptr.type.ccount == writeMask.popCount()) { - // Simple case: We write to the entire register m_module.opStore(ptr.id, value.id); } else { - // We only write to part of the destination - // register, so we need to load and modify it DxbcRegisterValue tmp = emitValueLoad(ptr); tmp = emitRegisterInsert(tmp, value, writeMask); @@ -5838,7 +5234,6 @@ namespace dxvk { DxbcRegisterValue DxbcCompiler::emitRegisterLoadRaw( const DxbcRegister& reg) { - // Try to find index range for the given register const DxbcIndexRange* indexRange = nullptr; if (reg.idxDim && reg.idx[reg.idxDim - 1u].relReg) { @@ -5870,8 +5265,6 @@ namespace dxvk { m_module.constu32(indexRange->length)); } - // Kind of ugly to have an empty else block here but there's no - // way for us to know the current block ID for the phi below DxbcConditional cond; cond.labelIf = m_module.allocateId(); cond.labelElse = m_module.allocateId(); @@ -5906,7 +5299,6 @@ namespace dxvk { DxbcRegisterValue value = emitValueLoad(emitGetOperandPtr(reg)); - // Pad icb values to a vec4 since the app may access components that are always 0 if (reg.type == DxbcOperandType::ImmediateConstantBuffer && value.type.ccount < 4u) { DxbcVectorType zeroType; zeroType.ctype = value.type.ctype; @@ -5928,9 +5320,6 @@ namespace dxvk { DxbcRegisterValue DxbcCompiler::emitConstantBufferLoad( const DxbcRegister& reg, DxbcRegMask writeMask) { - // Constant buffers take a two-dimensional index: - // (0) register index (immediate) - // (1) constant offset (relative) DxbcRegisterInfo info; info.type.ctype = DxbcScalarType::Float32; info.type.ccount = 4; @@ -5952,7 +5341,6 @@ namespace dxvk { m_constantBuffers.at(regId).varId, indices.size(), indices.data()); - // Load individual components from buffer std::array ccomps = { 0, 0, 0, 0 }; std::array scomps = { 0, 0, 0, 0 }; uint32_t scount = 0; @@ -5993,7 +5381,6 @@ namespace dxvk { scount, scomps.data()); } - // Apply any post-processing that might be necessary result = emitRegisterBitcast(result, reg.dataType); result = emitSrcOperandModifiers(result, reg.modifiers); return result; @@ -6008,14 +5395,12 @@ namespace dxvk { DxbcRegisterValue result; if (reg.componentCount == DxbcComponentCount::Component1) { - // Create one single u32 constant result.type.ctype = DxbcScalarType::Uint32; result.type.ccount = 1; result.id = m_module.constu32(reg.imm.u32_1); result = emitRegisterExtend(result, writeMask.popCount()); } else if (reg.componentCount == DxbcComponentCount::Component4) { - // Create a u32 vector with as many components as needed std::array indices = { }; uint32_t indexId = 0; @@ -6037,26 +5422,19 @@ namespace dxvk { } } else { - // Something went horribly wrong in the decoder or the shader is broken throw DxvkError("DxbcCompiler: Invalid component count for immediate operand"); } - // Cast constants to the requested type return emitRegisterBitcast(result, reg.dataType); } else if (reg.type == DxbcOperandType::ConstantBuffer) { return emitConstantBufferLoad(reg, writeMask); } else { - // Load operand from the operand pointer DxbcRegisterValue result = emitRegisterLoadRaw(reg); - // Apply operand swizzle to the operand value result = emitRegisterSwizzle(result, reg.swizzle, writeMask); - // Cast it to the requested type. We need to do - // this after the swizzling for 64-bit types. result = emitRegisterBitcast(result, reg.dataType); - // Apply operand modifiers result = emitSrcOperandModifiers(result, reg.modifiers); return result; } @@ -6100,7 +5478,6 @@ namespace dxvk { void DxbcCompiler::emitInputSetup() { m_module.setLateConst(m_vArrayLengthId, &m_vArrayLength); - // Copy all defined v# registers into the input array const uint32_t vecTypeId = m_module.defVectorType(m_module.defFloatType(32), 4); const uint32_t ptrTypeId = m_module.defPointerType(vecTypeId, spv::StorageClassPrivate); @@ -6121,8 +5498,6 @@ namespace dxvk { } } - // Copy all system value registers into the array, - // preserving any previously written contents. for (const DxbcSvMapping& map : m_vMappings) { const uint32_t registerId = m_module.consti32(map.regId); @@ -6147,8 +5522,6 @@ namespace dxvk { void DxbcCompiler::emitInputSetup(uint32_t vertexCount) { m_module.setLateConst(m_vArrayLengthId, &m_vArrayLength); - // Copy all defined v# registers into the input array. Note - // that the outer index of the array is the vertex index. const uint32_t vecTypeId = m_module.defVectorType(m_module.defFloatType(32), 4); const uint32_t dstPtrTypeId = m_module.defPointerType(vecTypeId, spv::StorageClassPrivate); @@ -6179,8 +5552,6 @@ namespace dxvk { } } - // Copy all system value registers into the array, - // preserving any previously written contents. for (const DxbcSvMapping& map : m_vMappings) { const uint32_t registerId = m_module.consti32(map.regId); @@ -6240,9 +5611,6 @@ namespace dxvk { void DxbcCompiler::emitOutputDepthClamp() { - // HACK: Some drivers do not clamp FragDepth to [minDepth..maxDepth] - // before writing to the depth attachment, but we do not have acccess - // to those. Clamp to [0..1] instead. if (m_ps.builtinDepth) { DxbcRegisterPointer ptr; ptr.type = { DxbcScalarType::Float32, 1 }; @@ -6466,9 +5834,6 @@ namespace dxvk { ptrIn.type = { DxbcScalarType::Float32, 4 }; ptrIn.id = m_ps.builtinFragCoord; - // The X, Y and Z components of the SV_POSITION semantic - // are identical to Vulkan's FragCoord builtin, but we - // need to compute the reciprocal of the W component. DxbcRegisterValue fragCoord = emitValueLoad(ptrIn); uint32_t componentIndex = 3; @@ -6675,18 +6040,18 @@ namespace dxvk { }; static const std::array s_tessFactors = {{ - { m_hs.builtinTessLevelOuter, 0 }, // FinalQuadUeq0EdgeTessFactor - { m_hs.builtinTessLevelOuter, 1 }, // FinalQuadVeq0EdgeTessFactor - { m_hs.builtinTessLevelOuter, 2 }, // FinalQuadUeq1EdgeTessFactor - { m_hs.builtinTessLevelOuter, 3 }, // FinalQuadVeq1EdgeTessFactor - { m_hs.builtinTessLevelInner, 0 }, // FinalQuadUInsideTessFactor - { m_hs.builtinTessLevelInner, 1 }, // FinalQuadVInsideTessFactor - { m_hs.builtinTessLevelOuter, 0 }, // FinalTriUeq0EdgeTessFactor - { m_hs.builtinTessLevelOuter, 1 }, // FinalTriVeq0EdgeTessFactor - { m_hs.builtinTessLevelOuter, 2 }, // FinalTriWeq0EdgeTessFactor - { m_hs.builtinTessLevelInner, 0 }, // FinalTriInsideTessFactor - { m_hs.builtinTessLevelOuter, 0 }, // FinalLineDensityTessFactor - { m_hs.builtinTessLevelOuter, 1 }, // FinalLineDetailTessFactor + { m_hs.builtinTessLevelOuter, 0 }, + { m_hs.builtinTessLevelOuter, 1 }, + { m_hs.builtinTessLevelOuter, 2 }, + { m_hs.builtinTessLevelOuter, 3 }, + { m_hs.builtinTessLevelInner, 0 }, + { m_hs.builtinTessLevelInner, 1 }, + { m_hs.builtinTessLevelOuter, 0 }, + { m_hs.builtinTessLevelOuter, 1 }, + { m_hs.builtinTessLevelOuter, 2 }, + { m_hs.builtinTessLevelInner, 0 }, + { m_hs.builtinTessLevelOuter, 0 }, + { m_hs.builtinTessLevelOuter, 1 }, }}; const TessFactor tessFactor = s_tessFactors.at(uint32_t(sv) @@ -6695,7 +6060,6 @@ namespace dxvk { const uint32_t tessFactorArrayIndex = m_module.constu32(tessFactor.index); - // Apply global tess factor limit float maxTessFactor = m_hs.maxTessFactor; if (m_moduleInfo.tess != nullptr) { @@ -6841,7 +6205,6 @@ namespace dxvk { for (auto e = m_isgn->begin(); e != m_isgn->end(); e++) { if (e->systemValue == sv) { - // Load individual components from the source array uint32_t componentIndex = 0; std::array componentIds = {{ 0, 0, 0, 0 }}; @@ -6862,7 +6225,6 @@ namespace dxvk { } } - // Put everything into one vector DxbcRegisterValue dstValue; dstValue.type = { DxbcScalarType::Float32, componentIndex }; dstValue.id = componentIds[0]; @@ -6873,7 +6235,6 @@ namespace dxvk { componentIndex, componentIds.data()); } - // Store vector to the input array uint32_t registerId = m_module.consti32(e->registerId); DxbcRegisterPointer dstInput; @@ -6904,12 +6265,9 @@ namespace dxvk { void DxbcCompiler::emitInit() { - // Set up common capabilities for all shaders m_module.enableCapability(spv::CapabilityShader); m_module.enableCapability(spv::CapabilityImageQuery); - // Initialize the shader module with capabilities - // etc. Each shader type has its own peculiarities. switch (m_programInfo.type()) { case DxbcProgramType::VertexShader: emitVsInit(); break; case DxbcProgramType::HullShader: emitHsInit(); break; @@ -6966,10 +6324,8 @@ namespace dxvk { m_module.enableCapability(spv::CapabilityCullDistance); m_module.enableCapability(spv::CapabilityDrawParameters); - // Standard input array emitDclInputArray(0); - // Cull/clip distances as outputs m_clipDistances = emitDclClipCullDistanceArray( m_analysis->clipCullOut.numClipPlanes, spv::BuiltInClipDistance, @@ -6980,7 +6336,6 @@ namespace dxvk { spv::BuiltInCullDistance, spv::StorageClassOutput); - // Main function of the vertex shader m_vs.functionId = m_module.allocateId(); m_module.setDebugName(m_vs.functionId, "vs_main"); @@ -7018,7 +6373,6 @@ namespace dxvk { m_ds.builtinTessLevelOuter = emitBuiltinTessLevelOuter(spv::StorageClassInput); m_ds.builtinTessLevelInner = emitBuiltinTessLevelInner(spv::StorageClassInput); - // Cull/clip distances as outputs m_clipDistances = emitDclClipCullDistanceArray( m_analysis->clipCullOut.numClipPlanes, spv::BuiltInClipDistance, @@ -7029,7 +6383,6 @@ namespace dxvk { spv::BuiltInCullDistance, spv::StorageClassOutput); - // Main function of the domain shader m_ds.functionId = m_module.allocateId(); m_module.setDebugName(m_ds.functionId, "ds_main"); @@ -7047,7 +6400,6 @@ namespace dxvk { m_module.enableCapability(spv::CapabilityClipDistance); m_module.enableCapability(spv::CapabilityCullDistance); - // Enable capabilities for xfb mode if necessary if (m_moduleInfo.xfb) { m_module.enableCapability(spv::CapabilityGeometryStreams); m_module.enableCapability(spv::CapabilityTransformFeedback); @@ -7055,11 +6407,9 @@ namespace dxvk { m_module.setExecutionMode(m_entryPointId, spv::ExecutionModeXfb); } - // We only need outputs if rasterization is enabled m_gs.needsOutputSetup = !m_moduleInfo.xfb || m_moduleInfo.xfb->rasterizedStream >= 0; - // Cull/clip distances as outputs m_clipDistances = emitDclClipCullDistanceArray( m_analysis->clipCullOut.numClipPlanes, spv::BuiltInClipDistance, @@ -7070,11 +6420,9 @@ namespace dxvk { spv::BuiltInCullDistance, spv::StorageClassOutput); - // Emit Xfb variables if necessary if (m_moduleInfo.xfb) emitXfbOutputDeclarations(); - // Main function of the vertex shader m_gs.functionId = m_module.allocateId(); m_module.setDebugName(m_gs.functionId, "gs_main"); @@ -7093,10 +6441,8 @@ namespace dxvk { m_module.setExecutionMode(m_entryPointId, spv::ExecutionModeOriginUpperLeft); - // Standard input array emitDclInputArray(0); - // Cull/clip distances as inputs m_clipDistances = emitDclClipCullDistanceArray( m_analysis->clipCullIn.numClipPlanes, spv::BuiltInClipDistance, @@ -7107,7 +6453,6 @@ namespace dxvk { spv::BuiltInCullDistance, spv::StorageClassInput); - // Main function of the pixel shader m_ps.functionId = m_module.allocateId(); m_module.setDebugName(m_ps.functionId, "ps_main"); @@ -7121,7 +6466,6 @@ namespace dxvk { void DxbcCompiler::emitCsInit() { - // Main function of the compute shader m_cs.functionId = m_module.allocateId(); m_module.setDebugName(m_cs.functionId, "cs_main"); @@ -7152,13 +6496,11 @@ namespace dxvk { if (m_hs.cpPhase.functionId == 0) m_hs.cpPhase = this->emitNewHullShaderPassthroughPhase(); - // Control point phase this->emitMainFunctionBegin(); this->emitInputSetup(m_hs.vertexCountIn); this->emitHsControlPointPhase(m_hs.cpPhase); this->emitHsPhaseBarrier(); - // Fork-join phases and output setup this->emitHsInvocationBlockBegin(1); for (const auto& phase : m_hs.forkPhases) @@ -7196,8 +6538,6 @@ namespace dxvk { m_module.opFunctionCall( m_module.defVoidType(), m_gs.functionId, 0, nullptr); - // No output setup at this point as that was - // already done during the EmitVertex step this->emitFunctionEnd(); } @@ -7209,8 +6549,6 @@ namespace dxvk { this->emitClipCullLoad(DxbcSystemValue::CullDistance, m_cullDistances); if (m_hasRasterizerOrderedUav) { - // For simplicity, just lock the entire fragment shader - // if there are any rasterizer ordered views. m_module.enableExtension("SPV_EXT_fragment_shader_interlock"); if (m_module.hasCapability(spv::CapabilitySampleRateShading) @@ -7293,7 +6631,6 @@ namespace dxvk { m_moduleInfo.xfb->strides[xfbEntry->bufferId]); } - // TODO Compact location/component assignment for (uint32_t i = 0; i < m_xfbVars.size(); i++) { m_xfbVars[i].location = i; m_xfbVars[i].component = 0; @@ -7362,9 +6699,6 @@ namespace dxvk { info.ctype = DxbcScalarType::Float32; info.ccount = 4; - // Define the array type. This will be two-dimensional - // in some shaders, with the outer index representing - // the vertex ID within an invocation. m_vArrayLength = m_isgn != nullptr ? std::max(1u, m_isgn->maxRegisterCount()) : 1; m_vArrayLengthId = m_module.lateConst32(getScalarTypeId(DxbcScalarType::Uint32)); @@ -7376,9 +6710,6 @@ namespace dxvk { arrayTypeId, m_module.constu32(vertexCount)); } - // Define the actual variable. Note that this is private - // because we will copy input registers and some system - // variables to the array during the setup phase. const uint32_t ptrTypeId = m_module.defPointerType( arrayTypeId, spv::StorageClassPrivate); @@ -7433,7 +6764,6 @@ namespace dxvk { uint32_t funTypeId = m_module.defFunctionType( m_module.defVoidType(), 0, nullptr); - // Begin passthrough function uint32_t funId = m_module.allocateId(); m_module.setDebugName(funId, "hs_passthrough"); @@ -7442,8 +6772,6 @@ namespace dxvk { funTypeId); this->emitFunctionLabel(); - // We'll basically copy each input variable to the corresponding - // output, using the shader's invocation ID as the array index. const uint32_t invocationId = m_module.opLoad( getScalarTypeId(DxbcScalarType::Uint32), m_hs.builtinInvocationId); @@ -7455,7 +6783,6 @@ namespace dxvk { DxbcSystemValue::None, DxbcInterpolationMode::Undefined); - // Vector type index const std::array dstIndices = {{ invocationId, m_module.constu32(i->registerId) }}; @@ -7477,7 +6804,6 @@ namespace dxvk { emitValueStore(dstPtr, srcValue, DxbcRegMask::firstN(srcValue.type.ccount)); } - // End function this->emitFunctionEnd(); DxbcCompilerHsControlPointPhase result; @@ -7636,19 +6962,14 @@ namespace dxvk { uint32_t DxbcCompiler::emitSamplePosArray() { const std::array samplePosVectors = {{ - // Invalid sample count / unbound resource m_module.constvec2f32( 0.0f, 0.0f), - // VK_SAMPLE_COUNT_1_BIT m_module.constvec2f32( 0.0f, 0.0f), - // VK_SAMPLE_COUNT_2_BIT m_module.constvec2f32( 0.25f, 0.25f), m_module.constvec2f32(-0.25f,-0.25f), - // VK_SAMPLE_COUNT_4_BIT m_module.constvec2f32(-0.125f,-0.375f), m_module.constvec2f32( 0.375f,-0.125f), m_module.constvec2f32(-0.375f, 0.125f), m_module.constvec2f32( 0.125f, 0.375f), - // VK_SAMPLE_COUNT_8_BIT m_module.constvec2f32( 0.0625f,-0.1875f), m_module.constvec2f32(-0.0625f, 0.1875f), m_module.constvec2f32( 0.3125f, 0.0625f), @@ -7657,7 +6978,6 @@ namespace dxvk { m_module.constvec2f32(-0.4375f,-0.0625f), m_module.constvec2f32( 0.1875f, 0.4375f), m_module.constvec2f32( 0.4375f,-0.4375f), - // VK_SAMPLE_COUNT_16_BIT m_module.constvec2f32( 0.0625f, 0.0625f), m_module.constvec2f32(-0.0625f,-0.1875f), m_module.constvec2f32(-0.1875f, 0.1250f), @@ -7920,10 +7240,6 @@ namespace dxvk { || m_programInfo.type() != DxbcProgramType::ComputeShader) return; - // If both masks are 0, emit a barrier in case at least one read-write UAV - // has a pending unsynchronized access. Only consider read-after-write and - // write-after-read hazards, assume that back-to-back stores are safe and - // do not overlap in memory. Atomics are also completely ignored here. uint64_t rdMask = m_uavRdMask; uint64_t wrMask = m_uavWrMask; @@ -7939,8 +7255,6 @@ namespace dxvk { insertBarrier |= (m_analysis->uavInfos[uav].accessFlags & rwAccess) == rwAccess; } - // Need to be in uniform top-level control flow, or otherwise - // it is not safe to insert control barriers. if (insertBarrier && m_controlFlowBlocks.empty() && m_topLevelIsUniform) { m_module.opControlBarrier( m_module.constu32(spv::ScopeWorkgroup), @@ -7956,7 +7270,6 @@ namespace dxvk { m_uavRdMask = 0u; } - // Mark pending accesses m_uavWrMask |= writeMask; m_uavRdMask |= readMask; } @@ -8093,9 +7406,6 @@ namespace dxvk { DxbcRegisterPointer DxbcCompiler::getIndexableTempPtr( const DxbcRegister& operand, DxbcRegisterValue vectorId) { - // x# regs are indexed as follows: - // (0) register index (immediate) - // (1) element index (relative) const uint32_t regId = operand.idx[0].offset; DxbcRegisterInfo info; @@ -8124,8 +7434,6 @@ namespace dxvk { uint32_t DxbcCompiler::getUavCoherence(uint32_t registerId, DxbcUavFlags flags) { - // For any ROV with write access, we must ensure that - // availability operations happen within the locked scope. if (flags.test(DxbcUavFlag::RasterizerOrdered) && (m_analysis->uavInfos[registerId].accessFlags & VK_ACCESS_SHADER_WRITE_BIT)) { m_hasGloballyCoherentUav = true; @@ -8133,22 +7441,14 @@ namespace dxvk { return spv::ScopeQueueFamily; } - // Ignore any resources that can't both be read and written in - // the current shader, explicit availability/visibility operands - // are not useful in that case. if (m_analysis->uavInfos[registerId].accessFlags != (VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT)) return 0; - // If the globally coherent flag is set, the resource must be - // coherent across multiple workgroups of the same dispatch if (flags.test(DxbcUavFlag::GloballyCoherent)) { m_hasGloballyCoherentUav = true; return spv::ScopeQueueFamily; } - // In compute shaders, UAVs are implicitly workgroup coherent, - // but we can rely on memory barrier instructions to make any - // access available and visible to the entire workgroup. if (m_programInfo.type() == DxbcProgramType::ComputeShader) return spv::ScopeInvocation; diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_decoder.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_decoder.cpp index 0de7ddad1..d1f0632c9 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_decoder.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_decoder.cpp @@ -41,8 +41,6 @@ namespace dxvk { void DxbcDecodeContext::decodeInstruction(DxbcCodeSlice& code) { const uint32_t token0 = code.at(0); - // Initialize the instruction structure. Some of these values - // may not get written otherwise while decoding the instruction. m_instruction.op = static_cast(bit::extract(token0, 0, 10)); m_instruction.opClass = DxbcInstClass::Undefined; m_instruction.sampleControls = { 0, 0, 0 }; @@ -56,12 +54,8 @@ namespace dxvk { m_instruction.customDataSize = 0; m_instruction.customData = nullptr; - // Reset the index pointer, which may still contain - // a non-zero value from the previous iteration m_indexId = 0; - // Instruction length, in DWORDs. This includes the token - // itself and any other prefix that an instruction may have. uint32_t length = 0; if (m_instruction.op == DxbcOpcode::CustomData) { @@ -72,8 +66,6 @@ namespace dxvk { this->decodeOperation(code.take(length)); } - // Advance the caller's slice to the next token so that - // they can make consecutive calls to decodeInstruction() code = code.skip(length); } @@ -86,11 +78,9 @@ namespace dxvk { return; } - // Custom data blocks have their own instruction class m_instruction.op = DxbcOpcode::CustomData; m_instruction.opClass = DxbcInstClass::CustomData; - // We'll point into the code buffer rather than making a copy m_instruction.customDataType = static_cast( bit::extract(code.at(0), 11, 31)); m_instruction.customDataSize = blockLength - 2; @@ -101,15 +91,11 @@ namespace dxvk { void DxbcDecodeContext::decodeOperation(DxbcCodeSlice code) { uint32_t token = code.read(); - // Result modifiers, which are applied to common ALU ops m_instruction.modifiers.saturate = !!bit::extract(token, 13, 13); m_instruction.modifiers.precise = !!bit::extract(token, 19, 22); - // Opcode controls. It will depend on the - // opcode itself which ones are valid. m_instruction.controls = DxbcShaderOpcodeControls(token); - // Process extended opcode tokens while (bit::extract(token, 31, 31)) { token = code.read(); @@ -128,7 +114,6 @@ namespace dxvk { aoffimmi.v = bit::extract(token, 13, 16); aoffimmi.w = bit::extract(token, 17, 20); - // Four-bit signed numbers, sign-extend them m_instruction.sampleControls.u = aoffimmi.u; m_instruction.sampleControls.v = aoffimmi.v; m_instruction.sampleControls.w = aoffimmi.w; @@ -136,7 +121,7 @@ namespace dxvk { case DxbcExtOpcode::ResourceDim: case DxbcExtOpcode::ResourceReturnType: - break; // part of resource description + break; default: Logger::warn(str::format( @@ -145,9 +130,6 @@ namespace dxvk { } } - // Retrieve the instruction format in order to parse the - // operands. Doing this mostly automatically means that - // the compiler can rely on the operands being valid. const DxbcInstFormat format = dxbcInstructionFormat(m_instruction.op); m_instruction.opClass = format.instructionClass; @@ -157,39 +139,29 @@ namespace dxvk { void DxbcDecodeContext::decodeComponentSelection(DxbcRegister& reg, uint32_t token) { - // Pick the correct component selection mode based on the - // component count. We'll simplify this here so that the - // compiler can assume that everything is a 4D vector. reg.componentCount = static_cast(bit::extract(token, 0, 1)); switch (reg.componentCount) { - // No components - used for samplers etc. case DxbcComponentCount::Component0: reg.mask = DxbcRegMask(false, false, false, false); reg.swizzle = DxbcRegSwizzle(0, 0, 0, 0); break; - // One component - used for immediates - // and a few built-in registers. case DxbcComponentCount::Component1: reg.mask = DxbcRegMask(true, false, false, false); reg.swizzle = DxbcRegSwizzle(0, 0, 0, 0); break; - // Four components - everything else. This requires us - // to actually parse the component selection mode. case DxbcComponentCount::Component4: { const DxbcRegMode componentMode = static_cast(bit::extract(token, 2, 3)); switch (componentMode) { - // Write mask for destination operands case DxbcRegMode::Mask: reg.mask = bit::extract(token, 4, 7); reg.swizzle = DxbcRegSwizzle(0, 1, 2, 3); break; - // Swizzle for source operands (including resources) case DxbcRegMode::Swizzle: reg.mask = DxbcRegMask(true, true, true, true); reg.swizzle = DxbcRegSwizzle( @@ -199,9 +171,6 @@ namespace dxvk { bit::extract(token, 10, 11)); break; - // Selection of one component. We can generate both a - // mask and a swizzle for this so that the compiler - // won't have to deal with this case specifically. case DxbcRegMode::Select1: { const uint32_t n = bit::extract(token, 4, 5); reg.mask = DxbcRegMask(n == 0, n == 1, n == 2, n == 3); @@ -223,13 +192,10 @@ namespace dxvk { while (bit::extract(token, 31, 31)) { token = code.read(); - // Type of the extended operand token const DxbcOperandExt extTokenType = static_cast(bit::extract(token, 0, 5)); switch (extTokenType) { - // Operand modifiers, which are used to manipulate the - // value of a source operand during the load operation case DxbcOperandExt::OperandModifier: reg.modifiers = bit::extract(token, 6, 13); break; @@ -247,13 +213,10 @@ namespace dxvk { if (reg.type == DxbcOperandType::Imm32 || reg.type == DxbcOperandType::Imm64) { switch (reg.componentCount) { - // This is commonly used if only one vector - // component is involved in an operation case DxbcComponentCount::Component1: { reg.imm.u32_1 = code.read(); } break; - // Typical four-component vector case DxbcComponentCount::Component4: { reg.imm.u32_4[0] = code.read(); reg.imm.u32_4[1] = code.read(); @@ -272,7 +235,6 @@ namespace dxvk { reg.idxDim = bit::extract(token, 20, 21); for (uint32_t i = 0; i < reg.idxDim; i++) { - // An index can be encoded in various different ways const DxbcOperandIndexRepresentation repr = static_cast( bit::extract(token, 22 + 3 * i, 24 + 3 * i)); diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_defs.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_defs.cpp index 0d193ada9..34effc2d3 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_defs.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_defs.cpp @@ -3,344 +3,273 @@ namespace dxvk { const std::array g_instructionFormats = {{ - /* Add */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* And */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Break */ { 0, DxbcInstClass::ControlFlow }, - /* Breakc */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Call */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Callc */ { 2, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Case */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Continue */ { 0, DxbcInstClass::ControlFlow }, - /* Continuec */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Cut */ { 0, DxbcInstClass::GeometryEmit }, - /* Default */ { 0, DxbcInstClass::ControlFlow }, - /* DerivRtx */ { 2, DxbcInstClass::VectorDeriv, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* DerivRty */ { 2, DxbcInstClass::VectorDeriv, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Discard */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Div */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Dp2 */ { 3, DxbcInstClass::VectorDot, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Dp3 */ { 3, DxbcInstClass::VectorDot, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Dp4 */ { 3, DxbcInstClass::VectorDot, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Else */ { 0, DxbcInstClass::ControlFlow }, - /* Emit */ { 0, DxbcInstClass::GeometryEmit }, - /* EmitThenCut */ { 0, DxbcInstClass::GeometryEmit }, - /* EndIf */ { 0, DxbcInstClass::ControlFlow }, - /* EndLoop */ { 0, DxbcInstClass::ControlFlow }, - /* EndSwitch */ { 0, DxbcInstClass::ControlFlow }, - /* Eq */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Exp */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Frc */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* FtoI */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* FtoU */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Ge */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* IAdd */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* If */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* IEq */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* IGe */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* ILt */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* IMad */ { 4, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* IMax */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* IMin */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* IMul */ { 4, DxbcInstClass::VectorImul, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* INe */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* INeg */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* IShl */ { 3, DxbcInstClass::VectorShift, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* IShr */ { 3, DxbcInstClass::VectorShift, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ItoF */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* Label */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* Ld */ { 3, DxbcInstClass::TextureFetch, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* LdMs */ { 4, DxbcInstClass::TextureFetch, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* Log */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Loop */ { 0, DxbcInstClass::ControlFlow }, - /* Lt */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Mad */ { 4, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Min */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Max */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* CustomData */ { 0, DxbcInstClass::CustomData }, - /* Mov */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Movc */ { 4, DxbcInstClass::VectorCmov, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Mul */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Ne */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Nop */ { 0, DxbcInstClass::NoOperation }, - /* Not */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Or */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ResInfo */ { 3, DxbcInstClass::TextureQuery, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Ret */ { 0, DxbcInstClass::ControlFlow }, - /* Retc */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* RoundNe */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* RoundNi */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* RoundPi */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* RoundZ */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Rsq */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Sample */ { 4, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleC */ { 5, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -348,7 +277,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleClz */ { 5, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -356,7 +284,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleL */ { 5, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -364,7 +291,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleD */ { 6, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -373,7 +299,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleB */ { 5, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -381,240 +306,188 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Sqrt */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Switch */ { 1, DxbcInstClass::ControlFlow, { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* SinCos */ { 3, DxbcInstClass::VectorSinCos, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* UDiv */ { 4, DxbcInstClass::VectorIdiv, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ULt */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UGe */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UMul */ { 4, DxbcInstClass::VectorImul, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UMad */ { 4, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UMax */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UMin */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UShr */ { 3, DxbcInstClass::VectorShift, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UtoF */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Xor */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* DclResource */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclConstantBuffer */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclSampler */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclIndexRange */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclGsOutputPrimitiveTopology */ { 0, DxbcInstClass::Declaration }, - /* DclGsInputPrimitive */ { 0, DxbcInstClass::Declaration }, - /* DclMaxOutputVertexCount */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclInput */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclInputSgv */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclInputSiv */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclInputPs */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclInputPsSgv */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclInputPsSiv */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclOutput */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclOutputSgv */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclOutputSiv */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclTemps */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclIndexableTemp */ { 3, DxbcInstClass::Declaration, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclGlobalFlags */ { 0, DxbcInstClass::Declaration }, - /* Reserved0 */ { 0, DxbcInstClass::Undefined }, - /* Lod */ { 4, DxbcInstClass::TextureQueryLod, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4 */ { 4, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SamplePos */ { 3, DxbcInstClass::TextureQueryMsPos, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* SampleInfo */ { 2, DxbcInstClass::TextureQueryMs, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Reserved1 */ { }, - /* HsDecls */ { 0, DxbcInstClass::HullShaderPhase }, - /* HsControlPointPhase */ { 0, DxbcInstClass::HullShaderPhase }, - /* HsForkPhase */ { 0, DxbcInstClass::HullShaderPhase }, - /* HsJoinPhase */ { 0, DxbcInstClass::HullShaderPhase }, - /* EmitStream */ { 1, DxbcInstClass::GeometryEmit, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* CutStream */ { 1, DxbcInstClass::GeometryEmit, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* EmitThenCutStream */ { 1, DxbcInstClass::GeometryEmit, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* InterfaceCall */ { }, - /* BufInfo */ { 2, DxbcInstClass::BufferQuery, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* DerivRtxCoarse */ { 2, DxbcInstClass::VectorDeriv, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* DerivRtxFine */ { 2, DxbcInstClass::VectorDeriv, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* DerivRtyCoarse */ { 2, DxbcInstClass::VectorDeriv, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* DerivRtyFine */ { 2, DxbcInstClass::VectorDeriv, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4C */ { 5, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -622,7 +495,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4Po */ { 5, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -630,7 +502,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4PoC */ { 6, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, @@ -639,60 +510,48 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Rcp */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* F32toF16 */ { 2, DxbcInstClass::ConvertFloat16, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* F16toF32 */ { 2, DxbcInstClass::ConvertFloat16, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UAddc */ { }, - /* USubb */ { }, - /* CountBits */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* FirstBitHi */ { 2, DxbcInstClass::BitScan, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* FirstBitLo */ { 2, DxbcInstClass::BitScan, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* FirstBitShi */ { 2, DxbcInstClass::BitScan, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* UBfe */ { 4, DxbcInstClass::BitExtract, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* IBfe */ { 4, DxbcInstClass::BitExtract, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* Bfi */ { 5, DxbcInstClass::BitInsert, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, @@ -700,12 +559,10 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* BfRev */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Swapc */ { 5, DxbcInstClass::VectorCmov, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, @@ -713,217 +570,174 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* DclStream */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* DclFunctionBody */ { }, - /* DclFunctionTable */ { }, - /* DclInterface */ { }, - /* DclInputControlPointCount */ { 0, DxbcInstClass::Declaration }, - /* DclOutputControlPointCount */ { 0, DxbcInstClass::Declaration }, - /* DclTessDomain */ { 0, DxbcInstClass::Declaration }, - /* DclTessPartitioning */ { 0, DxbcInstClass::Declaration }, - /* DclTessOutputPrimitive */ { 0, DxbcInstClass::Declaration }, - /* DclHsMaxTessFactor */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::Imm32, DxbcScalarType::Float32 }, } }, - /* DclHsForkPhaseInstanceCount */ { 1, DxbcInstClass::HullShaderInstCnt, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclHsJoinPhaseInstanceCount */ { 1, DxbcInstClass::HullShaderInstCnt, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclThreadGroup */ { 3, DxbcInstClass::Declaration, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclUavTyped */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclUavRaw */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclUavStructured */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclThreadGroupSharedMemoryRaw */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclThreadGroupSharedMemoryStructured */ { 3, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* DclResourceRaw */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, } }, - /* DclResourceStructured */ { 2, DxbcInstClass::Declaration, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* LdUavTyped */ { 3, DxbcInstClass::TypedUavLoad, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* StoreUavTyped */ { 3, DxbcInstClass::TypedUavStore, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* LdRaw */ { 3, DxbcInstClass::BufferLoad, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* StoreRaw */ { 3, DxbcInstClass::BufferStore, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* LdStructured */ { 4, DxbcInstClass::BufferLoad, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* StoreStructured */ { 4, DxbcInstClass::BufferStore, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicAnd */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicOr */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicXor */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicCmpStore */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicIAdd */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicIMax */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* AtomicIMin */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* AtomicUMax */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* AtomicUMin */ { 3, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicAlloc */ { 2, DxbcInstClass::AtomicCounter, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicConsume */ { 2, DxbcInstClass::AtomicCounter, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicIAdd */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicAnd */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicOr */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicXor */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicExch */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicCmpExch */ { 5, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -931,181 +745,147 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicIMax */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* ImmAtomicIMin */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* ImmAtomicUMax */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ImmAtomicUMin */ { 4, DxbcInstClass::Atomic, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* Sync */ { 0, DxbcInstClass::Barrier }, - /* DAdd */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DMax */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DMin */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DMul */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DEq */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DGe */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DLt */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DNe */ { 3, DxbcInstClass::VectorCmp, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DMov */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DMovc */ { 4, DxbcInstClass::VectorCmov, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DtoF */ { 2, DxbcInstClass::ConvertFloat64, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* FtoD */ { 2, DxbcInstClass::ConvertFloat64, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* EvalSnapped */ { 3, DxbcInstClass::Interpolate, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* EvalSampleIndex */ { 3, DxbcInstClass::Interpolate, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* EvalCentroid */ { 2, DxbcInstClass::Interpolate, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* DclGsInstanceCount */ { 1, DxbcInstClass::Declaration, { { DxbcOperandKind::Imm32, DxbcScalarType::Uint32 }, } }, - /* Abort */ { }, - /* DebugBreak */ { }, - /* ReservedBegin11_1 */ { }, - /* DDiv */ { 3, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DFma */ { 4, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DRcp */ { 2, DxbcInstClass::VectorAlu, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* Msad */ { 4, DxbcInstClass::VectorMsad, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* DtoI */ { 2, DxbcInstClass::ConvertFloat64, { { DxbcOperandKind::DstReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* DtoU */ { 2, DxbcInstClass::ConvertFloat64, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float64 }, } }, - /* ItoD */ { 2, DxbcInstClass::ConvertFloat64, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* UtoD */ { 2, DxbcInstClass::ConvertFloat64, { { DxbcOperandKind::DstReg, DxbcScalarType::Float64 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* ReservedBegin11_2 */ { }, - /* Gather4S */ { 5, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1113,7 +893,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4CS */ { 6, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1122,7 +901,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4PoS */ { 6, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1131,7 +909,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* Gather4PoCS */ { 7, DxbcInstClass::TextureGather, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1141,14 +918,12 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* LdS */ { 4, DxbcInstClass::TextureFetch, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* LdMsS */ { 5, DxbcInstClass::TextureFetch, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1156,21 +931,18 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, } }, - /* LdUavTypedS */ { 4, DxbcInstClass::TypedUavLoad, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* LdRawS */ { 4, DxbcInstClass::BufferLoad, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* LdStructuredS */ { 5, DxbcInstClass::BufferLoad, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1178,7 +950,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Sint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, } }, - /* SampleLS */ { 6, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1187,7 +958,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleClzS */ { 6, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1196,7 +966,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleClampS */ { 6, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1205,7 +974,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleBClampS */ { 7, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1215,7 +983,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleDClampS */ { 8, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1226,7 +993,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* SampleCClampS */ { 7, DxbcInstClass::TextureSample, { { DxbcOperandKind::DstReg, DxbcScalarType::Float32 }, { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, @@ -1236,7 +1002,6 @@ namespace dxvk { { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Float32 }, } }, - /* CheckAccessFullyMapped */ { 2, DxbcInstClass::SparseCheckAccess, { { DxbcOperandKind::DstReg, DxbcScalarType::Uint32 }, { DxbcOperandKind::SrcReg, DxbcScalarType::Uint32 }, diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_header.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_header.cpp index 9b5f69895..c73eef378 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_header.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_header.cpp @@ -3,21 +3,18 @@ namespace dxvk { DxbcHeader::DxbcHeader(DxbcReader& reader) { - // FourCC at the start of the file, must be 'DXBC' DxbcTag fourcc = reader.readTag(); if (fourcc != "DXBC") throw DxvkError("DxbcHeader::DxbcHeader: Invalid fourcc, expected 'DXBC'"); - // Stuff we don't actually need to store - reader.skip(4 * sizeof(uint32_t)); // Check sum - reader.skip(1 * sizeof(uint32_t)); // Constant 1 - reader.skip(1 * sizeof(uint32_t)); // Bytecode length + + reader.skip(4 * sizeof(uint32_t)); + reader.skip(1 * sizeof(uint32_t)); + reader.skip(1 * sizeof(uint32_t)); - // Number of chunks in the file uint32_t chunkCount = reader.readu32(); - // Chunk offsets are stored immediately after for (uint32_t i = 0; i < chunkCount; i++) m_chunkOffsets.push_back(reader.readu32()); } diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_module.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_module.cpp index 573d503b0..c2ff65673 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_module.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_module.cpp @@ -8,13 +8,9 @@ namespace dxvk { : m_header(reader) { for (uint32_t i = 0; i < m_header.numChunks(); i++) { - // The chunk tag is stored at the beginning of each chunk auto chunkReader = reader.clone(m_header.chunkOffset(i)); auto tag = chunkReader.readTag(); - // The chunk size follows right after the four-character - // code. This does not include the eight bytes that are - // consumed by the FourCC and chunk length entry. auto chunkLength = chunkReader.readu32(); chunkReader = chunkReader.clone(8); diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_names.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_names.cpp index c6a00eccc..ce9dfbd63 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_names.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_names.cpp @@ -442,4 +442,4 @@ namespace dxvk { } -} //namespace dxvk +} diff --git a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_util.cpp b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_util.cpp index 3d885fc9c..e1c1c4418 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_util.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/dxbc/dxbc_util.cpp @@ -4,14 +4,14 @@ namespace dxvk { uint32_t primitiveVertexCount(DxbcPrimitive primitive) { static const std::array s_vertexCounts = { - 0, // Undefined - 1, // Point - 2, // Line - 3, // Triangle - 0, // Undefined - 0, // Undefined - 4, // Line with adjacency - 6, // Triangle with adjacency + 0, + 1, + 2, + 3, + 0, + 0, + 4, + 6, }; if (primitive >= DxbcPrimitive::Patch1) { diff --git a/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_code_buffer.cpp b/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_code_buffer.cpp index f8166df51..456c46d9e 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_code_buffer.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_code_buffer.cpp @@ -130,7 +130,6 @@ namespace dxvk { } } - // Commit current word this->putWord(word); } @@ -138,9 +137,9 @@ namespace dxvk { void SpirvCodeBuffer::putHeader(uint32_t version, uint32_t boundIds) { this->putWord(spv::MagicNumber); this->putWord(version); - this->putWord(0); // Generator + this->putWord(0); this->putWord(boundIds); - this->putWord(0); // Schema + this->putWord(0); } @@ -152,7 +151,6 @@ namespace dxvk { uint32_t SpirvCodeBuffer::strLen(const char* str) { - // Null-termination plus padding return (std::strlen(str) + 4) / 4; } diff --git a/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_module.cpp b/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_module.cpp index aad1b39ec..f37d93b24 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_module.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/spirv/spirv_module.cpp @@ -29,9 +29,6 @@ namespace dxvk { result.append(m_typeConstDefs); result.append(m_variables); - // Perform some crude dead code elimination. In some cases, our compilers - // may emit invalid code, such as an unreachable block branching to a loop's - // continue block, but those cases cannot be reasonably detected up-front. std::unordered_set reachableBlocks; std::unordered_set mergeBlocks; @@ -78,8 +75,6 @@ namespace dxvk { void SpirvModule::enableCapability( spv::Capability capability) { - // Scan the generated instructions to check - // whether we already enabled the capability. if (!hasCapability(capability)) { m_capabilities.putIns (spv::OpCapability, 2); m_capabilities.putWord(capability); @@ -416,7 +411,6 @@ namespace dxvk { std::array args = { value, value, value, value }; - // Can't make a scalar composite. if (count == 1) return args[0]; @@ -434,7 +428,6 @@ namespace dxvk { std::array args = { value, value, value, value }; - // Can't make a scalar composite. if (count == 1) return args[0]; @@ -452,7 +445,6 @@ namespace dxvk { std::array args = { value, value, value, value }; - // Can't make a scalar composite. if (count == 1) return args[0]; @@ -470,7 +462,6 @@ namespace dxvk { std::array args = { value, value, value, value }; - // Can't make a scalar composite. if (count == 1) return args[0]; @@ -3765,7 +3756,6 @@ namespace dxvk { uint32_t SpirvModule::opSinCos( uint32_t x, bool useBuiltIn) { - // We only operate on 32-bit floats here uint32_t floatType = defFloatType(32); uint32_t resultType = defVectorType(floatType, 2u); @@ -3777,7 +3767,6 @@ namespace dxvk { uint32_t sintType = defIntType(32, true); uint32_t boolType = defBoolType(); - // Normalize input to multiple of pi/4 uint32_t xNorm = opFMul(floatType, opFAbs(floatType, x), constf32(4.0 / pi)); uint32_t xTrunc = opTrunc(floatType, xNorm); @@ -3785,11 +3774,9 @@ namespace dxvk { uint32_t xInt = opConvertFtoU(uintType, xTrunc); - // Mirror input along x axis as necessary uint32_t mirror = opINotEqual(boolType, opBitwiseAnd(uintType, xInt, constu32(1u)), constu32(0u)); xFract = opSelect(floatType, mirror, opFSub(floatType, constf32(1.0f), xFract), xFract); - // Compute taylor series for fractional part uint32_t xFract_2 = opFMul(floatType, xFract, xFract); uint32_t xFract_4 = opFMul(floatType, xFract_2, xFract_2); uint32_t xFract_6 = opFMul(floatType, xFract_4, xFract_2); @@ -3809,23 +3796,18 @@ namespace dxvk { taylor = opFMul(floatType, taylor, xFract); decorate(taylor, spv::DecorationNoContraction); - // Compute co-function based on sin^2 + cos^2 = 1 uint32_t coFunc = opSqrt(floatType, opFSub(floatType, constf32(1.0f), opFMul(floatType, taylor, taylor))); - // Determine whether the taylor series was used for sine or cosine and assign the correct result uint32_t funcIsSin = opIEqual(boolType, opBitwiseAnd(uintType, opIAdd(uintType, xInt, constu32(1u)), constu32(2u)), constu32(0u)); uint32_t sin = opSelect(floatType, funcIsSin, taylor, coFunc); uint32_t cos = opSelect(floatType, funcIsSin, coFunc, taylor); - // Determine whether sine is negative. Interpret the input as a - // signed integer in order to propagate signed zeroes properly. uint32_t inputNeg = opSLessThan(boolType, opBitcast(sintType, x), consti32(0)); uint32_t sinNeg = opINotEqual(boolType, opBitwiseAnd(uintType, xInt, constu32(4u)), constu32(0u)); sinNeg = opLogicalNotEqual(boolType, sinNeg, inputNeg); - // Determine whether cosine is negative uint32_t cosNeg = opINotEqual(boolType, opBitwiseAnd(uintType, opIAdd(uintType, xInt, constu32(2u)), constu32(4u)), constu32(0u)); sin = opSelect(floatType, sinNeg, opFNegate(floatType, sin), sin); @@ -3841,9 +3823,6 @@ namespace dxvk { spv::Op op, uint32_t argCount, const uint32_t* argIds) { - // Since the type info is stored in the code buffer, - // we can use the code buffer to look up type IDs as - // well. Result IDs are always stored as argument 1. for (auto ins : m_typeConstDefs) { bool match = ins.opCode() == op && ins.length() == 2 + argCount; @@ -3855,7 +3834,6 @@ namespace dxvk { return ins.arg(1); } - // Type not yet declared, create a new one. uint32_t resultId = this->allocateId(); m_typeConstDefs.putIns (op, 2 + argCount); m_typeConstDefs.putWord(resultId); @@ -3871,7 +3849,6 @@ namespace dxvk { uint32_t typeId, uint32_t argCount, const uint32_t* argIds) { - // Avoid declaring constants multiple times for (auto ins : m_typeConstDefs) { bool match = ins.opCode() == op && ins.length() == 3 + argCount @@ -3889,7 +3866,6 @@ namespace dxvk { return id; } - // Constant not yet declared, make a new one uint32_t resultId = this->allocateId(); m_typeConstDefs.putIns (op, 3 + argCount); m_typeConstDefs.putWord(typeId); @@ -3940,7 +3916,6 @@ namespace dxvk { uint32_t SpirvModule::getImageOperandWordCount(const SpirvImageOperands& op) const { - // Each flag may add one or more operands const uint32_t result = ((op.flags & spv::ImageOperandsBiasMask) ? 1 : 0) + ((op.flags & spv::ImageOperandsLodMask) ? 1 : 0) @@ -3953,7 +3928,6 @@ namespace dxvk { + ((op.flags & spv::ImageOperandsMakeTexelAvailableMask) ? 1 : 0) + ((op.flags & spv::ImageOperandsMakeTexelVisibleMask) ? 1 : 0); - // Add a DWORD for the operand mask if it is non-zero return op.flags ? result + 1 : 0; } @@ -4003,7 +3977,6 @@ namespace dxvk { return sclass == spv::StorageClassInput || sclass == spv::StorageClassOutput; } else { - // All global variables need to be declared return sclass != spv::StorageClassFunction; } } @@ -4055,10 +4028,6 @@ namespace dxvk { case spv::OpLoopMerge: { mergeBlocks.insert(ins.arg(1)); - // It is possible for the continue block to be unreachable in - // practice, but we still need to emit it if we are not going - // to eliminate this loop. Since the current block dominates - // the loop, use it to keep the continue block intact. branches.insert({ blockId, ins.arg(2) }); } break; diff --git a/app/src/main/cpp/thirdparty/dxbc/src/util/util_string.cpp b/app/src/main/cpp/thirdparty/dxbc/src/util/util_string.cpp index 9bb4b6114..83403e536 100644 --- a/app/src/main/cpp/thirdparty/dxbc/src/util/util_string.cpp +++ b/app/src/main/cpp/thirdparty/dxbc/src/util/util_string.cpp @@ -9,20 +9,15 @@ namespace dxvk::str { uint32_t first = begin[0]; if (likely(first < 0x80)) { - // Basic ASCII character ch = uint32_t(first); return begin + 1; } else if (unlikely(first < 0xC0)) { - // Character starts with a continuation byte, - // just skip until we find the next valid prefix while ((begin < end) && (((*begin) & 0xC0) == 0x80)) begin += 1; ch = uint32_t('?'); return begin; } else { - // The number of leading 1 bits in the first byte - // determines the length of this character size_t length = bit::lzcnt((~first) << 24); if (unlikely(begin + length > end)) { @@ -43,7 +38,6 @@ namespace dxvk::str { | ((uint32_t(begin[2]) & 0x3F) << 6) | ((uint32_t(begin[3]) & 0x3F)); } else { - // Invalid prefix ch = uint32_t('?'); } @@ -71,7 +65,6 @@ namespace dxvk::str { + ((uint32_t(begin[1]) & 0x3FF)); return begin + 2; } else if (unlikely(first < 0xE000)) { - // Stray low surrogate ch = uint32_t('?'); return begin + 1; } else { @@ -137,7 +130,6 @@ namespace dxvk::str { return 4; } else { - // Invalid code point for UTF-8 return 0; } } @@ -157,8 +149,6 @@ namespace dxvk::str { return 1; } else if (ch < 0xE000) { - // Private use code points, - // we can't encode these return 0; } else if (ch < 0x10000) { if (begin) { @@ -181,7 +171,6 @@ namespace dxvk::str { return 2; } else { - // Invalid code point return 0; } } diff --git a/app/src/main/cpp/waylandcomp/CMakeLists.txt b/app/src/main/cpp/waylandcomp/CMakeLists.txt index 7cd4255dd..bd7c4a6ba 100644 --- a/app/src/main/cpp/waylandcomp/CMakeLists.txt +++ b/app/src/main/cpp/waylandcomp/CMakeLists.txt @@ -1,9 +1,3 @@ -# ── Embedded Wayland compositor (experimental parallel runtime) ────────────── -# Runs a libwayland-server compositor in-process so games launched via Wine's -# winewayland.drv can present into it. Protocol glue is pre-generated in -# generated/ (no host wayland-scanner needed at app build time). Built by the app -# (add_subdirectory from ../CMakeLists.txt) and on its own by standalone/ for -# build-waylandcomp.yml. add_library(droiddeckwayland SHARED src/compositor.c src/vk_present.c @@ -38,25 +32,15 @@ add_library(droiddeckwayland SHARED src/framegen_bridge.c src/framegen_jni.c ) -# Warnings on for the compositor's own C. They do not fail the build; they are there to be read. target_compile_options(droiddeckwayland PRIVATE $<$:-Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers>) target_include_directories(droiddeckwayland PRIVATE generated prebuilt/include src - # The X11 Vulkan renderer's committed SPIR-V headers (*_frag.h / upscale_vert.h, made by - # Bannerlator's gen_shaders.sh): the screen-effect chain (src/effects_chain.c) runs the same bytes. ${CMAKE_CURRENT_SOURCE_DIR}/../shaders ) -# ── Frame generation (feat/wayland-framegen) ───────────────────────────────── -# The two native engines the X11 renderer runs (LSFG Native: framegen/lsfg, Win-FG Native: -# framegen/winfg) are compiled into this library unmodified and driven by -# src/framegen_engine.cpp through the compositor's own Turnip entry points. They need the -# framegen tree and the vendored DXBC translator (thirdparty/dxbc, for the LSFG cache format), -# which only the app build has: the standalone build (standalone/) gets the stub, which reports -# every engine unavailable and generates nothing. set(DROIDDECK_FRAMEGEN_LSFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/lsfg) set(DROIDDECK_FRAMEGEN_WINFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/winfg) if (TARGET dxbc AND EXISTS ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp @@ -80,15 +64,14 @@ if (TARGET dxbc AND EXISTS ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_pacer.cpp ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_engine.cpp ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_governor.cpp - # The Lossless.dll import (validate / extract / cache) for the app's LsfgNative. ${DROIDDECK_FRAMEGEN_LSFG_DIR}/lsfg_jni.cpp ${DROIDDECK_FRAMEGEN_WINFG_DIR}/framegen.cpp ${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_vkd.cpp ${DROIDDECK_FRAMEGEN_WINFG_DIR}/winfg_engine.cpp ) target_include_directories(droiddeckwayland PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/.. # as the frame-generation engines see it - ${CMAKE_CURRENT_SOURCE_DIR}/../framegen # VulkanRendererContext.h (VkTable), lsfg/, winfg/ + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_SOURCE_DIR}/../framegen ) target_compile_options(droiddeckwayland PRIVATE $<$:-std=c++17 -fvisibility=hidden>) @@ -104,8 +87,6 @@ endif() add_library(wayland-server SHARED IMPORTED) set_target_properties(wayland-server PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/prebuilt/lib/libwayland-server.so) -# Vulkan is loaded at runtime via adrenotools (Turnip), not linked directly - so the -# compositor uses Turnip's driver, which supports dmabuf import (drm_format_modifier). target_link_libraries(droiddeckwayland wayland-server adrenotools diff --git a/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-protocol.c index 08e5d76e0..17b0c0dfd 100644 --- a/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright 2026 The412Banner * @@ -29,7 +27,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-server-protocol.h index a06ea0706..788982c10 100644 --- a/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/banner-ahb-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef BANNER_AHB_V1_SERVER_PROTOCOL_H #define BANNER_AHB_V1_SERVER_PROTOCOL_H @@ -14,12 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_banner_ahb_v1 The banner_ahb_v1 protocol - * @section page_ifaces_banner_ahb_v1 Interfaces - * - @subpage page_iface_banner_ahb_v1 - Android gralloc buffers behind dma-buf wl_buffers +/* * @section page_copyright_banner_ahb_v1 Copyright - *
  *
  * Copyright 2026 The412Banner
  *
@@ -41,132 +36,23 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + struct banner_ahb_v1; struct wl_buffer; #ifndef BANNER_AHB_V1_INTERFACE #define BANNER_AHB_V1_INTERFACE -/** - * @page page_iface_banner_ahb_v1 banner_ahb_v1 - * @section page_iface_banner_ahb_v1_desc Description - * - * Zero-copy window layers on Android. A Vulkan swapchain image that the - * client allocated as an Android AHardwareBuffer (gralloc) is still shared - * as a dma-buf through zwp_linux_dmabuf_v1, so the compositor can import - * and draw it the usual way. This interface additionally hands the - * compositor the AHardwareBuffer itself, once per wl_buffer, so the - * compositor can put the buffer straight on an Android SurfaceControl - * layer (SurfaceFlinger / the display hardware composite it) without - * copying it. - * - * The AHardwareBuffer travels over a Unix socket: the client creates a - * socketpair, writes the buffer's handle into one end with - * AHardwareBuffer_sendHandleToUnixSocket, then sends the other end in the - * attach request. The compositor reads it with - * AHardwareBuffer_recvHandleFromUnixSocket and closes the socket. Its - * reference to the AHardwareBuffer lives as long as the wl_buffer (plus - * whatever the display still holds). - * - * The compositor advertises this global whenever it can put buffers on a - * display layer at all; whether it WANTS gralloc buffers right now is the - * mode event (version 2), sent on bind and every time the user switches - * zero-copy on or off during the session. A client decides per swapchain, - * when it creates the swapchain, from the last mode it received, and a - * client whose live swapchain was built for the other mode retires it - * (VK_ERROR_OUT_OF_DATE_KHR on its next acquire or present) so the program - * rebuilds it for the new mode. Buffers of the old swapchain stay fully - * presentable until then: an attached AHardwareBuffer keeps its association - * for the wl_buffer's lifetime whatever the mode is now. A version 1 - * client (no mode event) keeps deciding from its own environment. A - * client that does not see the global (or does not want it) allocates - * swapchain images the standard way; nothing else changes. - * - * Synchronisation stays implicit: the dma-buf carries the client's render - * fence (DMA_BUF_IOCTL_IMPORT_SYNC_FILE by the client's driver), the - * compositor exports it as the layer's acquire fence, and the compositor - * imports the display's release fence into the dma-buf before it sends - * wl_buffer.release, so the client's next acquire waits on it. - * @section page_iface_banner_ahb_v1_api API - * See @ref iface_banner_ahb_v1. - */ -/** - * @defgroup iface_banner_ahb_v1 The banner_ahb_v1 interface - * - * Zero-copy window layers on Android. A Vulkan swapchain image that the - * client allocated as an Android AHardwareBuffer (gralloc) is still shared - * as a dma-buf through zwp_linux_dmabuf_v1, so the compositor can import - * and draw it the usual way. This interface additionally hands the - * compositor the AHardwareBuffer itself, once per wl_buffer, so the - * compositor can put the buffer straight on an Android SurfaceControl - * layer (SurfaceFlinger / the display hardware composite it) without - * copying it. - * - * The AHardwareBuffer travels over a Unix socket: the client creates a - * socketpair, writes the buffer's handle into one end with - * AHardwareBuffer_sendHandleToUnixSocket, then sends the other end in the - * attach request. The compositor reads it with - * AHardwareBuffer_recvHandleFromUnixSocket and closes the socket. Its - * reference to the AHardwareBuffer lives as long as the wl_buffer (plus - * whatever the display still holds). - * - * The compositor advertises this global whenever it can put buffers on a - * display layer at all; whether it WANTS gralloc buffers right now is the - * mode event (version 2), sent on bind and every time the user switches - * zero-copy on or off during the session. A client decides per swapchain, - * when it creates the swapchain, from the last mode it received, and a - * client whose live swapchain was built for the other mode retires it - * (VK_ERROR_OUT_OF_DATE_KHR on its next acquire or present) so the program - * rebuilds it for the new mode. Buffers of the old swapchain stay fully - * presentable until then: an attached AHardwareBuffer keeps its association - * for the wl_buffer's lifetime whatever the mode is now. A version 1 - * client (no mode event) keeps deciding from its own environment. A - * client that does not see the global (or does not want it) allocates - * swapchain images the standard way; nothing else changes. - * - * Synchronisation stays implicit: the dma-buf carries the client's render - * fence (DMA_BUF_IOCTL_IMPORT_SYNC_FILE by the client's driver), the - * compositor exports it as the layer's acquire fence, and the compositor - * imports the display's release fence into the dma-buf before it sends - * wl_buffer.release, so the client's next acquire waits on it. - */ extern const struct wl_interface banner_ahb_v1_interface; #endif -/** - * @ingroup iface_banner_ahb_v1 - * @struct banner_ahb_v1_interface - */ struct banner_ahb_v1_interface { - /** - * destroy the object - * - * Buffers already attached keep their AHardwareBuffer - * association. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * hand the compositor a buffer's AHardwareBuffer - * - * Associates the AHardwareBuffer readable from socket with the - * (already created) dma-buf wl_buffer. width, height and stride - * (pixels) describe the gralloc allocation; modifier_hi/lo is the - * DRM format modifier the dma-buf was shared with (the same value - * passed to zwp_linux_buffer_params_v1.add); image_count is the - * number of images in the swapchain this buffer belongs to (for - * the compositor's log). Sent once per wl_buffer, before the - * buffer is first attached to a surface. - * @param buffer the dma-buf wl_buffer - * @param socket socket the AHardwareBuffer handle was written to - * @param width buffer width in pixels - * @param height buffer height in pixels - * @param stride gralloc row stride in pixels - * @param modifier_hi high 32 bits of the DRM format modifier - * @param modifier_lo low 32 bits of the DRM format modifier - * @param image_count images in the client's swapchain - */ void (*attach)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *buffer, @@ -181,26 +67,11 @@ struct banner_ahb_v1_interface { #define BANNER_AHB_V1_MODE 0 -/** - * @ingroup iface_banner_ahb_v1 - */ #define BANNER_AHB_V1_MODE_SINCE_VERSION 2 -/** - * @ingroup iface_banner_ahb_v1 - */ #define BANNER_AHB_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_banner_ahb_v1 - */ #define BANNER_AHB_V1_ATTACH_SINCE_VERSION 1 -/** - * @ingroup iface_banner_ahb_v1 - * Sends an mode event to the client owning the resource. - * @param resource_ The client's resource - * @param enabled 1 = gralloc swapchain images wanted, 0 = not - */ static inline void banner_ahb_v1_send_mode(struct wl_resource *resource_, uint32_t enabled) { diff --git a/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-protocol.c index 9ed8996dd..ae0a87daa 100644 --- a/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright 2026 The412Banner * @@ -29,7 +27,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-server-protocol.h index f2608a46e..78f65e7fb 100644 --- a/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/banner-desktop-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef BANNER_DESKTOP_V1_SERVER_PROTOCOL_H #define BANNER_DESKTOP_V1_SERVER_PROTOCOL_H @@ -14,12 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_banner_desktop_v1 The banner_desktop_v1 protocol - * @section page_ifaces_banner_desktop_v1 Interfaces - * - @subpage page_iface_banner_desktop_v1 - Windows virtual desktop layout +/* * @section page_copyright_banner_desktop_v1 Copyright - *
  *
  * Copyright 2026 The412Banner
  *
@@ -41,98 +36,41 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + struct banner_desktop_v1; struct wl_surface; #ifndef BANNER_DESKTOP_V1_INTERFACE #define BANNER_DESKTOP_V1_INTERFACE -/** - * @page page_iface_banner_desktop_v1 banner_desktop_v1 - * @section page_iface_banner_desktop_v1_desc Description - * - * Each Wine process is a separate Wayland client, so a Windows virtual - * desktop can't be built from subsurfaces. Instead the process that owns - * the desktop window marks one surface as the desktop, and every process - * reports where its top-level windows sit on that desktop and the global - * window stacking order. The compositor draws the desktop at the bottom, - * the windows at the reported positions, and sends pointer and keyboard - * input to the desktop surface; Wine routes it to the right window. - * @section page_iface_banner_desktop_v1_api API - * See @ref iface_banner_desktop_v1. - */ -/** - * @defgroup iface_banner_desktop_v1 The banner_desktop_v1 interface - * - * Each Wine process is a separate Wayland client, so a Windows virtual - * desktop can't be built from subsurfaces. Instead the process that owns - * the desktop window marks one surface as the desktop, and every process - * reports where its top-level windows sit on that desktop and the global - * window stacking order. The compositor draws the desktop at the bottom, - * the windows at the reported positions, and sends pointer and keyboard - * input to the desktop surface; Wine routes it to the right window. - */ extern const struct wl_interface banner_desktop_v1_interface; #endif -/** - * @ingroup iface_banner_desktop_v1 - * @struct banner_desktop_v1_interface - */ struct banner_desktop_v1_interface { - /** - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * mark a surface as the virtual desktop - * - * The surface is drawn at the origin, below every window, and - * sets the size of the desktop. - */ void (*set_desktop)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface); - /** - * place a top-level window on the desktop - * - * Position of the top-left corner of the surface in desktop - * coordinates, and the window handle used by set_zorder. - */ void (*set_window)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface, uint32_t hwnd, int32_t x, int32_t y); - /** - * report the desktop's window stacking order - * - * Visible top-level window handles as 32-bit values, topmost - * first. - */ void (*set_zorder)(struct wl_client *client, struct wl_resource *resource, struct wl_array *hwnds); }; -/** - * @ingroup iface_banner_desktop_v1 - */ #define BANNER_DESKTOP_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_banner_desktop_v1 - */ #define BANNER_DESKTOP_V1_SET_DESKTOP_SINCE_VERSION 1 -/** - * @ingroup iface_banner_desktop_v1 - */ #define BANNER_DESKTOP_V1_SET_WINDOW_SINCE_VERSION 1 -/** - * @ingroup iface_banner_desktop_v1 - */ #define BANNER_DESKTOP_V1_SET_ZORDER_SINCE_VERSION 1 #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/generated/color-management-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/color-management-v1-protocol.c index ff31f8a41..e4fe936c2 100644 --- a/app/src/main/cpp/waylandcomp/generated/color-management-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/color-management-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright 2019 Sebastian Wick * Copyright 2019 Erwin Burema @@ -34,7 +32,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/color-management-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/color-management-v1-server-protocol.h index 08008e8f8..edd25e45b 100644 --- a/app/src/main/cpp/waylandcomp/generated/color-management-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/color-management-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef COLOR_MANAGEMENT_V1_SERVER_PROTOCOL_H #define COLOR_MANAGEMENT_V1_SERVER_PROTOCOL_H @@ -14,64 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_color_management_v1 The color_management_v1 protocol - * color management protocol - * - * @section page_desc_color_management_v1 Description - * - * The aim of the color management extension is to allow clients to know - * the color properties of outputs, and to tell the compositor about the color - * properties of their content on surfaces. Doing this enables a compositor - * to perform automatic color management of content for different outputs - * according to how content is intended to look like. - * - * The color properties are represented as an image description object which - * is immutable after it has been created. A wl_output always has an - * associated image description that clients can observe. A wl_surface - * always has an associated preferred image description as a hint chosen by - * the compositor that clients can also observe. Clients can set an image - * description on a wl_surface to denote the color characteristics of the - * surface contents. - * - * An image description includes SDR and HDR colorimetry and encoding, HDR - * metadata, and viewing environment parameters. An image description does - * not include the properties set through color-representation extension. - * It is expected that the color-representation extension is used in - * conjunction with the color management extension when necessary, - * particularly with the YUV family of pixel formats. - * - * Recommendation ITU-T H.273 - * "Coding-independent code points for video signal type identification" - * shall be referred to as simply H.273 here. - * - * The color-and-hdr repository - * (https://gitlab.freedesktop.org/pq/color-and-hdr) contains - * background information on the protocol design and legacy color management. - * It also contains a glossary, learning resources for digital color, tools, - * samples and more. - * - * The terminology used in this protocol is based on common color science and - * color encoding terminology where possible. The glossary in the color-and-hdr - * repository shall be the authority on the definition of terms in this - * protocol. - * - * Warning! The protocol described in this file is currently in the testing - * phase. Backward compatible changes may be added together with the - * corresponding interface version bump. Backward incompatible changes can - * only be done by creating a new major version of the extension. - * - * @section page_ifaces_color_management_v1 Interfaces - * - @subpage page_iface_wp_color_manager_v1 - color manager singleton - * - @subpage page_iface_wp_color_management_output_v1 - output color properties - * - @subpage page_iface_wp_color_management_surface_v1 - color management extension to a surface - * - @subpage page_iface_wp_color_management_surface_feedback_v1 - color management extension to a surface - * - @subpage page_iface_wp_image_description_creator_icc_v1 - holder of image description ICC information - * - @subpage page_iface_wp_image_description_creator_params_v1 - holder of image description parameters - * - @subpage page_iface_wp_image_description_v1 - Colorimetric image description - * - @subpage page_iface_wp_image_description_info_v1 - Colorimetric image description information +/* * @section page_copyright_color_management_v1 Copyright - *
  *
  * Copyright 2019 Sebastian Wick
  * Copyright 2019 Erwin Burema
@@ -98,8 +41,64 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_output; struct wl_surface; struct wp_color_management_output_v1; @@ -113,360 +112,47 @@ struct wp_image_description_v1; #ifndef WP_COLOR_MANAGER_V1_INTERFACE #define WP_COLOR_MANAGER_V1_INTERFACE -/** - * @page page_iface_wp_color_manager_v1 wp_color_manager_v1 - * @section page_iface_wp_color_manager_v1_desc Description - * - * A singleton global interface used for getting color management extensions - * for wl_surface and wl_output objects, and for creating client defined - * image description objects. The extension interfaces allow - * getting the image description of outputs and setting the image - * description of surfaces. - * - * Compositors should never remove this global. - * @section page_iface_wp_color_manager_v1_api API - * See @ref iface_wp_color_manager_v1. - */ -/** - * @defgroup iface_wp_color_manager_v1 The wp_color_manager_v1 interface - * - * A singleton global interface used for getting color management extensions - * for wl_surface and wl_output objects, and for creating client defined - * image description objects. The extension interfaces allow - * getting the image description of outputs and setting the image - * description of surfaces. - * - * Compositors should never remove this global. - */ extern const struct wl_interface wp_color_manager_v1_interface; #endif #ifndef WP_COLOR_MANAGEMENT_OUTPUT_V1_INTERFACE #define WP_COLOR_MANAGEMENT_OUTPUT_V1_INTERFACE -/** - * @page page_iface_wp_color_management_output_v1 wp_color_management_output_v1 - * @section page_iface_wp_color_management_output_v1_desc Description - * - * A wp_color_management_output_v1 describes the color properties of an - * output. - * - * The wp_color_management_output_v1 is associated with the wl_output global - * underlying the wl_output object. Therefore the client destroying the - * wl_output object has no impact, but the compositor removing the output - * global makes the wp_color_management_output_v1 object inert. - * @section page_iface_wp_color_management_output_v1_api API - * See @ref iface_wp_color_management_output_v1. - */ -/** - * @defgroup iface_wp_color_management_output_v1 The wp_color_management_output_v1 interface - * - * A wp_color_management_output_v1 describes the color properties of an - * output. - * - * The wp_color_management_output_v1 is associated with the wl_output global - * underlying the wl_output object. Therefore the client destroying the - * wl_output object has no impact, but the compositor removing the output - * global makes the wp_color_management_output_v1 object inert. - */ extern const struct wl_interface wp_color_management_output_v1_interface; #endif #ifndef WP_COLOR_MANAGEMENT_SURFACE_V1_INTERFACE #define WP_COLOR_MANAGEMENT_SURFACE_V1_INTERFACE -/** - * @page page_iface_wp_color_management_surface_v1 wp_color_management_surface_v1 - * @section page_iface_wp_color_management_surface_v1_desc Description - * - * A wp_color_management_surface_v1 allows the client to set the color - * space and HDR properties of a surface. - * - * If the wl_surface associated with the wp_color_management_surface_v1 is - * destroyed, the wp_color_management_surface_v1 object becomes inert. - * @section page_iface_wp_color_management_surface_v1_api API - * See @ref iface_wp_color_management_surface_v1. - */ -/** - * @defgroup iface_wp_color_management_surface_v1 The wp_color_management_surface_v1 interface - * - * A wp_color_management_surface_v1 allows the client to set the color - * space and HDR properties of a surface. - * - * If the wl_surface associated with the wp_color_management_surface_v1 is - * destroyed, the wp_color_management_surface_v1 object becomes inert. - */ extern const struct wl_interface wp_color_management_surface_v1_interface; #endif #ifndef WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_INTERFACE #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_INTERFACE -/** - * @page page_iface_wp_color_management_surface_feedback_v1 wp_color_management_surface_feedback_v1 - * @section page_iface_wp_color_management_surface_feedback_v1_desc Description - * - * A wp_color_management_surface_feedback_v1 allows the client to get the - * preferred image description of a surface. - * - * If the wl_surface associated with this object is destroyed, the - * wp_color_management_surface_feedback_v1 object becomes inert. - * @section page_iface_wp_color_management_surface_feedback_v1_api API - * See @ref iface_wp_color_management_surface_feedback_v1. - */ -/** - * @defgroup iface_wp_color_management_surface_feedback_v1 The wp_color_management_surface_feedback_v1 interface - * - * A wp_color_management_surface_feedback_v1 allows the client to get the - * preferred image description of a surface. - * - * If the wl_surface associated with this object is destroyed, the - * wp_color_management_surface_feedback_v1 object becomes inert. - */ extern const struct wl_interface wp_color_management_surface_feedback_v1_interface; #endif #ifndef WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_INTERFACE #define WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_INTERFACE -/** - * @page page_iface_wp_image_description_creator_icc_v1 wp_image_description_creator_icc_v1 - * @section page_iface_wp_image_description_creator_icc_v1_desc Description - * - * This type of object is used for collecting all the information required - * to create a wp_image_description_v1 object from an ICC file. A complete - * set of required parameters consists of these properties: - * - ICC file - * - * Each required property must be set exactly once if the client is to create - * an image description. The set requests verify that a property was not - * already set. The create request verifies that all required properties are - * set. There may be several alternative requests for setting each property, - * and in that case the client must choose one of them. - * - * Once all properties have been set, the create request must be used to - * create the image description object, destroying the creator in the - * process. - * @section page_iface_wp_image_description_creator_icc_v1_api API - * See @ref iface_wp_image_description_creator_icc_v1. - */ -/** - * @defgroup iface_wp_image_description_creator_icc_v1 The wp_image_description_creator_icc_v1 interface - * - * This type of object is used for collecting all the information required - * to create a wp_image_description_v1 object from an ICC file. A complete - * set of required parameters consists of these properties: - * - ICC file - * - * Each required property must be set exactly once if the client is to create - * an image description. The set requests verify that a property was not - * already set. The create request verifies that all required properties are - * set. There may be several alternative requests for setting each property, - * and in that case the client must choose one of them. - * - * Once all properties have been set, the create request must be used to - * create the image description object, destroying the creator in the - * process. - */ extern const struct wl_interface wp_image_description_creator_icc_v1_interface; #endif #ifndef WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_INTERFACE #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_INTERFACE -/** - * @page page_iface_wp_image_description_creator_params_v1 wp_image_description_creator_params_v1 - * @section page_iface_wp_image_description_creator_params_v1_desc Description - * - * This type of object is used for collecting all the parameters required - * to create a wp_image_description_v1 object. A complete set of required - * parameters consists of these properties: - * - transfer characteristic function (tf) - * - chromaticities of primaries and white point (primary color volume) - * - * The following properties are optional and have a well-defined default - * if not explicitly set: - * - primary color volume luminance range - * - reference white luminance level - * - mastering display primaries and white point (target color volume) - * - mastering luminance range - * - * The following properties are optional and will be ignored - * if not explicitly set: - * - maximum content light level - * - maximum frame-average light level - * - * Each required property must be set exactly once if the client is to create - * an image description. The set requests verify that a property was not - * already set. The create request verifies that all required properties are - * set. There may be several alternative requests for setting each property, - * and in that case the client must choose one of them. - * - * Once all properties have been set, the create request must be used to - * create the image description object, destroying the creator in the - * process. - * @section page_iface_wp_image_description_creator_params_v1_api API - * See @ref iface_wp_image_description_creator_params_v1. - */ -/** - * @defgroup iface_wp_image_description_creator_params_v1 The wp_image_description_creator_params_v1 interface - * - * This type of object is used for collecting all the parameters required - * to create a wp_image_description_v1 object. A complete set of required - * parameters consists of these properties: - * - transfer characteristic function (tf) - * - chromaticities of primaries and white point (primary color volume) - * - * The following properties are optional and have a well-defined default - * if not explicitly set: - * - primary color volume luminance range - * - reference white luminance level - * - mastering display primaries and white point (target color volume) - * - mastering luminance range - * - * The following properties are optional and will be ignored - * if not explicitly set: - * - maximum content light level - * - maximum frame-average light level - * - * Each required property must be set exactly once if the client is to create - * an image description. The set requests verify that a property was not - * already set. The create request verifies that all required properties are - * set. There may be several alternative requests for setting each property, - * and in that case the client must choose one of them. - * - * Once all properties have been set, the create request must be used to - * create the image description object, destroying the creator in the - * process. - */ extern const struct wl_interface wp_image_description_creator_params_v1_interface; #endif #ifndef WP_IMAGE_DESCRIPTION_V1_INTERFACE #define WP_IMAGE_DESCRIPTION_V1_INTERFACE -/** - * @page page_iface_wp_image_description_v1 wp_image_description_v1 - * @section page_iface_wp_image_description_v1_desc Description - * - * An image description carries information about the color encoding used on - * a surface when attached to a wl_surface via - * wp_color_management_surface_v1.set_image_description. A compositor can use - * this information to decode pixel values into colorimetrically meaningful - * quantities. - * - * Note, that the wp_image_description_v1 object is not ready to be used - * immediately after creation. The object eventually delivers either the - * 'ready' or the 'failed' event, specified in all requests creating it. The - * object is deemed "ready" after receiving the 'ready' event. - * - * An object which is not ready is illegal to use, it can only be destroyed. - * Any other request in this interface shall result in the 'not_ready' - * protocol error. Attempts to use an object which is not ready through other - * interfaces shall raise protocol errors defined there. - * - * Once created and regardless of how it was created, a - * wp_image_description_v1 object always refers to one fixed image - * description. It cannot change after creation. - * @section page_iface_wp_image_description_v1_api API - * See @ref iface_wp_image_description_v1. - */ -/** - * @defgroup iface_wp_image_description_v1 The wp_image_description_v1 interface - * - * An image description carries information about the color encoding used on - * a surface when attached to a wl_surface via - * wp_color_management_surface_v1.set_image_description. A compositor can use - * this information to decode pixel values into colorimetrically meaningful - * quantities. - * - * Note, that the wp_image_description_v1 object is not ready to be used - * immediately after creation. The object eventually delivers either the - * 'ready' or the 'failed' event, specified in all requests creating it. The - * object is deemed "ready" after receiving the 'ready' event. - * - * An object which is not ready is illegal to use, it can only be destroyed. - * Any other request in this interface shall result in the 'not_ready' - * protocol error. Attempts to use an object which is not ready through other - * interfaces shall raise protocol errors defined there. - * - * Once created and regardless of how it was created, a - * wp_image_description_v1 object always refers to one fixed image - * description. It cannot change after creation. - */ extern const struct wl_interface wp_image_description_v1_interface; #endif #ifndef WP_IMAGE_DESCRIPTION_INFO_V1_INTERFACE #define WP_IMAGE_DESCRIPTION_INFO_V1_INTERFACE -/** - * @page page_iface_wp_image_description_info_v1 wp_image_description_info_v1 - * @section page_iface_wp_image_description_info_v1_desc Description - * - * Sends all matching events describing an image description object exactly - * once and finally sends the 'done' event. - * - * This means - * - if the image description is parametric, it must send - * - primaries - * - named_primaries, if applicable - * - at least one of tf_power and tf_named, as applicable - * - luminances - * - target_primaries - * - target_luminance - * - if the image description is parametric, it may send, if applicable, - * - target_max_cll - * - target_max_fall - * - if the image description contains an ICC profile, it must send the - * icc_file event - * - * Once a wp_image_description_info_v1 object has delivered a 'done' event it - * is automatically destroyed. - * - * Every wp_image_description_info_v1 created from the same - * wp_image_description_v1 shall always return the exact same data. - * @section page_iface_wp_image_description_info_v1_api API - * See @ref iface_wp_image_description_info_v1. - */ -/** - * @defgroup iface_wp_image_description_info_v1 The wp_image_description_info_v1 interface - * - * Sends all matching events describing an image description object exactly - * once and finally sends the 'done' event. - * - * This means - * - if the image description is parametric, it must send - * - primaries - * - named_primaries, if applicable - * - at least one of tf_power and tf_named, as applicable - * - luminances - * - target_primaries - * - target_luminance - * - if the image description is parametric, it may send, if applicable, - * - target_max_cll - * - target_max_fall - * - if the image description contains an ICC profile, it must send the - * icc_file event - * - * Once a wp_image_description_info_v1 object has delivered a 'done' event it - * is automatically destroyed. - * - * Every wp_image_description_info_v1 created from the same - * wp_image_description_v1 shall always return the exact same data. - */ extern const struct wl_interface wp_image_description_info_v1_interface; #endif #ifndef WP_COLOR_MANAGER_V1_ERROR_ENUM #define WP_COLOR_MANAGER_V1_ERROR_ENUM enum wp_color_manager_v1_error { - /** - * request not supported - */ WP_COLOR_MANAGER_V1_ERROR_UNSUPPORTED_FEATURE = 0, - /** - * color management surface exists already - */ WP_COLOR_MANAGER_V1_ERROR_SURFACE_EXISTS = 1, }; -#endif /* WP_COLOR_MANAGER_V1_ERROR_ENUM */ +#endif #ifndef WP_COLOR_MANAGER_V1_ERROR_ENUM_IS_VALID #define WP_COLOR_MANAGER_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_manager_v1 - * Validate a wp_color_manager_v1 error value. - * - * @return true on success, false on error. - * @ref wp_color_manager_v1_error - */ static inline bool wp_color_manager_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -478,56 +164,21 @@ wp_color_manager_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_COLOR_MANAGER_V1_ERROR_ENUM_IS_VALID */ +#endif #ifndef WP_COLOR_MANAGER_V1_RENDER_INTENT_ENUM #define WP_COLOR_MANAGER_V1_RENDER_INTENT_ENUM -/** - * @ingroup iface_wp_color_manager_v1 - * rendering intents - * - * See the ICC.1:2022 specification from the International Color Consortium - * for more details about rendering intents. - * - * The principles of ICC defined rendering intents apply with all types of - * image descriptions, not only those with ICC file profiles. - * - * Compositors must support the perceptual rendering intent. Other - * rendering intents are optional. - */ enum wp_color_manager_v1_render_intent { - /** - * perceptual - */ WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL = 0, - /** - * media-relative colorimetric - */ WP_COLOR_MANAGER_V1_RENDER_INTENT_RELATIVE = 1, - /** - * saturation - */ WP_COLOR_MANAGER_V1_RENDER_INTENT_SATURATION = 2, - /** - * ICC-absolute colorimetric - */ WP_COLOR_MANAGER_V1_RENDER_INTENT_ABSOLUTE = 3, - /** - * media-relative colorimetric + black point compensation - */ WP_COLOR_MANAGER_V1_RENDER_INTENT_RELATIVE_BPC = 4, }; -#endif /* WP_COLOR_MANAGER_V1_RENDER_INTENT_ENUM */ +#endif #ifndef WP_COLOR_MANAGER_V1_RENDER_INTENT_ENUM_IS_VALID #define WP_COLOR_MANAGER_V1_RENDER_INTENT_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_manager_v1 - * Validate a wp_color_manager_v1 render_intent value. - * - * @return true on success, false on error. - * @ref wp_color_manager_v1_render_intent - */ static inline bool wp_color_manager_v1_render_intent_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -545,69 +196,24 @@ wp_color_manager_v1_render_intent_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_COLOR_MANAGER_V1_RENDER_INTENT_ENUM_IS_VALID */ +#endif #ifndef WP_COLOR_MANAGER_V1_FEATURE_ENUM #define WP_COLOR_MANAGER_V1_FEATURE_ENUM -/** - * @ingroup iface_wp_color_manager_v1 - * compositor supported features - */ enum wp_color_manager_v1_feature { - /** - * create_icc_creator request - */ WP_COLOR_MANAGER_V1_FEATURE_ICC_V2_V4 = 0, - /** - * create_parametric_creator request - */ WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC = 1, - /** - * parametric set_primaries request - */ WP_COLOR_MANAGER_V1_FEATURE_SET_PRIMARIES = 2, - /** - * parametric set_tf_power request - */ WP_COLOR_MANAGER_V1_FEATURE_SET_TF_POWER = 3, - /** - * parametric set_luminances request - */ WP_COLOR_MANAGER_V1_FEATURE_SET_LUMINANCES = 4, - /** - * parametric set_mastering_display_primaries request - * - * The compositor supports set_mastering_display_primaries - * request with a target color volume fully contained inside the - * primary color volume. - */ WP_COLOR_MANAGER_V1_FEATURE_SET_MASTERING_DISPLAY_PRIMARIES = 5, - /** - * parametric target exceeds primary color volume - * - * The compositor additionally supports target color volumes that - * extend outside of the primary color volume. - * - * This can only be advertised if feature - * set_mastering_display_primaries is supported as well. - */ WP_COLOR_MANAGER_V1_FEATURE_EXTENDED_TARGET_VOLUME = 6, - /** - * get_windows_scrgb request - */ WP_COLOR_MANAGER_V1_FEATURE_WINDOWS_SCRGB = 7, }; -#endif /* WP_COLOR_MANAGER_V1_FEATURE_ENUM */ +#endif #ifndef WP_COLOR_MANAGER_V1_FEATURE_ENUM_IS_VALID #define WP_COLOR_MANAGER_V1_FEATURE_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_manager_v1 - * Validate a wp_color_manager_v1 feature value. - * - * @return true on success, false on error. - * @ref wp_color_manager_v1_feature - */ static inline bool wp_color_manager_v1_feature_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -631,121 +237,26 @@ wp_color_manager_v1_feature_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_COLOR_MANAGER_V1_FEATURE_ENUM_IS_VALID */ +#endif #ifndef WP_COLOR_MANAGER_V1_PRIMARIES_ENUM #define WP_COLOR_MANAGER_V1_PRIMARIES_ENUM -/** - * @ingroup iface_wp_color_manager_v1 - * named color primaries - * - * Named color primaries used to encode well-known sets of primaries. H.273 - * is the authority, when it comes to the exact values of primaries and - * authoritative specifications, where an equivalent code point exists. - * - * A value of 0 is invalid and will never be present in the list of enums. - * - * Descriptions do list the specifications for convenience. - */ enum wp_color_manager_v1_primaries { - /** - * Color primaries for the sRGB color space as defined by the BT.709 standard - * - * Color primaries as defined by - Rec. ITU-R BT.709-6 - Rec. - * ITU-R BT.1361-0 conventional colour gamut system and extended - * colour gamut system (historical) - IEC 61966-2-1 sRGB or sYCC - - * IEC 61966-2-4 - Society of Motion Picture and Television - * Engineers (SMPTE) RP 177 (1993) Annex B Equivalent to H.273 - * ColourPrimaries code point 1. - */ WP_COLOR_MANAGER_V1_PRIMARIES_SRGB = 1, - /** - * Color primaries for PAL-M as defined by the BT.470 standard - * - * Color primaries as defined by - Rec. ITU-R BT.470-6 System M - * (historical) - United States National Television System - * Committee 1953 Recommendation for transmission standards for - * color television - United States Federal Communications - * Commission (2003) Title 47 Code of Federal Regulations 73.682 - * (a)(20) Equivalent to H.273 ColourPrimaries code point 4. - */ WP_COLOR_MANAGER_V1_PRIMARIES_PAL_M = 2, - /** - * Color primaries for PAL as defined by the BT.601 standard - * - * Color primaries as defined by - Rec. ITU-R BT.470-6 System B, - * G (historical) - Rec. ITU-R BT.601-7 625 - Rec. ITU-R BT.1358-0 - * 625 (historical) - Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM - * Equivalent to H.273 ColourPrimaries code point 5. - */ WP_COLOR_MANAGER_V1_PRIMARIES_PAL = 3, - /** - * Color primaries for NTSC as defined by the BT.601 standard - * - * Color primaries as defined by - Rec. ITU-R BT.601-7 525 - Rec. - * ITU-R BT.1358-1 525 or 625 (historical) - Rec. ITU-R BT.1700-0 - * NTSC - SMPTE 170M (2004) - SMPTE 240M (1999) (historical) - * Equivalent to H.273 ColourPrimaries code point 6 and 7. - */ WP_COLOR_MANAGER_V1_PRIMARIES_NTSC = 4, - /** - * Generic film with colour filters using Illuminant C - * - * Color primaries as defined by H.273 for generic film. - * Equivalent to H.273 ColourPrimaries code point 8. - */ WP_COLOR_MANAGER_V1_PRIMARIES_GENERIC_FILM = 5, - /** - * Color primaries as defined by the BT.2020 and BT.2100 standard - * - * Color primaries as defined by - Rec. ITU-R BT.2020-2 - Rec. - * ITU-R BT.2100-0 Equivalent to H.273 ColourPrimaries code point - * 9. - */ WP_COLOR_MANAGER_V1_PRIMARIES_BT2020 = 6, - /** - * Color primaries of the full CIE 1931 XYZ color space - * - * Color primaries as defined as the maximum of the CIE 1931 XYZ - * color space by - SMPTE ST 428-1 - (CIE 1931 XYZ as in ISO - * 11664-1) Equivalent to H.273 ColourPrimaries code point 10. - */ WP_COLOR_MANAGER_V1_PRIMARIES_CIE1931_XYZ = 7, - /** - * Color primaries of the DCI P3 color space as defined by the SMPTE RP 431 standard - * - * Color primaries as defined by Digital Cinema System and - * published in SMPTE RP 431-2 (2011). Equivalent to H.273 - * ColourPrimaries code point 11. - */ WP_COLOR_MANAGER_V1_PRIMARIES_DCI_P3 = 8, - /** - * Color primaries of Display P3 variant of the DCI-P3 color space as defined by the SMPTE EG 432 standard - * - * Color primaries as defined by Digital Cinema System and - * published in SMPTE EG 432-1 (2010). Equivalent to H.273 - * ColourPrimaries code point 12. - */ WP_COLOR_MANAGER_V1_PRIMARIES_DISPLAY_P3 = 9, - /** - * Color primaries of the Adobe RGB color space as defined by the ISO 12640 standard - * - * Color primaries as defined by Adobe as "Adobe RGB" and later - * published by ISO 12640-4 (2011). - */ WP_COLOR_MANAGER_V1_PRIMARIES_ADOBE_RGB = 10, }; -#endif /* WP_COLOR_MANAGER_V1_PRIMARIES_ENUM */ +#endif #ifndef WP_COLOR_MANAGER_V1_PRIMARIES_ENUM_IS_VALID #define WP_COLOR_MANAGER_V1_PRIMARIES_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_manager_v1 - * Validate a wp_color_manager_v1 primaries value. - * - * @return true on success, false on error. - * @ref wp_color_manager_v1_primaries - */ static inline bool wp_color_manager_v1_primaries_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -773,173 +284,29 @@ wp_color_manager_v1_primaries_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_COLOR_MANAGER_V1_PRIMARIES_ENUM_IS_VALID */ +#endif #ifndef WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ENUM #define WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ENUM -/** - * @ingroup iface_wp_color_manager_v1 - * named transfer functions - * - * Named transfer functions used to represent well-known transfer - * characteristics. H.273 is the authority, when it comes to the exact - * formulas and authoritative specifications, where an equivalent code - * point exists. - * - * A value of 0 is invalid and will never be present in the list of enums. - * - * Descriptions do list the specifications for convenience. - */ enum wp_color_manager_v1_transfer_function { - /** - * BT.1886 display transfer characteristic - * - * Rec. ITU-R BT.1886 is the display transfer characteristic - * assumed by - Rec. ITU-R BT.601-7 525 and 625 - Rec. ITU-R - * BT.709-6 - Rec. ITU-R BT.2020-2 These recommendations are - * referred to by H.273 TransferCharacteristics code points 1, 6, - * 14, and 15, which are all equivalent. - * - * This TF implies these default luminances from Rec. ITU-R - * BT.2035: - primary color volume minimum: 0.01 cd/m² - primary - * color volume maximum: 100 cd/m² - reference white: 100 cd/m² - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_BT1886 = 1, - /** - * Assumed display gamma 2.2 transfer function - * - * Transfer characteristics as defined by - Rec. ITU-R BT.470-6 - * System M (historical) - United States National Television System - * Committee 1953 Recommendation for transmission standards for - * color television - United States Federal Communications - * Commission (2003) Title 47 Code of Federal Regulations 73.682 - * (a) (20) - Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM Equivalent - * to H.273 TransferCharacteristics code point 4. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22 = 2, - /** - * Assumed display gamma 2.8 transfer function - * - * Transfer characteristics as defined by - Rec. ITU-R BT.470-6 - * System B, G (historical) Equivalent to H.273 - * TransferCharacteristics code point 5. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA28 = 3, - /** - * SMPTE ST 240 transfer function - * - * Transfer characteristics as defined by - SMPTE ST 240 (1999) - * Equivalent to H.273 TransferCharacteristics code point 7. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST240 = 4, - /** - * extended linear transfer function - * - * Linear transfer function defined over all real numbers. - * Normalised electrical values are equal the normalised optical - * values. - * - * The differences to H.273 TransferCharacteristics code point 8 - * are the definition over all real numbers. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR = 5, - /** - * logarithmic 100:1 transfer function - * - * Logarithmic transfer characteristic (100:1 range). Equivalent - * to H.273 TransferCharacteristics code point 9. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_100 = 6, - /** - * logarithmic (100*Sqrt(10) : 1) transfer function - * - * Logarithmic transfer characteristic (100 * Sqrt(10) : 1 - * range). Equivalent to H.273 TransferCharacteristics code point - * 10. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_316 = 7, - /** - * IEC 61966-2-4 transfer function - * - * Transfer characteristics as defined by - IEC 61966-2-4 - * Equivalent to H.273 TransferCharacteristics code point 11. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_XVYCC = 8, - /** - * sRGB piece-wise transfer function - * - * Transfer characteristics as defined by - IEC 61966-2-1 sRGB - * Equivalent to H.273 TransferCharacteristics code point 13 with - * MatrixCoefficients set to 0. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_SRGB = 9, - /** - * Extended sRGB piece-wise transfer function - * - * Transfer characteristics as defined by - IEC 61966-2-1 sYCC - * Equivalent to H.273 TransferCharacteristics code point 13 with - * MatrixCoefficients set to anything but 0. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB = 10, - /** - * perceptual quantizer transfer function - * - * Transfer characteristics as defined by - SMPTE ST 2084 (2014) - * for 10-, 12-, 14- and 16-bit systems - Rec. ITU-R BT.2100-2 - * perceptual quantization (PQ) system Equivalent to H.273 - * TransferCharacteristics code point 16. - * - * This TF implies these default luminances - primary color volume - * minimum: 0.005 cd/m² - primary color volume maximum: 10000 - * cd/m² - reference white: 203 cd/m² - * - * The difference between the primary color volume minimum and - * maximum must be approximately 10000 cd/m² as that is the swing - * of the EOTF defined by ST 2084 and BT.2100. The default value - * for the reference white is a protocol addition: it is suggested - * by Report ITU-R BT.2408-7 and is not part of ST 2084 or BT.2100. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ = 11, - /** - * SMPTE ST 428 transfer function - * - * Transfer characteristics as defined by - SMPTE ST 428-1 (2019) - * Equivalent to H.273 TransferCharacteristics code point 17. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST428 = 12, - /** - * hybrid log-gamma transfer function - * - * Transfer characteristics as defined by - ARIB STD-B67 (2015) - - * Rec. ITU-R BT.2100-2 hybrid log-gamma (HLG) system Equivalent to - * H.273 TransferCharacteristics code point 18. - * - * This TF implies these default luminances - primary color volume - * minimum: 0.005 cd/m² - primary color volume maximum: 1000 - * cd/m² - reference white: 203 cd/m² - * - * HLG is a relative display-referred signal with a specified - * non-linear mapping to the display peak luminance (the HLG OOTF). - * All absolute luminance values used here for HLG assume a 1000 - * cd/m² peak display. - * - * The default value for the reference white is a protocol - * addition: it is suggested by Report ITU-R BT.2408-7 and is not - * part of ARIB STD-B67 or BT.2100. - */ WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG = 13, }; -#endif /* WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ENUM */ +#endif #ifndef WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ENUM_IS_VALID #define WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_manager_v1 - * Validate a wp_color_manager_v1 transfer_function value. - * - * @return true on success, false on error. - * @ref wp_color_manager_v1_transfer_function - */ static inline bool wp_color_manager_v1_transfer_function_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -973,149 +340,29 @@ wp_color_manager_v1_transfer_function_is_valid(uint32_t value, uint32_t version) return false; } } -#endif /* WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_color_manager_v1 - * @struct wp_color_manager_v1_interface - */ struct wp_color_manager_v1_interface { - /** - * destroy the color manager - * - * Destroy the wp_color_manager_v1 object. This does not affect - * any other objects in any way. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * create a color management interface for a wl_output - * - * This creates a new wp_color_management_output_v1 object for - * the given wl_output. - * - * See the wp_color_management_output_v1 interface for more - * details. - */ void (*get_output)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *output); - /** - * create a color management interface for a wl_surface - * - * If a wp_color_management_surface_v1 object already exists for - * the given wl_surface, the protocol error surface_exists is - * raised. - * - * This creates a new color wp_color_management_surface_v1 object - * for the given wl_surface. - * - * See the wp_color_management_surface_v1 interface for more - * details. - */ void (*get_surface)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *surface); - /** - * create a color management feedback interface - * - * This creates a new color - * wp_color_management_surface_feedback_v1 object for the given - * wl_surface. - * - * See the wp_color_management_surface_feedback_v1 interface for - * more details. - */ void (*get_surface_feedback)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *surface); - /** - * make a new ICC-based image description creator object - * - * Makes a new ICC-based image description creator object with - * all properties initially unset. The client can then use the - * object's interface to define all the required properties for an - * image description and finally create a wp_image_description_v1 - * object. - * - * This request can be used when the compositor advertises - * wp_color_manager_v1.feature.icc_v2_v4. Otherwise this request - * raises the protocol error unsupported_feature. - * @param obj the new creator object - */ void (*create_icc_creator)(struct wl_client *client, struct wl_resource *resource, uint32_t obj); - /** - * make a new parametric image description creator object - * - * Makes a new parametric image description creator object with - * all properties initially unset. The client can then use the - * object's interface to define all the required properties for an - * image description and finally create a wp_image_description_v1 - * object. - * - * This request can be used when the compositor advertises - * wp_color_manager_v1.feature.parametric. Otherwise this request - * raises the protocol error unsupported_feature. - * @param obj the new creator object - */ void (*create_parametric_creator)(struct wl_client *client, struct wl_resource *resource, uint32_t obj); - /** - * create Windows-scRGB image description object - * - * This creates a pre-defined image description for the so-called - * Windows-scRGB stimulus encoding. This comes from the Windows 10 - * handling of its own definition of an scRGB color space for an - * HDR screen driven in BT.2100/PQ signalling mode. - * - * Windows-scRGB uses sRGB (BT.709) color primaries and white - * point. The transfer characteristic is extended linear. - * - * The nominal color channel value range is extended, meaning it - * includes negative and greater than 1.0 values. Negative values - * are used to escape the sRGB color gamut boundaries. To make use - * of the extended range, the client needs to use a pixel format - * that can represent those values, e.g. floating-point 16 bits per - * channel. - * - * Nominal color value R=G=B=0.0 corresponds to BT.2100/PQ system 0 - * cd/m², and R=G=B=1.0 corresponds to BT.2100/PQ system 80 - * cd/m². The maximum is R=G=B=125.0 corresponding to 10k cd/m². - * - * Windows-scRGB is displayed by Windows 10 by converting it to - * BT.2100/PQ, maintaining the CIE 1931 chromaticity and mapping - * the luminance as above. No adjustment is made to the signal to - * account for the viewing conditions. - * - * The reference white level of Windows-scRGB is unknown. If a - * reference white level must be assumed for compositor processing, - * it should be R=G=B=2.5375 corresponding to 203 cd/m² of Report - * ITU-R BT.2408-7. - * - * The target color volume of Windows-scRGB is unknown. The color - * gamut may be anything between sRGB and BT.2100. - * - * Note: EGL_EXT_gl_colorspace_scrgb_linear definition differs from - * Windows-scRGB by using R=G=B=1.0 as the reference white level, - * while Windows-scRGB reference white level is unknown or varies. - * However, it seems probable that Windows implements both - * EGL_EXT_gl_colorspace_scrgb_linear and Vulkan - * VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT as Windows-scRGB. - * - * This request can be used when the compositor advertises - * wp_color_manager_v1.feature.windows_scrgb. Otherwise this - * request raises the protocol error unsupported_feature. - * - * The resulting image description object does not allow - * get_information request. The wp_image_description_v1.ready event - * shall be sent. - */ void (*create_windows_scrgb)(struct wl_client *client, struct wl_resource *resource, uint32_t image_description); @@ -1127,165 +374,53 @@ struct wp_color_manager_v1_interface { #define WP_COLOR_MANAGER_V1_SUPPORTED_PRIMARIES_NAMED 3 #define WP_COLOR_MANAGER_V1_DONE 4 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_SUPPORTED_INTENT_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_SUPPORTED_FEATURE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_SUPPORTED_TF_NAMED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_SUPPORTED_PRIMARIES_NAMED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_GET_OUTPUT_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_GET_SURFACE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_GET_SURFACE_FEEDBACK_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_CREATE_ICC_CREATOR_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_CREATE_PARAMETRIC_CREATOR_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - */ #define WP_COLOR_MANAGER_V1_CREATE_WINDOWS_SCRGB_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_manager_v1 - * Sends an supported_intent event to the client owning the resource. - * @param resource_ The client's resource - * @param render_intent rendering intent - */ static inline void wp_color_manager_v1_send_supported_intent(struct wl_resource *resource_, uint32_t render_intent) { wl_resource_post_event(resource_, WP_COLOR_MANAGER_V1_SUPPORTED_INTENT, render_intent); } -/** - * @ingroup iface_wp_color_manager_v1 - * Sends an supported_feature event to the client owning the resource. - * @param resource_ The client's resource - * @param feature supported feature - */ static inline void wp_color_manager_v1_send_supported_feature(struct wl_resource *resource_, uint32_t feature) { wl_resource_post_event(resource_, WP_COLOR_MANAGER_V1_SUPPORTED_FEATURE, feature); } -/** - * @ingroup iface_wp_color_manager_v1 - * Sends an supported_tf_named event to the client owning the resource. - * @param resource_ The client's resource - * @param tf Named transfer function - */ static inline void wp_color_manager_v1_send_supported_tf_named(struct wl_resource *resource_, uint32_t tf) { wl_resource_post_event(resource_, WP_COLOR_MANAGER_V1_SUPPORTED_TF_NAMED, tf); } -/** - * @ingroup iface_wp_color_manager_v1 - * Sends an supported_primaries_named event to the client owning the resource. - * @param resource_ The client's resource - * @param primaries Named color primaries - */ static inline void wp_color_manager_v1_send_supported_primaries_named(struct wl_resource *resource_, uint32_t primaries) { wl_resource_post_event(resource_, WP_COLOR_MANAGER_V1_SUPPORTED_PRIMARIES_NAMED, primaries); } -/** - * @ingroup iface_wp_color_manager_v1 - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wp_color_manager_v1_send_done(struct wl_resource *resource_) { wl_resource_post_event(resource_, WP_COLOR_MANAGER_V1_DONE); } -/** - * @ingroup iface_wp_color_management_output_v1 - * @struct wp_color_management_output_v1_interface - */ struct wp_color_management_output_v1_interface { - /** - * destroy the color management output - * - * Destroy the color wp_color_management_output_v1 object. This - * does not affect any remaining protocol objects. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * get the image description of the output - * - * This creates a new wp_image_description_v1 object for the - * current image description of the output. There always is exactly - * one image description active for an output so the client should - * destroy the image description created by earlier invocations of - * this request. This request is usually sent as a reaction to the - * image_description_changed event or when creating a - * wp_color_management_output_v1 object. - * - * The image description of an output represents the color encoding - * the output expects. There might be performance and power - * advantages, as well as improved color reproduction, if a content - * update matches the image description of the output it is being - * shown on. If a content update is shown on any other output than - * the one it matches the image description of, then the color - * reproduction on those outputs might be considerably worse. - * - * The created wp_image_description_v1 object preserves the image - * description of the output from the time the object was created. - * - * The resulting image description object allows get_information - * request. - * - * If this protocol object is inert, the resulting image - * description object shall immediately deliver the - * wp_image_description_v1.failed event with the no_output cause. - * - * If the interface version is inadequate for the output's image - * description, meaning that the client does not support all the - * events needed to deliver the crucial information, the resulting - * image description object shall immediately deliver the - * wp_image_description_v1.failed event with the low_version cause. - * - * Otherwise the object shall immediately deliver the ready event. - */ void (*get_image_description)(struct wl_client *client, struct wl_resource *resource, uint32_t image_description); @@ -1293,25 +428,11 @@ struct wp_color_management_output_v1_interface { #define WP_COLOR_MANAGEMENT_OUTPUT_V1_IMAGE_DESCRIPTION_CHANGED 0 -/** - * @ingroup iface_wp_color_management_output_v1 - */ #define WP_COLOR_MANAGEMENT_OUTPUT_V1_IMAGE_DESCRIPTION_CHANGED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_output_v1 - */ #define WP_COLOR_MANAGEMENT_OUTPUT_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_output_v1 - */ #define WP_COLOR_MANAGEMENT_OUTPUT_V1_GET_IMAGE_DESCRIPTION_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_output_v1 - * Sends an image_description_changed event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wp_color_management_output_v1_send_image_description_changed(struct wl_resource *resource_) { @@ -1320,35 +441,15 @@ wp_color_management_output_v1_send_image_description_changed(struct wl_resource #ifndef WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_ENUM #define WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_ENUM -/** - * @ingroup iface_wp_color_management_surface_v1 - * protocol errors - */ enum wp_color_management_surface_v1_error { - /** - * unsupported rendering intent - */ WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_RENDER_INTENT = 0, - /** - * invalid image description - */ WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_IMAGE_DESCRIPTION = 1, - /** - * forbidden request on inert object - */ WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_INERT = 2, }; -#endif /* WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_ENUM */ +#endif #ifndef WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_ENUM_IS_VALID #define WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_management_surface_v1 - * Validate a wp_color_management_surface_v1 error value. - * - * @return true on success, false on error. - * @ref wp_color_management_surface_v1_error - */ static inline bool wp_color_management_surface_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1362,128 +463,34 @@ wp_color_management_surface_v1_error_is_valid(uint32_t value, uint32_t version) return false; } } -#endif /* WP_COLOR_MANAGEMENT_SURFACE_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_color_management_surface_v1 - * @struct wp_color_management_surface_v1_interface - */ struct wp_color_management_surface_v1_interface { - /** - * destroy the color management interface for a surface - * - * Destroy the wp_color_management_surface_v1 object and do the - * same as unset_image_description. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the surface image description - * - * If this protocol object is inert, the protocol error inert is - * raised. - * - * Set the image description of the underlying surface. The image - * description and rendering intent are double-buffered state, see - * wl_surface.commit. - * - * It is the client's responsibility to understand the image - * description it sets on a surface, and to provide content that - * matches that image description. Compositors might convert images - * to match their own or any other image descriptions. - * - * Image descriptions which are not ready (see - * wp_image_description_v1) are forbidden in this request, and in - * such case the protocol error image_description is raised. - * - * All image descriptions which are ready (see - * wp_image_description_v1) are allowed and must always be accepted - * by the compositor. - * - * A rendering intent provides the client's preference on how - * content colors should be mapped to each output. The - * render_intent value must be one advertised by the compositor - * with wp_color_manager_v1.render_intent event, otherwise the - * protocol error render_intent is raised. - * - * When an image description is set on a surface, the Transfer - * Characteristics of the image description defines the valid range - * of the nominal (real-valued) color channel values. The - * processing of out-of-range color channel values is undefined, - * but compositors are recommended to clamp the values to the valid - * range when possible. - * - * By default, a surface does not have an associated image - * description nor a rendering intent. The handling of color on - * such surfaces is compositor implementation defined. Compositors - * should handle such surfaces as sRGB, but may handle them - * differently if they have specific requirements. - * - * Setting the image description has copy semantics; after this - * request, the image description can be immediately destroyed - * without affecting the pending state of the surface. - * @param render_intent rendering intent - */ void (*set_image_description)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *image_description, uint32_t render_intent); - /** - * remove the surface image description - * - * If this protocol object is inert, the protocol error inert is - * raised. - * - * This request removes any image description from the surface. See - * set_image_description for how a compositor handles a surface - * without an image description. This is double-buffered state, see - * wl_surface.commit. - */ void (*unset_image_description)(struct wl_client *client, struct wl_resource *resource); }; -/** - * @ingroup iface_wp_color_management_surface_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_surface_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_V1_SET_IMAGE_DESCRIPTION_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_surface_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_V1_UNSET_IMAGE_DESCRIPTION_SINCE_VERSION 1 #ifndef WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_ENUM #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_ENUM -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - * protocol errors - */ enum wp_color_management_surface_feedback_v1_error { - /** - * forbidden request on inert object - */ WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_INERT = 0, - /** - * attempted to use an unsupported feature - */ WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_UNSUPPORTED_FEATURE = 1, }; -#endif /* WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_ENUM */ +#endif #ifndef WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_ENUM_IS_VALID #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - * Validate a wp_color_management_surface_feedback_v1 error value. - * - * @return true on success, false on error. - * @ref wp_color_management_surface_feedback_v1_error - */ static inline bool wp_color_management_surface_feedback_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1495,76 +502,14 @@ wp_color_management_surface_feedback_v1_error_is_valid(uint32_t value, uint32_t return false; } } -#endif /* WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - * @struct wp_color_management_surface_feedback_v1_interface - */ struct wp_color_management_surface_feedback_v1_interface { - /** - * destroy the color management interface for a surface - * - * Destroy the wp_color_management_surface_feedback_v1 object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * get the preferred image description - * - * If this protocol object is inert, the protocol error inert is - * raised. - * - * The preferred image description represents the compositor's - * preferred color encoding for this wl_surface at the current - * time. There might be performance and power advantages, as well - * as improved color reproduction, if the image description of a - * content update matches the preferred image description. - * - * This creates a new wp_image_description_v1 object for the - * currently preferred image description for the wl_surface. The - * client should stop using and destroy the image descriptions - * created by earlier invocations of this request for the - * associated wl_surface. This request is usually sent as a - * reaction to the preferred_changed event or when creating a - * wp_color_management_surface_feedback_v1 object if the client is - * capable of adapting to image descriptions. - * - * The created wp_image_description_v1 object preserves the - * preferred image description of the wl_surface from the time the - * object was created. - * - * The resulting image description object allows get_information - * request. - * - * If the image description is parametric, the client should set it - * on its wl_surface only if the image description is an exact - * match with the client content. Particularly if everything else - * matches, but the target color volume is greater than what the - * client needs, the client should create its own parameric image - * description with its exact parameters. - * - * If the interface version is inadequate for the preferred image - * description, meaning that the client does not support all the - * events needed to deliver the crucial information, the resulting - * image description object shall immediately deliver the - * wp_image_description_v1.failed event with the low_version cause, - * otherwise the object shall immediately deliver the ready event. - */ void (*get_preferred)(struct wl_client *client, struct wl_resource *resource, uint32_t image_description); - /** - * get the preferred image description - * - * The same description as for get_preferred applies, except the - * returned image description is guaranteed to be parametric. This - * is meant for clients that can only deal with parametric image - * descriptions. - * - * If the compositor doesn't support parametric image descriptions, - * the unsupported_feature error is emitted. - */ void (*get_preferred_parametric)(struct wl_client *client, struct wl_resource *resource, uint32_t image_description); @@ -1572,30 +517,12 @@ struct wp_color_management_surface_feedback_v1_interface { #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_PREFERRED_CHANGED 0 -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_PREFERRED_CHANGED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_GET_PREFERRED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - */ #define WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_GET_PREFERRED_PARAMETRIC_SINCE_VERSION 1 -/** - * @ingroup iface_wp_color_management_surface_feedback_v1 - * Sends an preferred_changed event to the client owning the resource. - * @param resource_ The client's resource - * @param identity image description id number - */ static inline void wp_color_management_surface_feedback_v1_send_preferred_changed(struct wl_resource *resource_, uint32_t identity) { @@ -1604,43 +531,17 @@ wp_color_management_surface_feedback_v1_send_preferred_changed(struct wl_resourc #ifndef WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ENUM #define WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ENUM -/** - * @ingroup iface_wp_image_description_creator_icc_v1 - * protocol errors - */ enum wp_image_description_creator_icc_v1_error { - /** - * incomplete parameter set - */ WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_INCOMPLETE_SET = 0, - /** - * property already set - */ WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ALREADY_SET = 1, - /** - * fd not seekable and readable - */ WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_BAD_FD = 2, - /** - * no or too much data - */ WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_BAD_SIZE = 3, - /** - * offset + length exceeds file size - */ WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_OUT_OF_FILE = 4, }; -#endif /* WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ENUM */ +#endif #ifndef WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ENUM_IS_VALID #define WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_image_description_creator_icc_v1 - * Validate a wp_image_description_creator_icc_v1 error value. - * - * @return true on success, false on error. - * @ref wp_image_description_creator_icc_v1_error - */ static inline bool wp_image_description_creator_icc_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1658,91 +559,12 @@ wp_image_description_creator_icc_v1_error_is_valid(uint32_t value, uint32_t vers return false; } } -#endif /* WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_image_description_creator_icc_v1 - * @struct wp_image_description_creator_icc_v1_interface - */ struct wp_image_description_creator_icc_v1_interface { - /** - * Create the image description object from ICC data - * - * Create an image description object based on the ICC - * information previously set on this object. A compositor must - * parse the ICC data in some undefined but finite amount of time. - * - * The completeness of the parameter set is verified. If the set is - * not complete, the protocol error incomplete_set is raised. For - * the definition of a complete set, see the description of this - * interface. - * - * If the particular combination of the information is not - * supported by the compositor, the resulting image description - * object shall immediately deliver the - * wp_image_description_v1.failed event with the 'unsupported' - * cause. If a valid image description was created from the - * information, the wp_image_description_v1.ready event will - * eventually be sent instead. - * - * This request destroys the wp_image_description_creator_icc_v1 - * object. - * - * The resulting image description object does not allow - * get_information request. - */ void (*create)(struct wl_client *client, struct wl_resource *resource, uint32_t image_description); - /** - * set the ICC profile file - * - * Sets the ICC profile file to be used as the basis of the image - * description. - * - * The data shall be found through the given fd at the given - * offset, having the given length. The fd must be seekable and - * readable. Violating these requirements raises the bad_fd - * protocol error. - * - * If reading the data fails due to an error independent of the - * client, the compositor shall send the - * wp_image_description_v1.failed event on the created - * wp_image_description_v1 with the 'operating_system' cause. - * - * The maximum size of the ICC profile is 32 MB. If length is - * greater than that or zero, the protocol error bad_size is - * raised. If offset + length exceeds the file size, the protocol - * error out_of_file is raised. - * - * A compositor may read the file at any time starting from this - * request and only until whichever happens first: - If create - * request was issued, the wp_image_description_v1 object delivers - * either failed or ready event; or - if create request was not - * issued, this wp_image_description_creator_icc_v1 object is - * destroyed. - * - * A compositor shall not modify the contents of the file, and the - * fd may be sealed for writes and size changes. The client must - * ensure to its best ability that the data does not change while - * the compositor is reading it. - * - * The data must represent a valid ICC profile. The ICC profile - * version must be 2 or 4, it must be a 3 channel profile and the - * class must be Display or ColorSpace. Violating these - * requirements will not result in a protocol error, but will - * eventually send the wp_image_description_v1.failed event on the - * created wp_image_description_v1 with the 'unsupported' cause. - * - * See the International Color Consortium specification ICC.1:2022 - * for more details about ICC profiles. - * - * If ICC file has already been set on this object, the protocol - * error already_set is raised. - * @param icc_profile ICC profile - * @param offset byte offset in fd to start of ICC data - * @param length length of ICC data in bytes - */ void (*set_icc_file)(struct wl_client *client, struct wl_resource *resource, int32_t icc_profile, @@ -1751,58 +573,23 @@ struct wp_image_description_creator_icc_v1_interface { }; -/** - * @ingroup iface_wp_image_description_creator_icc_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_CREATE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_icc_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_ICC_V1_SET_ICC_FILE_SINCE_VERSION 1 #ifndef WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ENUM #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ENUM -/** - * @ingroup iface_wp_image_description_creator_params_v1 - * protocol errors - */ enum wp_image_description_creator_params_v1_error { - /** - * incomplete parameter set - */ WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_INCOMPLETE_SET = 0, - /** - * property already set - */ WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ALREADY_SET = 1, - /** - * request not supported - */ WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_UNSUPPORTED_FEATURE = 2, - /** - * invalid transfer characteristic - */ WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_INVALID_TF = 3, - /** - * invalid primaries named - */ WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_INVALID_PRIMARIES_NAMED = 4, - /** - * invalid luminance value or range - */ WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_INVALID_LUMINANCE = 5, }; -#endif /* WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ENUM */ +#endif #ifndef WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ENUM_IS_VALID #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_image_description_creator_params_v1 - * Validate a wp_image_description_creator_params_v1 error value. - * - * @return true on success, false on error. - * @ref wp_image_description_creator_params_v1_error - */ static inline bool wp_image_description_creator_params_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1822,144 +609,21 @@ wp_image_description_creator_params_v1_error_is_valid(uint32_t value, uint32_t v return false; } } -#endif /* WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_image_description_creator_params_v1 - * @struct wp_image_description_creator_params_v1_interface - */ struct wp_image_description_creator_params_v1_interface { - /** - * Create the image description object using params - * - * Create an image description object based on the parameters - * previously set on this object. - * - * The completeness of the parameter set is verified. If the set is - * not complete, the protocol error incomplete_set is raised. For - * the definition of a complete set, see the description of this - * interface. - * - * The protocol error invalid_luminance is raised if any of the - * following requirements is not met: - When max_cll is set, it - * must be greater than min L and less or equal to max L of the - * mastering luminance range. - When max_fall is set, it must be - * greater than min L and less or equal to max L of the mastering - * luminance range. - When both max_cll and max_fall are set, - * max_fall must be less or equal to max_cll. - * - * If the particular combination of the parameter set is not - * supported by the compositor, the resulting image description - * object shall immediately deliver the - * wp_image_description_v1.failed event with the 'unsupported' - * cause. If a valid image description was created from the - * parameter set, the wp_image_description_v1.ready event will - * eventually be sent instead. - * - * This request destroys the wp_image_description_creator_params_v1 - * object. - * - * The resulting image description object does not allow - * get_information request. - */ void (*create)(struct wl_client *client, struct wl_resource *resource, uint32_t image_description); - /** - * named transfer characteristic - * - * Sets the transfer characteristic using explicitly enumerated - * named functions. - * - * When the resulting image description is attached to an image, - * the content should be encoded and decoded according to the - * industry standard practices for the transfer characteristic. - * - * Only names advertised with wp_color_manager_v1 event - * supported_tf_named are allowed. Other values shall raise the - * protocol error invalid_tf. - * - * If transfer characteristic has already been set on this object, - * the protocol error already_set is raised. - * @param tf named transfer function - */ void (*set_tf_named)(struct wl_client *client, struct wl_resource *resource, uint32_t tf); - /** - * transfer characteristic as a power curve - * - * Sets the color component transfer characteristic to a power - * curve with the given exponent. Negative values are handled by - * mirroring the positive half of the curve through the origin. The - * valid domain and range of the curve are all finite real numbers. - * This curve represents the conversion from electrical to optical - * color channel values. - * - * When the resulting image description is attached to an image, - * the content should be encoded with the inverse of the power - * curve. - * - * The curve exponent shall be multiplied by 10000 to get the - * argument eexp value to carry the precision of 4 decimals. - * - * The curve exponent must be at least 1.0 and at most 10.0. - * Otherwise the protocol error invalid_tf is raised. - * - * If transfer characteristic has already been set on this object, - * the protocol error already_set is raised. - * - * This request can be used when the compositor advertises - * wp_color_manager_v1.feature.set_tf_power. Otherwise this request - * raises the protocol error unsupported_feature. - * @param eexp the exponent * 10000 - */ void (*set_tf_power)(struct wl_client *client, struct wl_resource *resource, uint32_t eexp); - /** - * named primaries - * - * Sets the color primaries and white point using explicitly - * named sets. This describes the primary color volume which is the - * basis for color value encoding. - * - * Only names advertised with wp_color_manager_v1 event - * supported_primaries_named are allowed. Other values shall raise - * the protocol error invalid_primaries_named. - * - * If primaries have already been set on this object, the protocol - * error already_set is raised. - * @param primaries named primaries - */ void (*set_primaries_named)(struct wl_client *client, struct wl_resource *resource, uint32_t primaries); - /** - * primaries as chromaticity coordinates - * - * Sets the color primaries and white point using CIE 1931 xy - * chromaticity coordinates. This describes the primary color - * volume which is the basis for color value encoding. - * - * Each coordinate value is multiplied by 1 million to get the - * argument value to carry precision of 6 decimals. - * - * If primaries have already been set on this object, the protocol - * error already_set is raised. - * - * This request can be used if the compositor advertises - * wp_color_manager_v1.feature.set_primaries. Otherwise this - * request raises the protocol error unsupported_feature. - * @param r_x Red x * 1M - * @param r_y Red y * 1M - * @param g_x Green x * 1M - * @param g_y Green y * 1M - * @param b_x Blue x * 1M - * @param b_y Blue y * 1M - * @param w_x White x * 1M - * @param w_y White y * 1M - */ void (*set_primaries)(struct wl_client *client, struct wl_resource *resource, int32_t r_x, @@ -1970,132 +634,11 @@ struct wp_image_description_creator_params_v1_interface { int32_t b_y, int32_t w_x, int32_t w_y); - /** - * primary color volume luminance range and reference white - * - * Sets the primary color volume luminance range and the - * reference white luminance level. These values include the - * minimum display emission and ambient flare luminances, assumed - * to be optically additive and have the chromaticity of the - * primary color volume white point. - * - * The default luminances from - * https://www.color.org/chardata/rgb/srgb.xalter are - primary - * color volume minimum: 0.2 cd/m² - primary color volume maximum: - * 80 cd/m² - reference white: 80 cd/m² - * - * Setting a named transfer characteristic can imply other default - * luminances. - * - * The default luminances get overwritten when this request is - * used. With transfer_function.st2084_pq the given 'max_lum' value - * is ignored, and 'max_lum' is taken as 'min_lum' + 10000 cd/m². - * - * 'min_lum' and 'max_lum' specify the minimum and maximum - * luminances of the primary color volume as reproduced by the - * targeted display. - * - * 'reference_lum' specifies the luminance of the reference white - * as reproduced by the targeted display, and reflects the targeted - * viewing environment. - * - * Compositors should make sure that all content is anchored, - * meaning that an input signal level of 'reference_lum' on one - * image description and another input signal level of - * 'reference_lum' on another image description should produce the - * same output level, even though the 'reference_lum' on both image - * representations can be different. - * - * 'reference_lum' may be higher than 'max_lum'. In that case - * reaching the reference white output level in image content - * requires the 'extended_target_volume' feature support. - * - * If 'max_lum' or 'reference_lum' are less than or equal to - * 'min_lum', the protocol error invalid_luminance is raised. - * - * The minimum luminance is multiplied by 10000 to get the argument - * 'min_lum' value and carries precision of 4 decimals. The maximum - * luminance and reference white luminance values are unscaled. - * - * If the primary color volume luminance range and the reference - * white luminance level have already been set on this object, the - * protocol error already_set is raised. - * - * This request can be used if the compositor advertises - * wp_color_manager_v1.feature.set_luminances. Otherwise this - * request raises the protocol error unsupported_feature. - * @param min_lum minimum luminance (cd/m²) * 10000 - * @param max_lum maximum luminance (cd/m²) - * @param reference_lum reference white luminance (cd/m²) - */ void (*set_luminances)(struct wl_client *client, struct wl_resource *resource, uint32_t min_lum, uint32_t max_lum, uint32_t reference_lum); - /** - * mastering display primaries as chromaticity coordinates - * - * Provides the color primaries and white point of the mastering - * display using CIE 1931 xy chromaticity coordinates. This is - * compatible with the SMPTE ST 2086 definition of HDR static - * metadata. - * - * The mastering display primaries and mastering display luminances - * define the target color volume. - * - * If mastering display primaries are not explicitly set, the - * target color volume is assumed to have the same primaries as the - * primary color volume. - * - * The target color volume is defined by all tristimulus values - * between 0.0 and 1.0 (inclusive) of the color space defined by - * the given mastering display primaries and white point. The - * colorimetry is identical between the container color space and - * the mastering display color space, including that no chromatic - * adaptation is applied even if the white points differ. - * - * The target color volume can exceed the primary color volume to - * allow for a greater color volume with an existing color space - * definition (for example scRGB). It can be smaller than the - * primary color volume to minimize gamut and tone mapping - * distances for big color spaces (HDR metadata). - * - * To make use of the entire target color volume a suitable pixel - * format has to be chosen (e.g. floating point to exceed the - * primary color volume, or abusing limited quantization range as - * with xvYCC). - * - * Each coordinate value is multiplied by 1 million to get the - * argument value to carry precision of 6 decimals. - * - * If mastering display primaries have already been set on this - * object, the protocol error already_set is raised. - * - * This request can be used if the compositor advertises - * wp_color_manager_v1.feature.set_mastering_display_primaries. - * Otherwise this request raises the protocol error - * unsupported_feature. The advertisement implies support only for - * target color volumes fully contained within the primary color - * volume. - * - * If a compositor additionally supports target color volume - * exceeding the primary color volume, it must advertise - * wp_color_manager_v1.feature.extended_target_volume. If a client - * uses target color volume exceeding the primary color volume and - * the compositor does not support it, the result is implementation - * defined. Compositors are recommended to detect this case and - * fail the image description gracefully, but it may as well result - * in color artifacts. - * @param r_x Red x * 1M - * @param r_y Red y * 1M - * @param g_x Green x * 1M - * @param g_y Green y * 1M - * @param b_x Blue x * 1M - * @param b_y Blue y * 1M - * @param w_x White x * 1M - * @param w_y White y * 1M - */ void (*set_mastering_display_primaries)(struct wl_client *client, struct wl_resource *resource, int32_t r_x, @@ -2106,148 +649,40 @@ struct wp_image_description_creator_params_v1_interface { int32_t b_y, int32_t w_x, int32_t w_y); - /** - * display mastering luminance range - * - * Sets the luminance range that was used during the content - * mastering process as the minimum and maximum absolute luminance - * L. These values include the minimum display emission and ambient - * flare luminances, assumed to be optically additive and have the - * chromaticity of the primary color volume white point. This - * should be compatible with the SMPTE ST 2086 definition of HDR - * static metadata. - * - * The mastering display primaries and mastering display luminances - * define the target color volume. - * - * If mastering luminances are not explicitly set, the target color - * volume is assumed to have the same min and max luminances as the - * primary color volume. - * - * If max L is less than or equal to min L, the protocol error - * invalid_luminance is raised. - * - * Min L value is multiplied by 10000 to get the argument min_lum - * value and carry precision of 4 decimals. Max L value is unscaled - * for max_lum. - * - * This request can be used if the compositor advertises - * wp_color_manager_v1.feature.set_mastering_display_primaries. - * Otherwise this request raises the protocol error - * unsupported_feature. The advertisement implies support only for - * target color volumes fully contained within the primary color - * volume. - * - * If a compositor additionally supports target color volume - * exceeding the primary color volume, it must advertise - * wp_color_manager_v1.feature.extended_target_volume. If a client - * uses target color volume exceeding the primary color volume and - * the compositor does not support it, the result is implementation - * defined. Compositors are recommended to detect this case and - * fail the image description gracefully, but it may as well result - * in color artifacts. - * @param min_lum min L (cd/m²) * 10000 - * @param max_lum max L (cd/m²) - */ void (*set_mastering_luminance)(struct wl_client *client, struct wl_resource *resource, uint32_t min_lum, uint32_t max_lum); - /** - * maximum content light level - * - * Sets the maximum content light level (max_cll) as defined by - * CTA-861-H. - * - * max_cll is undefined by default. - * @param max_cll Maximum content light level (cd/m²) - */ void (*set_max_cll)(struct wl_client *client, struct wl_resource *resource, uint32_t max_cll); - /** - * maximum frame-average light level - * - * Sets the maximum frame-average light level (max_fall) as - * defined by CTA-861-H. - * - * max_fall is undefined by default. - * @param max_fall Maximum frame-average light level (cd/m²) - */ void (*set_max_fall)(struct wl_client *client, struct wl_resource *resource, uint32_t max_fall); }; -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_CREATE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_TF_NAMED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_TF_POWER_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_PRIMARIES_NAMED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_PRIMARIES_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_LUMINANCES_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_MASTERING_DISPLAY_PRIMARIES_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_MASTERING_LUMINANCE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_MAX_CLL_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_creator_params_v1 - */ #define WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_SET_MAX_FALL_SINCE_VERSION 1 #ifndef WP_IMAGE_DESCRIPTION_V1_ERROR_ENUM #define WP_IMAGE_DESCRIPTION_V1_ERROR_ENUM -/** - * @ingroup iface_wp_image_description_v1 - * protocol errors - */ enum wp_image_description_v1_error { - /** - * attempted to use an object which is not ready - */ WP_IMAGE_DESCRIPTION_V1_ERROR_NOT_READY = 0, - /** - * get_information not allowed - */ WP_IMAGE_DESCRIPTION_V1_ERROR_NO_INFORMATION = 1, }; -#endif /* WP_IMAGE_DESCRIPTION_V1_ERROR_ENUM */ +#endif #ifndef WP_IMAGE_DESCRIPTION_V1_ERROR_ENUM_IS_VALID #define WP_IMAGE_DESCRIPTION_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_image_description_v1 - * Validate a wp_image_description_v1 error value. - * - * @return true on success, false on error. - * @ref wp_image_description_v1_error - */ static inline bool wp_image_description_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2259,43 +694,20 @@ wp_image_description_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_IMAGE_DESCRIPTION_V1_ERROR_ENUM_IS_VALID */ +#endif #ifndef WP_IMAGE_DESCRIPTION_V1_CAUSE_ENUM #define WP_IMAGE_DESCRIPTION_V1_CAUSE_ENUM -/** - * @ingroup iface_wp_image_description_v1 - * generic reason for failure - */ enum wp_image_description_v1_cause { - /** - * interface version too low - */ WP_IMAGE_DESCRIPTION_V1_CAUSE_LOW_VERSION = 0, - /** - * unsupported image description data - */ WP_IMAGE_DESCRIPTION_V1_CAUSE_UNSUPPORTED = 1, - /** - * error independent of the client - */ WP_IMAGE_DESCRIPTION_V1_CAUSE_OPERATING_SYSTEM = 2, - /** - * the relevant output no longer exists - */ WP_IMAGE_DESCRIPTION_V1_CAUSE_NO_OUTPUT = 3, }; -#endif /* WP_IMAGE_DESCRIPTION_V1_CAUSE_ENUM */ +#endif #ifndef WP_IMAGE_DESCRIPTION_V1_CAUSE_ENUM_IS_VALID #define WP_IMAGE_DESCRIPTION_V1_CAUSE_ENUM_IS_VALID -/** - * @ingroup iface_wp_image_description_v1 - * Validate a wp_image_description_v1 cause value. - * - * @return true on success, false on error. - * @ref wp_image_description_v1_cause - */ static inline bool wp_image_description_v1_cause_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2311,37 +723,11 @@ wp_image_description_v1_cause_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_IMAGE_DESCRIPTION_V1_CAUSE_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_image_description_v1 - * @struct wp_image_description_v1_interface - */ struct wp_image_description_v1_interface { - /** - * destroy the image description - * - * Destroy this object. It is safe to destroy an object which is - * not ready. - * - * Destroying a wp_image_description_v1 object has no side-effects, - * not even if a - * wp_color_management_surface_v1.set_image_description has not yet - * been followed by a wl_surface.commit. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * get information about the image description - * - * Creates a wp_image_description_info_v1 object which delivers - * the information that makes up the image description. - * - * Not all image description protocol objects allow get_information - * request. Whether it is allowed or not is defined by the request - * that created the object. If get_information is not allowed, the - * protocol error no_information is raised. - */ void (*get_information)(struct wl_client *client, struct wl_resource *resource, uint32_t information); @@ -2350,43 +736,18 @@ struct wp_image_description_v1_interface { #define WP_IMAGE_DESCRIPTION_V1_FAILED 0 #define WP_IMAGE_DESCRIPTION_V1_READY 1 -/** - * @ingroup iface_wp_image_description_v1 - */ #define WP_IMAGE_DESCRIPTION_V1_FAILED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_v1 - */ #define WP_IMAGE_DESCRIPTION_V1_READY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_v1 - */ #define WP_IMAGE_DESCRIPTION_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_v1 - */ #define WP_IMAGE_DESCRIPTION_V1_GET_INFORMATION_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_v1 - * Sends an failed event to the client owning the resource. - * @param resource_ The client's resource - * @param cause generic reason - * @param msg ad hoc human-readable explanation - */ static inline void wp_image_description_v1_send_failed(struct wl_resource *resource_, uint32_t cause, const char *msg) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_V1_FAILED, cause, msg); } -/** - * @ingroup iface_wp_image_description_v1 - * Sends an ready event to the client owning the resource. - * @param resource_ The client's resource - * @param identity image description id number - */ static inline void wp_image_description_v1_send_ready(struct wl_resource *resource_, uint32_t identity) { @@ -2405,195 +766,79 @@ wp_image_description_v1_send_ready(struct wl_resource *resource_, uint32_t ident #define WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_MAX_CLL 9 #define WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_MAX_FALL 10 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_ICC_FILE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_PRIMARIES_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_PRIMARIES_NAMED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_TF_POWER_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_TF_NAMED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_LUMINANCES_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_PRIMARIES_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_LUMINANCE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_MAX_CLL_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - */ #define WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_MAX_FALL_SINCE_VERSION 1 -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wp_image_description_info_v1_send_done(struct wl_resource *resource_) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_DONE); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an icc_file event to the client owning the resource. - * @param resource_ The client's resource - * @param icc ICC profile file descriptor - * @param icc_size ICC profile size, in bytes - */ static inline void wp_image_description_info_v1_send_icc_file(struct wl_resource *resource_, int32_t icc, uint32_t icc_size) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_ICC_FILE, icc, icc_size); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an primaries event to the client owning the resource. - * @param resource_ The client's resource - * @param r_x Red x * 1M - * @param r_y Red y * 1M - * @param g_x Green x * 1M - * @param g_y Green y * 1M - * @param b_x Blue x * 1M - * @param b_y Blue y * 1M - * @param w_x White x * 1M - * @param w_y White y * 1M - */ static inline void wp_image_description_info_v1_send_primaries(struct wl_resource *resource_, int32_t r_x, int32_t r_y, int32_t g_x, int32_t g_y, int32_t b_x, int32_t b_y, int32_t w_x, int32_t w_y) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_PRIMARIES, r_x, r_y, g_x, g_y, b_x, b_y, w_x, w_y); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an primaries_named event to the client owning the resource. - * @param resource_ The client's resource - * @param primaries named primaries - */ static inline void wp_image_description_info_v1_send_primaries_named(struct wl_resource *resource_, uint32_t primaries) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_PRIMARIES_NAMED, primaries); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an tf_power event to the client owning the resource. - * @param resource_ The client's resource - * @param eexp the exponent * 10000 - */ static inline void wp_image_description_info_v1_send_tf_power(struct wl_resource *resource_, uint32_t eexp) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_TF_POWER, eexp); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an tf_named event to the client owning the resource. - * @param resource_ The client's resource - * @param tf named transfer function - */ static inline void wp_image_description_info_v1_send_tf_named(struct wl_resource *resource_, uint32_t tf) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_TF_NAMED, tf); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an luminances event to the client owning the resource. - * @param resource_ The client's resource - * @param min_lum minimum luminance (cd/m²) * 10000 - * @param max_lum maximum luminance (cd/m²) - * @param reference_lum reference white luminance (cd/m²) - */ static inline void wp_image_description_info_v1_send_luminances(struct wl_resource *resource_, uint32_t min_lum, uint32_t max_lum, uint32_t reference_lum) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_LUMINANCES, min_lum, max_lum, reference_lum); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an target_primaries event to the client owning the resource. - * @param resource_ The client's resource - * @param r_x Red x * 1M - * @param r_y Red y * 1M - * @param g_x Green x * 1M - * @param g_y Green y * 1M - * @param b_x Blue x * 1M - * @param b_y Blue y * 1M - * @param w_x White x * 1M - * @param w_y White y * 1M - */ static inline void wp_image_description_info_v1_send_target_primaries(struct wl_resource *resource_, int32_t r_x, int32_t r_y, int32_t g_x, int32_t g_y, int32_t b_x, int32_t b_y, int32_t w_x, int32_t w_y) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_PRIMARIES, r_x, r_y, g_x, g_y, b_x, b_y, w_x, w_y); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an target_luminance event to the client owning the resource. - * @param resource_ The client's resource - * @param min_lum min L (cd/m²) * 10000 - * @param max_lum max L (cd/m²) - */ static inline void wp_image_description_info_v1_send_target_luminance(struct wl_resource *resource_, uint32_t min_lum, uint32_t max_lum) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_LUMINANCE, min_lum, max_lum); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an target_max_cll event to the client owning the resource. - * @param resource_ The client's resource - * @param max_cll Maximum content light-level (cd/m²) - */ static inline void wp_image_description_info_v1_send_target_max_cll(struct wl_resource *resource_, uint32_t max_cll) { wl_resource_post_event(resource_, WP_IMAGE_DESCRIPTION_INFO_V1_TARGET_MAX_CLL, max_cll); } -/** - * @ingroup iface_wp_image_description_info_v1 - * Sends an target_max_fall event to the client owning the resource. - * @param resource_ The client's resource - * @param max_fall Maximum frame-average light level (cd/m²) - */ static inline void wp_image_description_info_v1_send_target_max_fall(struct wl_resource *resource_, uint32_t max_fall) { diff --git a/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-client-protocol.h b/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-client-protocol.h index e7c4665f3..e0a20b854 100644 --- a/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-client-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-client-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef LINUX_DMABUF_V1_CLIENT_PROTOCOL_H #define LINUX_DMABUF_V1_CLIENT_PROTOCOL_H @@ -11,14 +10,8 @@ extern "C" { #endif -/** - * @page page_linux_dmabuf_v1 The linux_dmabuf_v1 protocol - * @section page_ifaces_linux_dmabuf_v1 Interfaces - * - @subpage page_iface_zwp_linux_dmabuf_v1 - factory for creating dmabuf-based wl_buffers - * - @subpage page_iface_zwp_linux_buffer_params_v1 - parameters for creating a dmabuf-based wl_buffer - * - @subpage page_iface_zwp_linux_dmabuf_feedback_v1 - dmabuf feedback +/* * @section page_copyright_linux_dmabuf_v1 Copyright - *
  *
  * Copyright © 2014, 2015 Collabora, Ltd.
  *
@@ -40,8 +33,14 @@ extern "C" {
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + struct wl_buffer; struct wl_surface; struct zwp_linux_buffer_params_v1; @@ -50,331 +49,21 @@ struct zwp_linux_dmabuf_v1; #ifndef ZWP_LINUX_DMABUF_V1_INTERFACE #define ZWP_LINUX_DMABUF_V1_INTERFACE -/** - * @page page_iface_zwp_linux_dmabuf_v1 zwp_linux_dmabuf_v1 - * @section page_iface_zwp_linux_dmabuf_v1_desc Description - * - * This interface offers ways to create generic dmabuf-based wl_buffers. - * - * For more information about dmabuf, see: - * https://www.kernel.org/doc/html/next/userspace-api/dma-buf-alloc-exchange.html - * - * Clients can use the get_surface_feedback request to get dmabuf feedback - * for a particular surface. If the client wants to retrieve feedback not - * tied to a surface, they can use the get_default_feedback request. - * - * The following are required from clients: - * - * - Clients must ensure that either all data in the dma-buf is - * coherent for all subsequent read access or that coherency is - * correctly handled by the underlying kernel-side dma-buf - * implementation. - * - * - Don't make any more attachments after sending the buffer to the - * compositor. Making more attachments later increases the risk of - * the compositor not being able to use (re-import) an existing - * dmabuf-based wl_buffer. - * - * The underlying graphics stack must ensure the following: - * - * - The dmabuf file descriptors relayed to the server will stay valid - * for the whole lifetime of the wl_buffer. This means the server may - * at any time use those fds to import the dmabuf into any kernel - * sub-system that might accept it. - * - * However, when the underlying graphics stack fails to deliver the - * promise, because of e.g. a device hot-unplug which raises internal - * errors, after the wl_buffer has been successfully created the - * compositor must not raise protocol errors to the client when dmabuf - * import later fails. - * - * To create a wl_buffer from one or more dmabufs, a client creates a - * zwp_linux_buffer_params_v1 object with a zwp_linux_dmabuf_v1.create_params - * request. All planes required by the intended format are added with - * the 'add' request. Finally, a 'create' or 'create_immed' request is - * issued, which has the following outcome depending on the import success. - * - * The 'create' request, - * - on success, triggers a 'created' event which provides the final - * wl_buffer to the client. - * - on failure, triggers a 'failed' event to convey that the server - * cannot use the dmabufs received from the client. - * - * For the 'create_immed' request, - * - on success, the server immediately imports the added dmabufs to - * create a wl_buffer. No event is sent from the server in this case. - * - on failure, the server can choose to either: - * - terminate the client by raising a fatal error. - * - mark the wl_buffer as failed, and send a 'failed' event to the - * client. If the client uses a failed wl_buffer as an argument to any - * request, the behaviour is compositor implementation-defined. - * - * For all DRM formats and unless specified in another protocol extension, - * pre-multiplied alpha is used for pixel values. - * - * Unless specified otherwise in another protocol extension, implicit - * synchronization is used. In other words, compositors and clients must - * wait and signal fences implicitly passed via the DMA-BUF's reservation - * mechanism. - * @section page_iface_zwp_linux_dmabuf_v1_api API - * See @ref iface_zwp_linux_dmabuf_v1. - */ -/** - * @defgroup iface_zwp_linux_dmabuf_v1 The zwp_linux_dmabuf_v1 interface - * - * This interface offers ways to create generic dmabuf-based wl_buffers. - * - * For more information about dmabuf, see: - * https://www.kernel.org/doc/html/next/userspace-api/dma-buf-alloc-exchange.html - * - * Clients can use the get_surface_feedback request to get dmabuf feedback - * for a particular surface. If the client wants to retrieve feedback not - * tied to a surface, they can use the get_default_feedback request. - * - * The following are required from clients: - * - * - Clients must ensure that either all data in the dma-buf is - * coherent for all subsequent read access or that coherency is - * correctly handled by the underlying kernel-side dma-buf - * implementation. - * - * - Don't make any more attachments after sending the buffer to the - * compositor. Making more attachments later increases the risk of - * the compositor not being able to use (re-import) an existing - * dmabuf-based wl_buffer. - * - * The underlying graphics stack must ensure the following: - * - * - The dmabuf file descriptors relayed to the server will stay valid - * for the whole lifetime of the wl_buffer. This means the server may - * at any time use those fds to import the dmabuf into any kernel - * sub-system that might accept it. - * - * However, when the underlying graphics stack fails to deliver the - * promise, because of e.g. a device hot-unplug which raises internal - * errors, after the wl_buffer has been successfully created the - * compositor must not raise protocol errors to the client when dmabuf - * import later fails. - * - * To create a wl_buffer from one or more dmabufs, a client creates a - * zwp_linux_buffer_params_v1 object with a zwp_linux_dmabuf_v1.create_params - * request. All planes required by the intended format are added with - * the 'add' request. Finally, a 'create' or 'create_immed' request is - * issued, which has the following outcome depending on the import success. - * - * The 'create' request, - * - on success, triggers a 'created' event which provides the final - * wl_buffer to the client. - * - on failure, triggers a 'failed' event to convey that the server - * cannot use the dmabufs received from the client. - * - * For the 'create_immed' request, - * - on success, the server immediately imports the added dmabufs to - * create a wl_buffer. No event is sent from the server in this case. - * - on failure, the server can choose to either: - * - terminate the client by raising a fatal error. - * - mark the wl_buffer as failed, and send a 'failed' event to the - * client. If the client uses a failed wl_buffer as an argument to any - * request, the behaviour is compositor implementation-defined. - * - * For all DRM formats and unless specified in another protocol extension, - * pre-multiplied alpha is used for pixel values. - * - * Unless specified otherwise in another protocol extension, implicit - * synchronization is used. In other words, compositors and clients must - * wait and signal fences implicitly passed via the DMA-BUF's reservation - * mechanism. - */ extern const struct wl_interface zwp_linux_dmabuf_v1_interface; #endif #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_INTERFACE #define ZWP_LINUX_BUFFER_PARAMS_V1_INTERFACE -/** - * @page page_iface_zwp_linux_buffer_params_v1 zwp_linux_buffer_params_v1 - * @section page_iface_zwp_linux_buffer_params_v1_desc Description - * - * This temporary object is a collection of dmabufs and other - * parameters that together form a single logical buffer. The temporary - * object may eventually create one wl_buffer unless cancelled by - * destroying it before requesting 'create'. - * - * Single-planar formats only require one dmabuf, however - * multi-planar formats may require more than one dmabuf. For all - * formats, an 'add' request must be called once per plane (even if the - * underlying dmabuf fd is identical). - * - * You must use consecutive plane indices ('plane_idx' argument for 'add') - * from zero to the number of planes used by the drm_fourcc format code. - * All planes required by the format must be given exactly once, but can - * be given in any order. Each plane index can only be set once; subsequent - * calls with a plane index which has already been set will result in a - * plane_set error being generated. - * @section page_iface_zwp_linux_buffer_params_v1_api API - * See @ref iface_zwp_linux_buffer_params_v1. - */ -/** - * @defgroup iface_zwp_linux_buffer_params_v1 The zwp_linux_buffer_params_v1 interface - * - * This temporary object is a collection of dmabufs and other - * parameters that together form a single logical buffer. The temporary - * object may eventually create one wl_buffer unless cancelled by - * destroying it before requesting 'create'. - * - * Single-planar formats only require one dmabuf, however - * multi-planar formats may require more than one dmabuf. For all - * formats, an 'add' request must be called once per plane (even if the - * underlying dmabuf fd is identical). - * - * You must use consecutive plane indices ('plane_idx' argument for 'add') - * from zero to the number of planes used by the drm_fourcc format code. - * All planes required by the format must be given exactly once, but can - * be given in any order. Each plane index can only be set once; subsequent - * calls with a plane index which has already been set will result in a - * plane_set error being generated. - */ extern const struct wl_interface zwp_linux_buffer_params_v1_interface; #endif #ifndef ZWP_LINUX_DMABUF_FEEDBACK_V1_INTERFACE #define ZWP_LINUX_DMABUF_FEEDBACK_V1_INTERFACE -/** - * @page page_iface_zwp_linux_dmabuf_feedback_v1 zwp_linux_dmabuf_feedback_v1 - * @section page_iface_zwp_linux_dmabuf_feedback_v1_desc Description - * - * This object advertises dmabuf parameters feedback. This includes the - * preferred devices and the supported formats/modifiers. - * - * The parameters are sent once when this object is created and whenever they - * change. The done event is always sent once after all parameters have been - * sent. When a single parameter changes, all parameters are re-sent by the - * compositor. - * - * Compositors can re-send the parameters when the current client buffer - * allocations are sub-optimal. Compositors should not re-send the - * parameters if re-allocating the buffers would not result in a more optimal - * configuration. In particular, compositors should avoid sending the exact - * same parameters multiple times in a row. - * - * The tranche_target_device and tranche_formats events are grouped by - * tranches of preference. For each tranche, a tranche_target_device, one - * tranche_flags and one or more tranche_formats events are sent, followed - * by a tranche_done event finishing the list. The tranches are sent in - * descending order of preference. All formats and modifiers in the same - * tranche have the same preference. - * - * To send parameters, the compositor sends one main_device event (unless - * the client bound version 6 or above), tranches (each consisting of one - * tranche_target_device event, one tranche_flags event, tranche_formats - * events and then a tranche_done event), then one done event. - * - * With version 6 and above, the compositor must always advertise at least - * one tranche with the sampling flag set. - * - * The compositor sends a format_table event at least once for each dmabuf - * parameters feedback object, before any tranche events. Unless the format - * table contents change later, the compositor is free not to send another - * format_table event for the same dmabuf parameters feedback object. - * @section page_iface_zwp_linux_dmabuf_feedback_v1_api API - * See @ref iface_zwp_linux_dmabuf_feedback_v1. - */ -/** - * @defgroup iface_zwp_linux_dmabuf_feedback_v1 The zwp_linux_dmabuf_feedback_v1 interface - * - * This object advertises dmabuf parameters feedback. This includes the - * preferred devices and the supported formats/modifiers. - * - * The parameters are sent once when this object is created and whenever they - * change. The done event is always sent once after all parameters have been - * sent. When a single parameter changes, all parameters are re-sent by the - * compositor. - * - * Compositors can re-send the parameters when the current client buffer - * allocations are sub-optimal. Compositors should not re-send the - * parameters if re-allocating the buffers would not result in a more optimal - * configuration. In particular, compositors should avoid sending the exact - * same parameters multiple times in a row. - * - * The tranche_target_device and tranche_formats events are grouped by - * tranches of preference. For each tranche, a tranche_target_device, one - * tranche_flags and one or more tranche_formats events are sent, followed - * by a tranche_done event finishing the list. The tranches are sent in - * descending order of preference. All formats and modifiers in the same - * tranche have the same preference. - * - * To send parameters, the compositor sends one main_device event (unless - * the client bound version 6 or above), tranches (each consisting of one - * tranche_target_device event, one tranche_flags event, tranche_formats - * events and then a tranche_done event), then one done event. - * - * With version 6 and above, the compositor must always advertise at least - * one tranche with the sampling flag set. - * - * The compositor sends a format_table event at least once for each dmabuf - * parameters feedback object, before any tranche events. Unless the format - * table contents change later, the compositor is free not to send another - * format_table event for the same dmabuf parameters feedback object. - */ extern const struct wl_interface zwp_linux_dmabuf_feedback_v1_interface; #endif -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * @struct zwp_linux_dmabuf_v1_listener - */ struct zwp_linux_dmabuf_v1_listener { - /** - * supported buffer format - * - * This event advertises one buffer format that the server - * supports. All the supported formats are advertised once when the - * client binds to this interface. A roundtrip after binding - * guarantees that the client has received all supported formats. - * - * For the definition of the format codes, see the - * zwp_linux_buffer_params_v1::create request. - * - * Starting version 4, the format event is deprecated and must not - * be sent by compositors. Instead, use get_default_feedback or - * get_surface_feedback. - * @param format DRM_FORMAT code - * @deprecated Deprecated since version 4 - */ void (*format)(void *data, struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1, uint32_t format); - /** - * supported buffer format modifier - * - * This event advertises the formats that the server supports, - * along with the modifiers supported for each format. All the - * supported modifiers for all the supported formats are advertised - * once when the client binds to this interface. A roundtrip after - * binding guarantees that the client has received all supported - * format-modifier pairs. - * - * For legacy support, DRM_FORMAT_MOD_INVALID (that is, modifier_hi - * == 0x00ffffff and modifier_lo == 0xffffffff) is allowed in this - * event. It indicates that the server can support the format with - * an implicit modifier. When a plane has DRM_FORMAT_MOD_INVALID as - * its modifier, it is as if no explicit modifier is specified. The - * effective modifier will be derived from the dmabuf. - * - * A compositor that sends valid modifiers and - * DRM_FORMAT_MOD_INVALID for a given format supports both explicit - * modifiers and implicit modifiers. - * - * For the definition of the format and modifier codes, see the - * zwp_linux_buffer_params_v1::create and - * zwp_linux_buffer_params_v1::add requests. - * - * Starting version 4, the modifier event is deprecated and must - * not be sent by compositors. Instead, use get_default_feedback or - * get_surface_feedback. - * @param format DRM_FORMAT code - * @param modifier_hi high 32 bits of layout modifier - * @param modifier_lo low 32 bits of layout modifier - * @since 3 - * @deprecated Deprecated since version 4 - */ void (*modifier)(void *data, struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1, uint32_t format, @@ -382,9 +71,6 @@ struct zwp_linux_dmabuf_v1_listener { uint32_t modifier_lo); }; -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ static inline int zwp_linux_dmabuf_v1_add_listener(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1, const struct zwp_linux_dmabuf_v1_listener *listener, void *data) @@ -398,40 +84,20 @@ zwp_linux_dmabuf_v1_add_listener(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1 #define ZWP_LINUX_DMABUF_V1_GET_DEFAULT_FEEDBACK 2 #define ZWP_LINUX_DMABUF_V1_GET_SURFACE_FEEDBACK 3 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_FORMAT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_MODIFIER_SINCE_VERSION 3 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_CREATE_PARAMS_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_GET_DEFAULT_FEEDBACK_SINCE_VERSION 4 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_GET_SURFACE_FEEDBACK_SINCE_VERSION 4 -/** @ingroup iface_zwp_linux_dmabuf_v1 */ static inline void zwp_linux_dmabuf_v1_set_user_data(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) zwp_linux_dmabuf_v1, user_data); } -/** @ingroup iface_zwp_linux_dmabuf_v1 */ static inline void * zwp_linux_dmabuf_v1_get_user_data(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1) { @@ -444,12 +110,6 @@ zwp_linux_dmabuf_v1_get_version(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1) return wl_proxy_get_version((struct wl_proxy *) zwp_linux_dmabuf_v1); } -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * - * Objects created through this interface, especially wl_buffers, will - * remain valid. - */ static inline void zwp_linux_dmabuf_v1_destroy(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1) { @@ -457,14 +117,6 @@ zwp_linux_dmabuf_v1_destroy(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1) ZWP_LINUX_DMABUF_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_linux_dmabuf_v1), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * - * This temporary object is used to collect multiple dmabuf handles into - * a single batch to create a wl_buffer. It can only be used once and - * should be destroyed after a 'created' or 'failed' event has been - * received. - */ static inline struct zwp_linux_buffer_params_v1 * zwp_linux_dmabuf_v1_create_params(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1) { @@ -476,14 +128,6 @@ zwp_linux_dmabuf_v1_create_params(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v return (struct zwp_linux_buffer_params_v1 *) params_id; } -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * - * This request creates a new zwp_linux_dmabuf_feedback_v1 object not bound - * to a particular surface. This object will deliver feedback about dmabuf - * parameters to use if the client doesn't support per-surface feedback - * (see get_surface_feedback). - */ static inline struct zwp_linux_dmabuf_feedback_v1 * zwp_linux_dmabuf_v1_get_default_feedback(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1) { @@ -495,16 +139,6 @@ zwp_linux_dmabuf_v1_get_default_feedback(struct zwp_linux_dmabuf_v1 *zwp_linux_d return (struct zwp_linux_dmabuf_feedback_v1 *) id; } -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * - * This request creates a new zwp_linux_dmabuf_feedback_v1 object for the - * specified wl_surface. This object will deliver feedback about dmabuf - * parameters to use for buffers attached to this surface. - * - * If the surface is destroyed before the zwp_linux_dmabuf_feedback_v1 object, - * the feedback object becomes inert. - */ static inline struct zwp_linux_dmabuf_feedback_v1 * zwp_linux_dmabuf_v1_get_surface_feedback(struct zwp_linux_dmabuf_v1 *zwp_linux_dmabuf_v1, struct wl_surface *surface) { @@ -519,99 +153,35 @@ zwp_linux_dmabuf_v1_get_surface_feedback(struct zwp_linux_dmabuf_v1 *zwp_linux_d #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM #define ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM enum zwp_linux_buffer_params_v1_error { - /** - * the zwp_linux_buffer_params_v1 object has already been used to create a wl_buffer - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ALREADY_USED = 0, - /** - * plane index out of bounds - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_PLANE_IDX = 1, - /** - * the plane index was already set - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_PLANE_SET = 2, - /** - * missing or too many planes to create a buffer - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INCOMPLETE = 3, - /** - * format not supported - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_FORMAT = 4, - /** - * invalid width or height - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_DIMENSIONS = 5, - /** - * offset + stride * height goes out of dmabuf bounds - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_OUT_OF_BOUNDS = 6, - /** - * invalid wl_buffer resulted from importing dmabufs via the create_immed request on given buffer_params - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_WL_BUFFER = 7, - /** - * an array with mismatching size for a dev_t was used - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_DEV_T_SIZE = 8, }; -#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM */ +#endif #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM #define ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM enum zwp_linux_buffer_params_v1_flags { - /** - * contents are y-inverted - */ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_Y_INVERT = 1, - /** - * content is interlaced - */ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_INTERLACED = 2, - /** - * bottom field first - */ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_BOTTOM_FIRST = 4, }; -#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM */ +#endif -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * @struct zwp_linux_buffer_params_v1_listener - */ struct zwp_linux_buffer_params_v1_listener { - /** - * buffer creation succeeded - * - * This event indicates that the attempted buffer creation was - * successful. It provides the new wl_buffer referencing the - * dmabuf(s). - * - * Upon receiving this event, the client should destroy the - * zwp_linux_buffer_params_v1 object. - * @param buffer id for the the newly created wl_buffer - */ void (*created)(void *data, struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, struct wl_buffer *buffer); - /** - * buffer creation failed - * - * This event indicates that the attempted buffer creation has - * failed. It usually means that one of the dmabuf constraints has - * not been fulfilled. - * - * Upon receiving this event, the client should destroy the - * zwp_linux_buffer_params_v1 object. - */ void (*failed)(void *data, struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1); }; -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ static inline int zwp_linux_buffer_params_v1_add_listener(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, const struct zwp_linux_buffer_params_v1_listener *listener, void *data) @@ -626,44 +196,21 @@ zwp_linux_buffer_params_v1_add_listener(struct zwp_linux_buffer_params_v1 *zwp_l #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED 3 #define ZWP_LINUX_BUFFER_PARAMS_V1_SET_SAMPLING_DEVICE 4 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_FAILED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_ADD_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_SET_SAMPLING_DEVICE_SINCE_VERSION 6 -/** @ingroup iface_zwp_linux_buffer_params_v1 */ static inline void zwp_linux_buffer_params_v1_set_user_data(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) zwp_linux_buffer_params_v1, user_data); } -/** @ingroup iface_zwp_linux_buffer_params_v1 */ static inline void * zwp_linux_buffer_params_v1_get_user_data(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1) { @@ -676,12 +223,6 @@ zwp_linux_buffer_params_v1_get_version(struct zwp_linux_buffer_params_v1 *zwp_li return wl_proxy_get_version((struct wl_proxy *) zwp_linux_buffer_params_v1); } -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * - * Cleans up the temporary data sent to the server for dmabuf-based - * wl_buffer creation. - */ static inline void zwp_linux_buffer_params_v1_destroy(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1) { @@ -689,29 +230,6 @@ zwp_linux_buffer_params_v1_destroy(struct zwp_linux_buffer_params_v1 *zwp_linux_ ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_linux_buffer_params_v1), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * - * This request adds one dmabuf to the set in this - * zwp_linux_buffer_params_v1. - * - * The 64-bit unsigned value combined from modifier_hi and modifier_lo - * is the dmabuf layout modifier. DRM AddFB2 ioctl calls this the - * fb modifier, which is defined in drm_mode.h of Linux UAPI. - * This is an opaque token. Drivers use this token to express tiling, - * compression, etc. driver-specific modifications to the base format - * defined by the DRM fourcc code. - * - * Starting from version 4, the invalid_format protocol error is sent if - * the format + modifier pair was not advertised as supported. - * - * Starting from version 5, the invalid_format protocol error is sent if - * all planes don't use the same modifier. - * - * This request raises the PLANE_IDX error if plane_idx is too large. - * The error PLANE_SET is raised if attempting to set a plane that - * was already set. - */ static inline void zwp_linux_buffer_params_v1_add(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, int32_t fd, uint32_t plane_idx, uint32_t offset, uint32_t stride, uint32_t modifier_hi, uint32_t modifier_lo) { @@ -719,69 +237,6 @@ zwp_linux_buffer_params_v1_add(struct zwp_linux_buffer_params_v1 *zwp_linux_buff ZWP_LINUX_BUFFER_PARAMS_V1_ADD, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_linux_buffer_params_v1), 0, fd, plane_idx, offset, stride, modifier_hi, modifier_lo); } -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * - * This asks for creation of a wl_buffer from the added dmabuf - * buffers. The wl_buffer is not created immediately but returned via - * the 'created' event if the dmabuf sharing succeeds. The sharing - * may fail at runtime for reasons a client cannot predict, in - * which case the 'failed' event is triggered. - * - * The 'format' argument is a DRM_FORMAT code, as defined by the - * libdrm's drm_fourcc.h. The Linux kernel's DRM sub-system is the - * authoritative source on how the format codes should work. - * - * The 'flags' is a bitfield of the flags defined in enum "flags". - * 'y_invert' means that the image needs to be y-flipped. - * - * Flag 'interlaced' means that the frame in the buffer is not - * progressive as usual, but interlaced. An interlaced buffer as - * supported here must always contain both top and bottom fields. - * The top field always begins on the first pixel row. The temporal - * ordering between the two fields is top field first, unless - * 'bottom_first' is specified. It is undefined whether 'bottom_first' - * is ignored if 'interlaced' is not set. - * - * This protocol does not convey any information about field rate, - * duration, or timing, other than the relative ordering between the - * two fields in one buffer. A compositor may have to estimate the - * intended field rate from the incoming buffer rate. It is undefined - * whether the time of receiving wl_surface.commit with a new buffer - * attached, applying the wl_surface state, wl_surface.frame callback - * trigger, presentation, or any other point in the compositor cycle - * is used to measure the frame or field times. There is no support - * for detecting missed or late frames/fields/buffers either, and - * there is no support whatsoever for cooperating with interlaced - * compositor output. - * - * The composited image quality resulting from the use of interlaced - * buffers is explicitly undefined. A compositor may use elaborate - * hardware features or software to deinterlace and create progressive - * output frames from a sequence of interlaced input buffers, or it - * may produce substandard image quality. However, compositors that - * cannot guarantee reasonable image quality in all cases are recommended - * to just reject all interlaced buffers. - * - * Any argument errors, including non-positive width or height, - * mismatch between the number of planes and the format, bad - * format, bad offset or stride, may be indicated by fatal protocol - * errors: INCOMPLETE, INVALID_FORMAT, INVALID_DIMENSIONS, - * OUT_OF_BOUNDS. - * - * Dmabuf import errors in the server that are not obvious client - * bugs are returned via the 'failed' event as non-fatal. This - * allows attempting dmabuf sharing and falling back in the client - * if it fails. - * - * This request can be sent only once in the object's lifetime, after - * which the only legal request is destroy. This object should be - * destroyed after issuing a 'create' request. Attempting to use this - * object after issuing 'create' raises the ALREADY_USED protocol error. - * - * It is not mandatory to issue 'create'. If a client wants to - * cancel the buffer creation, it can just destroy this object. - */ static inline void zwp_linux_buffer_params_v1_create(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, int32_t width, int32_t height, uint32_t format, uint32_t flags) { @@ -789,33 +244,6 @@ zwp_linux_buffer_params_v1_create(struct zwp_linux_buffer_params_v1 *zwp_linux_b ZWP_LINUX_BUFFER_PARAMS_V1_CREATE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_linux_buffer_params_v1), 0, width, height, format, flags); } -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * - * This asks for immediate creation of a wl_buffer by importing the - * added dmabufs. - * - * In case of import success, no event is sent from the server, and the - * wl_buffer is ready to be used by the client. - * - * Upon import failure, either of the following may happen, as seen fit - * by the implementation: - * - the client is terminated with one of the following fatal protocol - * errors: - * - INCOMPLETE, INVALID_FORMAT, INVALID_DIMENSIONS, OUT_OF_BOUNDS, - * in case of argument errors such as mismatch between the number - * of planes and the format, bad format, non-positive width or - * height, or bad offset or stride. - * - INVALID_WL_BUFFER, in case the cause for failure is unknown or - * platform specific. - * - the server creates an invalid wl_buffer, marks it as failed and - * sends a 'failed' event to the client. The result of using this - * invalid wl_buffer as an argument in any request by the client is - * defined by the compositor implementation. - * - * This takes the same arguments as a 'create' request, and obeys the - * same restrictions. - */ static inline struct wl_buffer * zwp_linux_buffer_params_v1_create_immed(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, int32_t width, int32_t height, uint32_t format, uint32_t flags) { @@ -827,23 +255,6 @@ zwp_linux_buffer_params_v1_create_immed(struct zwp_linux_buffer_params_v1 *zwp_l return (struct wl_buffer *) buffer_id; } -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * - * Set the device the compositor should import the dmabufs to for sampling - * in the next create or create_immed request. - * - * To avoid race conditions when the compositor removes a device from the - * tranches, it is not a protocol error if the device hasn't been advertised - * by the compositor in a tranche with the sampling flag, but the import is - * likely to fail in that case. - * - * If the client doesn't know a suitable target device, it shouldn't set one, - * and the compositor should attempt import on all devices it supports. - * - * If the array is too small to contain a dev_t or larger than required, the - * invalid_dev_t_size error will be emitted. - */ static inline void zwp_linux_buffer_params_v1_set_sampling_device(struct zwp_linux_buffer_params_v1 *zwp_linux_buffer_params_v1, struct wl_array *device) { @@ -854,221 +265,36 @@ zwp_linux_buffer_params_v1_set_sampling_device(struct zwp_linux_buffer_params_v1 #ifndef ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM enum zwp_linux_dmabuf_feedback_v1_tranche_flags { - /** - * direct scan-out tranche - * - * The scanout flag is a hint that direct scan-out may be - * attempted by the compositor on the target device if the client - * appropriately allocates a buffer. How to allocate a buffer that - * can be scanned out on the target device is - * implementation-defined. - * @since 4 - */ ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SCANOUT = 1, - /** - * sampling tranche - * - * The sampling flag describes that the compositor is able to - * efficiently sample from buffers imported to the target device if - * the client appropriately allocates a buffer. How to allocate a - * buffer that can be efficiently sampled on the target device is - * implementation defined. - * @since 6 - */ ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SAMPLING = 2, }; -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SCANOUT_SINCE_VERSION 4 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SAMPLING_SINCE_VERSION 6 -#endif /* ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM */ +#endif -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * @struct zwp_linux_dmabuf_feedback_v1_listener - */ struct zwp_linux_dmabuf_feedback_v1_listener { - /** - * all feedback has been sent - * - * This event is sent after all parameters of a - * zwp_linux_dmabuf_feedback_v1 object have been sent. - * - * This allows changes to the zwp_linux_dmabuf_feedback_v1 - * parameters to be seen as atomic, even if they happen via - * multiple events. - */ void (*done)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1); - /** - * format and modifier table - * - * This event provides a file descriptor which can be - * memory-mapped to access the format and modifier table. - * - * The table contains a tightly packed array of consecutive format - * + modifier pairs. Each pair is 16 bytes wide. It contains a - * format as a 32-bit unsigned integer, followed by 4 bytes of - * unused padding, and a modifier as a 64-bit unsigned integer. The - * native endianness is used. - * - * The client must map the file descriptor in read-only private - * mode. - * - * Compositors are not allowed to mutate the table file contents - * once this event has been sent. Instead, compositors must create - * a new, separate table file and re-send feedback parameters. - * Compositors are allowed to store duplicate format + modifier - * pairs in the table. - * @param fd table file descriptor - * @param size table size, in bytes - */ void (*format_table)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, int32_t fd, uint32_t size); - /** - * preferred main device - * - * This event advertises the main device that the server prefers - * to use when direct scan-out to the target device isn't possible. - * The advertised main device may be different for each - * zwp_linux_dmabuf_feedback_v1 object, and may change over time. - * - * There is exactly one main device. The compositor must send at - * least one preference tranche with tranche_target_device equal to - * main_device. - * - * Clients need to create buffers that the main device can import - * and read from, otherwise creating the dmabuf wl_buffer will fail - * (see the zwp_linux_buffer_params_v1.create and create_immed - * requests for details). The main device will also likely be kept - * active by the compositor, so clients can use it instead of - * waking up another device for power savings. - * - * In general the device is a DRM node. The DRM node type (primary - * vs. render) is unspecified. Clients must not rely on the - * compositor sending a particular node type. Clients cannot check - * two devices for equality by comparing the dev_t value. - * - * If explicit modifiers are not supported and the client performs - * buffer allocations on a different device than the main device, - * then the client must force the buffer to have a linear layout. - * - * With version 6 and above, this event is no longer sent. Clients - * should use a device with the sampling flag in the tranches - * instead. - * @param device device dev_t value - * @deprecated Deprecated since version 6 - */ void (*main_device)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, struct wl_array *device); - /** - * a preference tranche has been sent - * - * This event splits tranche_target_device and tranche_formats - * events into preference tranches. It is sent after a set of - * tranche_target_device and tranche_formats events; it represents - * the end of a tranche. The next tranche will have a lower - * preference. - */ void (*tranche_done)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1); - /** - * target device - * - * This event advertises the target device that the server - * prefers to use for a buffer created given this tranche. The - * advertised target device may be different for each preference - * tranche, and may change over time. - * - * There is exactly one target device per tranche. - * - * The target device may be a scan-out device, for example if the - * compositor prefers to directly scan-out a buffer created given - * this tranche. The target device may be a rendering device, for - * example if the compositor prefers to texture from said buffer. - * - * The client can use this hint to allocate the buffer in a way - * that makes it accessible from the target device, ideally - * directly. The buffer must still be accessible from a device with - * the sampling flag, either through direct import or a potentially - * more expensive fallback path. If the buffer can't be directly - * imported for sampling, then clients must be prepared for the - * compositor changing the tranche priority or making wl_buffer - * creation fail (see the zwp_linux_buffer_params_v1.create and - * create_immed requests for details). - * - * If the device is a DRM node, the DRM node type (primary vs. - * render) is unspecified. Clients must not rely on the compositor - * sending a particular node type. Clients cannot check two devices - * for equality by comparing the dev_t value. - * - * This event is tied to a preference tranche, see the tranche_done - * event. - * @param device device dev_t value - */ void (*tranche_target_device)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, struct wl_array *device); - /** - * supported buffer format modifiers - * - * This event advertises the format + modifier combinations that - * the compositor supports. - * - * It carries an array of indices, each referring to a format + - * modifier pair in the last received format table (see the - * format_table event). Each index is a 16-bit unsigned integer in - * native endianness. - * - * For legacy support, DRM_FORMAT_MOD_INVALID is an allowed - * modifier. It indicates that the server can support the format - * with an implicit modifier. When a buffer has - * DRM_FORMAT_MOD_INVALID as its modifier, it is as if no explicit - * modifier is specified. The effective modifier will be derived - * from the dmabuf. - * - * A compositor that sends valid modifiers and - * DRM_FORMAT_MOD_INVALID for a given format supports both explicit - * modifiers and implicit modifiers. - * - * Compositors must not send duplicate format + modifier pairs - * within the same tranche or across two different tranches with - * the same target device and flags. - * - * This event is tied to a preference tranche, see the tranche_done - * event. - * - * For the definition of the format and modifier codes, see the - * zwp_linux_buffer_params_v1.create request. - * @param indices array of 16-bit indexes - */ void (*tranche_formats)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, struct wl_array *indices); - /** - * tranche flags - * - * This event sets tranche-specific flags. This event is tied to - * a preference tranche, see the tranche_done event. With version 6 - * and above, the compositor must set at least one flag in each - * tranche. - * @param flags tranche flags - */ void (*tranche_flags)(void *data, struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, uint32_t flags); }; -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ static inline int zwp_linux_dmabuf_feedback_v1_add_listener(struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, const struct zwp_linux_dmabuf_feedback_v1_listener *listener, void *data) @@ -1079,48 +305,22 @@ zwp_linux_dmabuf_feedback_v1_add_listener(struct zwp_linux_dmabuf_feedback_v1 *z #define ZWP_LINUX_DMABUF_FEEDBACK_V1_DESTROY 0 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_FORMAT_TABLE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_MAIN_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_TARGET_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FORMATS_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_DESTROY_SINCE_VERSION 1 -/** @ingroup iface_zwp_linux_dmabuf_feedback_v1 */ static inline void zwp_linux_dmabuf_feedback_v1_set_user_data(struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) zwp_linux_dmabuf_feedback_v1, user_data); } -/** @ingroup iface_zwp_linux_dmabuf_feedback_v1 */ static inline void * zwp_linux_dmabuf_feedback_v1_get_user_data(struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1) { @@ -1133,12 +333,6 @@ zwp_linux_dmabuf_feedback_v1_get_version(struct zwp_linux_dmabuf_feedback_v1 *zw return wl_proxy_get_version((struct wl_proxy *) zwp_linux_dmabuf_feedback_v1); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * - * Using this request a client can tell the server that it is not going to - * use the zwp_linux_dmabuf_feedback_v1 object anymore. - */ static inline void zwp_linux_dmabuf_feedback_v1_destroy(struct zwp_linux_dmabuf_feedback_v1 *zwp_linux_dmabuf_feedback_v1) { diff --git a/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-protocol.c index 7d2c38984..f1014b981 100644 --- a/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2014, 2015 Collabora, Ltd. * @@ -29,7 +27,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-server-protocol.h index 7d87d90ac..849cdbe1d 100644 --- a/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/linux-dmabuf-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef LINUX_DMABUF_V1_SERVER_PROTOCOL_H #define LINUX_DMABUF_V1_SERVER_PROTOCOL_H @@ -14,14 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_linux_dmabuf_v1 The linux_dmabuf_v1 protocol - * @section page_ifaces_linux_dmabuf_v1 Interfaces - * - @subpage page_iface_zwp_linux_dmabuf_v1 - factory for creating dmabuf-based wl_buffers - * - @subpage page_iface_zwp_linux_buffer_params_v1 - parameters for creating a dmabuf-based wl_buffer - * - @subpage page_iface_zwp_linux_dmabuf_feedback_v1 - dmabuf feedback +/* * @section page_copyright_linux_dmabuf_v1 Copyright - *
  *
  * Copyright © 2014, 2015 Collabora, Ltd.
  *
@@ -43,8 +36,14 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + struct wl_buffer; struct wl_surface; struct zwp_linux_buffer_params_v1; @@ -53,322 +52,26 @@ struct zwp_linux_dmabuf_v1; #ifndef ZWP_LINUX_DMABUF_V1_INTERFACE #define ZWP_LINUX_DMABUF_V1_INTERFACE -/** - * @page page_iface_zwp_linux_dmabuf_v1 zwp_linux_dmabuf_v1 - * @section page_iface_zwp_linux_dmabuf_v1_desc Description - * - * This interface offers ways to create generic dmabuf-based wl_buffers. - * - * For more information about dmabuf, see: - * https://www.kernel.org/doc/html/next/userspace-api/dma-buf-alloc-exchange.html - * - * Clients can use the get_surface_feedback request to get dmabuf feedback - * for a particular surface. If the client wants to retrieve feedback not - * tied to a surface, they can use the get_default_feedback request. - * - * The following are required from clients: - * - * - Clients must ensure that either all data in the dma-buf is - * coherent for all subsequent read access or that coherency is - * correctly handled by the underlying kernel-side dma-buf - * implementation. - * - * - Don't make any more attachments after sending the buffer to the - * compositor. Making more attachments later increases the risk of - * the compositor not being able to use (re-import) an existing - * dmabuf-based wl_buffer. - * - * The underlying graphics stack must ensure the following: - * - * - The dmabuf file descriptors relayed to the server will stay valid - * for the whole lifetime of the wl_buffer. This means the server may - * at any time use those fds to import the dmabuf into any kernel - * sub-system that might accept it. - * - * However, when the underlying graphics stack fails to deliver the - * promise, because of e.g. a device hot-unplug which raises internal - * errors, after the wl_buffer has been successfully created the - * compositor must not raise protocol errors to the client when dmabuf - * import later fails. - * - * To create a wl_buffer from one or more dmabufs, a client creates a - * zwp_linux_buffer_params_v1 object with a zwp_linux_dmabuf_v1.create_params - * request. All planes required by the intended format are added with - * the 'add' request. Finally, a 'create' or 'create_immed' request is - * issued, which has the following outcome depending on the import success. - * - * The 'create' request, - * - on success, triggers a 'created' event which provides the final - * wl_buffer to the client. - * - on failure, triggers a 'failed' event to convey that the server - * cannot use the dmabufs received from the client. - * - * For the 'create_immed' request, - * - on success, the server immediately imports the added dmabufs to - * create a wl_buffer. No event is sent from the server in this case. - * - on failure, the server can choose to either: - * - terminate the client by raising a fatal error. - * - mark the wl_buffer as failed, and send a 'failed' event to the - * client. If the client uses a failed wl_buffer as an argument to any - * request, the behaviour is compositor implementation-defined. - * - * For all DRM formats and unless specified in another protocol extension, - * pre-multiplied alpha is used for pixel values. - * - * Unless specified otherwise in another protocol extension, implicit - * synchronization is used. In other words, compositors and clients must - * wait and signal fences implicitly passed via the DMA-BUF's reservation - * mechanism. - * @section page_iface_zwp_linux_dmabuf_v1_api API - * See @ref iface_zwp_linux_dmabuf_v1. - */ -/** - * @defgroup iface_zwp_linux_dmabuf_v1 The zwp_linux_dmabuf_v1 interface - * - * This interface offers ways to create generic dmabuf-based wl_buffers. - * - * For more information about dmabuf, see: - * https://www.kernel.org/doc/html/next/userspace-api/dma-buf-alloc-exchange.html - * - * Clients can use the get_surface_feedback request to get dmabuf feedback - * for a particular surface. If the client wants to retrieve feedback not - * tied to a surface, they can use the get_default_feedback request. - * - * The following are required from clients: - * - * - Clients must ensure that either all data in the dma-buf is - * coherent for all subsequent read access or that coherency is - * correctly handled by the underlying kernel-side dma-buf - * implementation. - * - * - Don't make any more attachments after sending the buffer to the - * compositor. Making more attachments later increases the risk of - * the compositor not being able to use (re-import) an existing - * dmabuf-based wl_buffer. - * - * The underlying graphics stack must ensure the following: - * - * - The dmabuf file descriptors relayed to the server will stay valid - * for the whole lifetime of the wl_buffer. This means the server may - * at any time use those fds to import the dmabuf into any kernel - * sub-system that might accept it. - * - * However, when the underlying graphics stack fails to deliver the - * promise, because of e.g. a device hot-unplug which raises internal - * errors, after the wl_buffer has been successfully created the - * compositor must not raise protocol errors to the client when dmabuf - * import later fails. - * - * To create a wl_buffer from one or more dmabufs, a client creates a - * zwp_linux_buffer_params_v1 object with a zwp_linux_dmabuf_v1.create_params - * request. All planes required by the intended format are added with - * the 'add' request. Finally, a 'create' or 'create_immed' request is - * issued, which has the following outcome depending on the import success. - * - * The 'create' request, - * - on success, triggers a 'created' event which provides the final - * wl_buffer to the client. - * - on failure, triggers a 'failed' event to convey that the server - * cannot use the dmabufs received from the client. - * - * For the 'create_immed' request, - * - on success, the server immediately imports the added dmabufs to - * create a wl_buffer. No event is sent from the server in this case. - * - on failure, the server can choose to either: - * - terminate the client by raising a fatal error. - * - mark the wl_buffer as failed, and send a 'failed' event to the - * client. If the client uses a failed wl_buffer as an argument to any - * request, the behaviour is compositor implementation-defined. - * - * For all DRM formats and unless specified in another protocol extension, - * pre-multiplied alpha is used for pixel values. - * - * Unless specified otherwise in another protocol extension, implicit - * synchronization is used. In other words, compositors and clients must - * wait and signal fences implicitly passed via the DMA-BUF's reservation - * mechanism. - */ extern const struct wl_interface zwp_linux_dmabuf_v1_interface; #endif #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_INTERFACE #define ZWP_LINUX_BUFFER_PARAMS_V1_INTERFACE -/** - * @page page_iface_zwp_linux_buffer_params_v1 zwp_linux_buffer_params_v1 - * @section page_iface_zwp_linux_buffer_params_v1_desc Description - * - * This temporary object is a collection of dmabufs and other - * parameters that together form a single logical buffer. The temporary - * object may eventually create one wl_buffer unless cancelled by - * destroying it before requesting 'create'. - * - * Single-planar formats only require one dmabuf, however - * multi-planar formats may require more than one dmabuf. For all - * formats, an 'add' request must be called once per plane (even if the - * underlying dmabuf fd is identical). - * - * You must use consecutive plane indices ('plane_idx' argument for 'add') - * from zero to the number of planes used by the drm_fourcc format code. - * All planes required by the format must be given exactly once, but can - * be given in any order. Each plane index can only be set once; subsequent - * calls with a plane index which has already been set will result in a - * plane_set error being generated. - * @section page_iface_zwp_linux_buffer_params_v1_api API - * See @ref iface_zwp_linux_buffer_params_v1. - */ -/** - * @defgroup iface_zwp_linux_buffer_params_v1 The zwp_linux_buffer_params_v1 interface - * - * This temporary object is a collection of dmabufs and other - * parameters that together form a single logical buffer. The temporary - * object may eventually create one wl_buffer unless cancelled by - * destroying it before requesting 'create'. - * - * Single-planar formats only require one dmabuf, however - * multi-planar formats may require more than one dmabuf. For all - * formats, an 'add' request must be called once per plane (even if the - * underlying dmabuf fd is identical). - * - * You must use consecutive plane indices ('plane_idx' argument for 'add') - * from zero to the number of planes used by the drm_fourcc format code. - * All planes required by the format must be given exactly once, but can - * be given in any order. Each plane index can only be set once; subsequent - * calls with a plane index which has already been set will result in a - * plane_set error being generated. - */ extern const struct wl_interface zwp_linux_buffer_params_v1_interface; #endif #ifndef ZWP_LINUX_DMABUF_FEEDBACK_V1_INTERFACE #define ZWP_LINUX_DMABUF_FEEDBACK_V1_INTERFACE -/** - * @page page_iface_zwp_linux_dmabuf_feedback_v1 zwp_linux_dmabuf_feedback_v1 - * @section page_iface_zwp_linux_dmabuf_feedback_v1_desc Description - * - * This object advertises dmabuf parameters feedback. This includes the - * preferred devices and the supported formats/modifiers. - * - * The parameters are sent once when this object is created and whenever they - * change. The done event is always sent once after all parameters have been - * sent. When a single parameter changes, all parameters are re-sent by the - * compositor. - * - * Compositors can re-send the parameters when the current client buffer - * allocations are sub-optimal. Compositors should not re-send the - * parameters if re-allocating the buffers would not result in a more optimal - * configuration. In particular, compositors should avoid sending the exact - * same parameters multiple times in a row. - * - * The tranche_target_device and tranche_formats events are grouped by - * tranches of preference. For each tranche, a tranche_target_device, one - * tranche_flags and one or more tranche_formats events are sent, followed - * by a tranche_done event finishing the list. The tranches are sent in - * descending order of preference. All formats and modifiers in the same - * tranche have the same preference. - * - * To send parameters, the compositor sends one main_device event (unless - * the client bound version 6 or above), tranches (each consisting of one - * tranche_target_device event, one tranche_flags event, tranche_formats - * events and then a tranche_done event), then one done event. - * - * With version 6 and above, the compositor must always advertise at least - * one tranche with the sampling flag set. - * - * The compositor sends a format_table event at least once for each dmabuf - * parameters feedback object, before any tranche events. Unless the format - * table contents change later, the compositor is free not to send another - * format_table event for the same dmabuf parameters feedback object. - * @section page_iface_zwp_linux_dmabuf_feedback_v1_api API - * See @ref iface_zwp_linux_dmabuf_feedback_v1. - */ -/** - * @defgroup iface_zwp_linux_dmabuf_feedback_v1 The zwp_linux_dmabuf_feedback_v1 interface - * - * This object advertises dmabuf parameters feedback. This includes the - * preferred devices and the supported formats/modifiers. - * - * The parameters are sent once when this object is created and whenever they - * change. The done event is always sent once after all parameters have been - * sent. When a single parameter changes, all parameters are re-sent by the - * compositor. - * - * Compositors can re-send the parameters when the current client buffer - * allocations are sub-optimal. Compositors should not re-send the - * parameters if re-allocating the buffers would not result in a more optimal - * configuration. In particular, compositors should avoid sending the exact - * same parameters multiple times in a row. - * - * The tranche_target_device and tranche_formats events are grouped by - * tranches of preference. For each tranche, a tranche_target_device, one - * tranche_flags and one or more tranche_formats events are sent, followed - * by a tranche_done event finishing the list. The tranches are sent in - * descending order of preference. All formats and modifiers in the same - * tranche have the same preference. - * - * To send parameters, the compositor sends one main_device event (unless - * the client bound version 6 or above), tranches (each consisting of one - * tranche_target_device event, one tranche_flags event, tranche_formats - * events and then a tranche_done event), then one done event. - * - * With version 6 and above, the compositor must always advertise at least - * one tranche with the sampling flag set. - * - * The compositor sends a format_table event at least once for each dmabuf - * parameters feedback object, before any tranche events. Unless the format - * table contents change later, the compositor is free not to send another - * format_table event for the same dmabuf parameters feedback object. - */ extern const struct wl_interface zwp_linux_dmabuf_feedback_v1_interface; #endif -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * @struct zwp_linux_dmabuf_v1_interface - */ struct zwp_linux_dmabuf_v1_interface { - /** - * unbind the factory - * - * Objects created through this interface, especially wl_buffers, - * will remain valid. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * create a temporary object for buffer parameters - * - * This temporary object is used to collect multiple dmabuf - * handles into a single batch to create a wl_buffer. It can only - * be used once and should be destroyed after a 'created' or - * 'failed' event has been received. - * @param params_id id for the newly created zwp_linux_buffer_params_v1 - */ void (*create_params)(struct wl_client *client, struct wl_resource *resource, uint32_t params_id); - /** - * get default feedback - * - * This request creates a new zwp_linux_dmabuf_feedback_v1 object - * not bound to a particular surface. This object will deliver - * feedback about dmabuf parameters to use if the client doesn't - * support per-surface feedback (see get_surface_feedback). - * @since 4 - */ void (*get_default_feedback)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * get feedback for a surface - * - * This request creates a new zwp_linux_dmabuf_feedback_v1 object - * for the specified wl_surface. This object will deliver feedback - * about dmabuf parameters to use for buffers attached to this - * surface. - * - * If the surface is destroyed before the - * zwp_linux_dmabuf_feedback_v1 object, the feedback object becomes - * inert. - * @since 4 - */ void (*get_surface_feedback)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -378,52 +81,20 @@ struct zwp_linux_dmabuf_v1_interface { #define ZWP_LINUX_DMABUF_V1_FORMAT 0 #define ZWP_LINUX_DMABUF_V1_MODIFIER 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_FORMAT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_MODIFIER_SINCE_VERSION 3 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_CREATE_PARAMS_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_GET_DEFAULT_FEEDBACK_SINCE_VERSION 4 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - */ #define ZWP_LINUX_DMABUF_V1_GET_SURFACE_FEEDBACK_SINCE_VERSION 4 -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * Sends an format event to the client owning the resource. - * @param resource_ The client's resource - * @param format DRM_FORMAT code - */ static inline void zwp_linux_dmabuf_v1_send_format(struct wl_resource *resource_, uint32_t format) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_V1_FORMAT, format); } -/** - * @ingroup iface_zwp_linux_dmabuf_v1 - * Sends an modifier event to the client owning the resource. - * @param resource_ The client's resource - * @param format DRM_FORMAT code - * @param modifier_hi high 32 bits of layout modifier - * @param modifier_lo low 32 bits of layout modifier - */ static inline void zwp_linux_dmabuf_v1_send_modifier(struct wl_resource *resource_, uint32_t format, uint32_t modifier_hi, uint32_t modifier_lo) { @@ -433,54 +104,20 @@ zwp_linux_dmabuf_v1_send_modifier(struct wl_resource *resource_, uint32_t format #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM #define ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM enum zwp_linux_buffer_params_v1_error { - /** - * the zwp_linux_buffer_params_v1 object has already been used to create a wl_buffer - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ALREADY_USED = 0, - /** - * plane index out of bounds - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_PLANE_IDX = 1, - /** - * the plane index was already set - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_PLANE_SET = 2, - /** - * missing or too many planes to create a buffer - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INCOMPLETE = 3, - /** - * format not supported - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_FORMAT = 4, - /** - * invalid width or height - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_DIMENSIONS = 5, - /** - * offset + stride * height goes out of dmabuf bounds - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_OUT_OF_BOUNDS = 6, - /** - * invalid wl_buffer resulted from importing dmabufs via the create_immed request on given buffer_params - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_WL_BUFFER = 7, - /** - * an array with mismatching size for a dev_t was used - */ ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_INVALID_DEV_T_SIZE = 8, }; -#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM */ +#endif #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM_IS_VALID #define ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * Validate a zwp_linux_buffer_params_v1 error value. - * - * @return true on success, false on error. - * @ref zwp_linux_buffer_params_v1_error - */ static inline bool zwp_linux_buffer_params_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -506,35 +143,19 @@ zwp_linux_buffer_params_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_ENUM_IS_VALID */ +#endif #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM #define ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM enum zwp_linux_buffer_params_v1_flags { - /** - * contents are y-inverted - */ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_Y_INVERT = 1, - /** - * content is interlaced - */ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_INTERLACED = 2, - /** - * bottom field first - */ ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_BOTTOM_FIRST = 4, }; -#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM */ +#endif #ifndef ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM_IS_VALID #define ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM_IS_VALID -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * Validate a zwp_linux_buffer_params_v1 flags value. - * - * @return true on success, false on error. - * @ref zwp_linux_buffer_params_v1_flags - */ static inline bool zwp_linux_buffer_params_v1_flags_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -546,51 +167,11 @@ zwp_linux_buffer_params_v1_flags_is_valid(uint32_t value, uint32_t version) { valid |= ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_BOTTOM_FIRST; return (value & ~valid) == 0; } -#endif /* ZWP_LINUX_BUFFER_PARAMS_V1_FLAGS_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * @struct zwp_linux_buffer_params_v1_interface - */ struct zwp_linux_buffer_params_v1_interface { - /** - * delete this object, used or not - * - * Cleans up the temporary data sent to the server for - * dmabuf-based wl_buffer creation. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * add a dmabuf to the temporary set - * - * This request adds one dmabuf to the set in this - * zwp_linux_buffer_params_v1. - * - * The 64-bit unsigned value combined from modifier_hi and - * modifier_lo is the dmabuf layout modifier. DRM AddFB2 ioctl - * calls this the fb modifier, which is defined in drm_mode.h of - * Linux UAPI. This is an opaque token. Drivers use this token to - * express tiling, compression, etc. driver-specific modifications - * to the base format defined by the DRM fourcc code. - * - * Starting from version 4, the invalid_format protocol error is - * sent if the format + modifier pair was not advertised as - * supported. - * - * Starting from version 5, the invalid_format protocol error is - * sent if all planes don't use the same modifier. - * - * This request raises the PLANE_IDX error if plane_idx is too - * large. The error PLANE_SET is raised if attempting to set a - * plane that was already set. - * @param fd dmabuf fd - * @param plane_idx plane index - * @param offset offset in bytes - * @param stride stride in bytes - * @param modifier_hi high 32 bits of layout modifier - * @param modifier_lo low 32 bits of layout modifier - */ void (*add)(struct wl_client *client, struct wl_resource *resource, int32_t fd, @@ -599,112 +180,12 @@ struct zwp_linux_buffer_params_v1_interface { uint32_t stride, uint32_t modifier_hi, uint32_t modifier_lo); - /** - * create a wl_buffer from the given dmabufs - * - * This asks for creation of a wl_buffer from the added dmabuf - * buffers. The wl_buffer is not created immediately but returned - * via the 'created' event if the dmabuf sharing succeeds. The - * sharing may fail at runtime for reasons a client cannot predict, - * in which case the 'failed' event is triggered. - * - * The 'format' argument is a DRM_FORMAT code, as defined by the - * libdrm's drm_fourcc.h. The Linux kernel's DRM sub-system is the - * authoritative source on how the format codes should work. - * - * The 'flags' is a bitfield of the flags defined in enum "flags". - * 'y_invert' means that the image needs to be y-flipped. - * - * Flag 'interlaced' means that the frame in the buffer is not - * progressive as usual, but interlaced. An interlaced buffer as - * supported here must always contain both top and bottom fields. - * The top field always begins on the first pixel row. The temporal - * ordering between the two fields is top field first, unless - * 'bottom_first' is specified. It is undefined whether - * 'bottom_first' is ignored if 'interlaced' is not set. - * - * This protocol does not convey any information about field rate, - * duration, or timing, other than the relative ordering between - * the two fields in one buffer. A compositor may have to estimate - * the intended field rate from the incoming buffer rate. It is - * undefined whether the time of receiving wl_surface.commit with a - * new buffer attached, applying the wl_surface state, - * wl_surface.frame callback trigger, presentation, or any other - * point in the compositor cycle is used to measure the frame or - * field times. There is no support for detecting missed or late - * frames/fields/buffers either, and there is no support whatsoever - * for cooperating with interlaced compositor output. - * - * The composited image quality resulting from the use of - * interlaced buffers is explicitly undefined. A compositor may use - * elaborate hardware features or software to deinterlace and - * create progressive output frames from a sequence of interlaced - * input buffers, or it may produce substandard image quality. - * However, compositors that cannot guarantee reasonable image - * quality in all cases are recommended to just reject all - * interlaced buffers. - * - * Any argument errors, including non-positive width or height, - * mismatch between the number of planes and the format, bad - * format, bad offset or stride, may be indicated by fatal protocol - * errors: INCOMPLETE, INVALID_FORMAT, INVALID_DIMENSIONS, - * OUT_OF_BOUNDS. - * - * Dmabuf import errors in the server that are not obvious client - * bugs are returned via the 'failed' event as non-fatal. This - * allows attempting dmabuf sharing and falling back in the client - * if it fails. - * - * This request can be sent only once in the object's lifetime, - * after which the only legal request is destroy. This object - * should be destroyed after issuing a 'create' request. Attempting - * to use this object after issuing 'create' raises the - * ALREADY_USED protocol error. - * - * It is not mandatory to issue 'create'. If a client wants to - * cancel the buffer creation, it can just destroy this object. - * @param width base plane width in pixels - * @param height base plane height in pixels - * @param format DRM_FORMAT code - * @param flags see enum flags - */ void (*create)(struct wl_client *client, struct wl_resource *resource, int32_t width, int32_t height, uint32_t format, uint32_t flags); - /** - * immediately create a wl_buffer from the given dmabufs - * - * This asks for immediate creation of a wl_buffer by importing - * the added dmabufs. - * - * In case of import success, no event is sent from the server, and - * the wl_buffer is ready to be used by the client. - * - * Upon import failure, either of the following may happen, as seen - * fit by the implementation: - the client is terminated with one - * of the following fatal protocol errors: - INCOMPLETE, - * INVALID_FORMAT, INVALID_DIMENSIONS, OUT_OF_BOUNDS, in case of - * argument errors such as mismatch between the number of planes - * and the format, bad format, non-positive width or height, or bad - * offset or stride. - INVALID_WL_BUFFER, in case the cause for - * failure is unknown or platform specific. - the server creates an - * invalid wl_buffer, marks it as failed and sends a 'failed' event - * to the client. The result of using this invalid wl_buffer as an - * argument in any request by the client is defined by the - * compositor implementation. - * - * This takes the same arguments as a 'create' request, and obeys - * the same restrictions. - * @param buffer_id id for the newly created wl_buffer - * @param width base plane width in pixels - * @param height base plane height in pixels - * @param format DRM_FORMAT code - * @param flags see enum flags - * @since 2 - */ void (*create_immed)(struct wl_client *client, struct wl_resource *resource, uint32_t buffer_id, @@ -712,26 +193,6 @@ struct zwp_linux_buffer_params_v1_interface { int32_t height, uint32_t format, uint32_t flags); - /** - * set the target device of the wl_buffer - * - * Set the device the compositor should import the dmabufs to for - * sampling in the next create or create_immed request. - * - * To avoid race conditions when the compositor removes a device - * from the tranches, it is not a protocol error if the device - * hasn't been advertised by the compositor in a tranche with the - * sampling flag, but the import is likely to fail in that case. - * - * If the client doesn't know a suitable target device, it - * shouldn't set one, and the compositor should attempt import on - * all devices it supports. - * - * If the array is too small to contain a dev_t or larger than - * required, the invalid_dev_t_size error will be emitted. - * @param device device dev_t value - * @since 6 - */ void (*set_sampling_device)(struct wl_client *client, struct wl_resource *resource, struct wl_array *device); @@ -740,53 +201,21 @@ struct zwp_linux_buffer_params_v1_interface { #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATED 0 #define ZWP_LINUX_BUFFER_PARAMS_V1_FAILED 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_FAILED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_ADD_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - */ #define ZWP_LINUX_BUFFER_PARAMS_V1_SET_SAMPLING_DEVICE_SINCE_VERSION 6 -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * Sends an created event to the client owning the resource. - * @param resource_ The client's resource - * @param buffer id for the the newly created wl_buffer - */ static inline void zwp_linux_buffer_params_v1_send_created(struct wl_resource *resource_, struct wl_resource *buffer) { wl_resource_post_event(resource_, ZWP_LINUX_BUFFER_PARAMS_V1_CREATED, buffer); } -/** - * @ingroup iface_zwp_linux_buffer_params_v1 - * Sends an failed event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_linux_buffer_params_v1_send_failed(struct wl_resource *resource_) { @@ -796,48 +225,15 @@ zwp_linux_buffer_params_v1_send_failed(struct wl_resource *resource_) #ifndef ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM enum zwp_linux_dmabuf_feedback_v1_tranche_flags { - /** - * direct scan-out tranche - * - * The scanout flag is a hint that direct scan-out may be - * attempted by the compositor on the target device if the client - * appropriately allocates a buffer. How to allocate a buffer that - * can be scanned out on the target device is - * implementation-defined. - * @since 4 - */ ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SCANOUT = 1, - /** - * sampling tranche - * - * The sampling flag describes that the compositor is able to - * efficiently sample from buffers imported to the target device if - * the client appropriately allocates a buffer. How to allocate a - * buffer that can be efficiently sampled on the target device is - * implementation defined. - * @since 6 - */ ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SAMPLING = 2, }; -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SCANOUT_SINCE_VERSION 4 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SAMPLING_SINCE_VERSION 6 -#endif /* ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM */ +#endif #ifndef ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM_IS_VALID #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM_IS_VALID -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Validate a zwp_linux_dmabuf_feedback_v1 tranche_flags value. - * - * @return true on success, false on error. - * @ref zwp_linux_dmabuf_feedback_v1_tranche_flags - */ static inline bool zwp_linux_dmabuf_feedback_v1_tranche_flags_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -847,19 +243,9 @@ zwp_linux_dmabuf_feedback_v1_tranche_flags_is_valid(uint32_t value, uint32_t ver valid |= ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SAMPLING; return (value & ~valid) == 0; } -#endif /* ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * @struct zwp_linux_dmabuf_feedback_v1_interface - */ struct zwp_linux_dmabuf_feedback_v1_interface { - /** - * destroy the feedback object - * - * Using this request a client can tell the server that it is not - * going to use the zwp_linux_dmabuf_feedback_v1 object anymore. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); }; @@ -872,117 +258,52 @@ struct zwp_linux_dmabuf_feedback_v1_interface { #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FORMATS 5 #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS 6 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_FORMAT_TABLE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_MAIN_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_TARGET_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FORMATS_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FLAGS_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - */ #define ZWP_LINUX_DMABUF_FEEDBACK_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_linux_dmabuf_feedback_v1_send_done(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_FEEDBACK_V1_DONE); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an format_table event to the client owning the resource. - * @param resource_ The client's resource - * @param fd table file descriptor - * @param size table size, in bytes - */ static inline void zwp_linux_dmabuf_feedback_v1_send_format_table(struct wl_resource *resource_, int32_t fd, uint32_t size) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_FEEDBACK_V1_FORMAT_TABLE, fd, size); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an main_device event to the client owning the resource. - * @param resource_ The client's resource - * @param device device dev_t value - */ static inline void zwp_linux_dmabuf_feedback_v1_send_main_device(struct wl_resource *resource_, struct wl_array *device) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_FEEDBACK_V1_MAIN_DEVICE, device); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an tranche_done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_linux_dmabuf_feedback_v1_send_tranche_done(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_DONE); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an tranche_target_device event to the client owning the resource. - * @param resource_ The client's resource - * @param device device dev_t value - */ static inline void zwp_linux_dmabuf_feedback_v1_send_tranche_target_device(struct wl_resource *resource_, struct wl_array *device) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_TARGET_DEVICE, device); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an tranche_formats event to the client owning the resource. - * @param resource_ The client's resource - * @param indices array of 16-bit indexes - */ static inline void zwp_linux_dmabuf_feedback_v1_send_tranche_formats(struct wl_resource *resource_, struct wl_array *indices) { wl_resource_post_event(resource_, ZWP_LINUX_DMABUF_FEEDBACK_V1_TRANCHE_FORMATS, indices); } -/** - * @ingroup iface_zwp_linux_dmabuf_feedback_v1 - * Sends an tranche_flags event to the client owning the resource. - * @param resource_ The client's resource - * @param flags tranche flags - */ static inline void zwp_linux_dmabuf_feedback_v1_send_tranche_flags(struct wl_resource *resource_, uint32_t flags) { diff --git a/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-protocol.c index 89ea811ba..8a697736a 100644 --- a/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2014 Jonas Ådahl * Copyright © 2015 Red Hat Inc. @@ -30,7 +28,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-server-protocol.h index 8aeb92141..599797e8a 100644 --- a/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/pointer-constraints-unstable-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef POINTER_CONSTRAINTS_UNSTABLE_V1_SERVER_PROTOCOL_H #define POINTER_CONSTRAINTS_UNSTABLE_V1_SERVER_PROTOCOL_H @@ -14,37 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_pointer_constraints_unstable_v1 The pointer_constraints_unstable_v1 protocol - * protocol for constraining pointer motions - * - * @section page_desc_pointer_constraints_unstable_v1 Description - * - * This protocol specifies a set of interfaces used for adding constraints to - * the motion of a pointer. Possible constraints include confining pointer - * motions to a given region, or locking it to its current position. - * - * In order to constrain the pointer, a client must first bind the global - * interface "wp_pointer_constraints" which, if a compositor supports pointer - * constraints, is exposed by the registry. Using the bound global object, the - * client uses the request that corresponds to the type of constraint it wants - * to make. See wp_pointer_constraints for more details. - * - * Warning! The protocol described in this file is experimental and backward - * incompatible changes may be made. Backward compatible changes may be added - * together with the corresponding interface version bump. Backward - * incompatible changes are done by bumping the version number in the protocol - * and interface names and resetting the interface version. Once the protocol - * is to be declared stable, the 'z' prefix and the version number in the - * protocol and interface names are removed and the interface version number is - * reset. - * - * @section page_ifaces_pointer_constraints_unstable_v1 Interfaces - * - @subpage page_iface_zwp_pointer_constraints_v1 - constrain the movement of a pointer - * - @subpage page_iface_zwp_locked_pointer_v1 - receive relative pointer motion events - * - @subpage page_iface_zwp_confined_pointer_v1 - confined pointer object +/* * @section page_copyright_pointer_constraints_unstable_v1 Copyright - *
  *
  * Copyright © 2014      Jonas Ådahl
  * Copyright © 2015      Red Hat Inc.
@@ -67,8 +37,37 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_pointer; struct wl_region; struct wl_surface; @@ -78,180 +77,26 @@ struct zwp_pointer_constraints_v1; #ifndef ZWP_POINTER_CONSTRAINTS_V1_INTERFACE #define ZWP_POINTER_CONSTRAINTS_V1_INTERFACE -/** - * @page page_iface_zwp_pointer_constraints_v1 zwp_pointer_constraints_v1 - * @section page_iface_zwp_pointer_constraints_v1_desc Description - * - * The global interface exposing pointer constraining functionality. It - * exposes two requests: lock_pointer for locking the pointer to its - * position, and confine_pointer for locking the pointer to a region. - * - * The lock_pointer and confine_pointer requests create the objects - * wp_locked_pointer and wp_confined_pointer respectively, and the client can - * use these objects to interact with the lock. - * - * For any surface, only one lock or confinement may be active across all - * wl_pointer objects of the same seat. If a lock or confinement is requested - * when another lock or confinement is active or requested on the same surface - * and with any of the wl_pointer objects of the same seat, an - * 'already_constrained' error will be raised. - * @section page_iface_zwp_pointer_constraints_v1_api API - * See @ref iface_zwp_pointer_constraints_v1. - */ -/** - * @defgroup iface_zwp_pointer_constraints_v1 The zwp_pointer_constraints_v1 interface - * - * The global interface exposing pointer constraining functionality. It - * exposes two requests: lock_pointer for locking the pointer to its - * position, and confine_pointer for locking the pointer to a region. - * - * The lock_pointer and confine_pointer requests create the objects - * wp_locked_pointer and wp_confined_pointer respectively, and the client can - * use these objects to interact with the lock. - * - * For any surface, only one lock or confinement may be active across all - * wl_pointer objects of the same seat. If a lock or confinement is requested - * when another lock or confinement is active or requested on the same surface - * and with any of the wl_pointer objects of the same seat, an - * 'already_constrained' error will be raised. - */ extern const struct wl_interface zwp_pointer_constraints_v1_interface; #endif #ifndef ZWP_LOCKED_POINTER_V1_INTERFACE #define ZWP_LOCKED_POINTER_V1_INTERFACE -/** - * @page page_iface_zwp_locked_pointer_v1 zwp_locked_pointer_v1 - * @section page_iface_zwp_locked_pointer_v1_desc Description - * - * The wp_locked_pointer interface represents a locked pointer state. - * - * While the lock of this object is active, the wl_pointer objects of the - * associated seat will not emit any wl_pointer.motion events. - * - * This object will send the event 'locked' when the lock is activated. - * Whenever the lock is activated, it is guaranteed that the locked surface - * will already have received pointer focus and that the pointer will be - * within the region passed to the request creating this object. - * - * To unlock the pointer, send the destroy request. This will also destroy - * the wp_locked_pointer object. - * - * If the compositor decides to unlock the pointer the unlocked event is - * sent. See wp_locked_pointer.unlock for details. - * - * When unlocking, the compositor may warp the cursor position to the set - * cursor position hint. If it does, it will not result in any relative - * motion events emitted via wp_relative_pointer. - * - * If the surface the lock was requested on is destroyed and the lock is not - * yet activated, the wp_locked_pointer object is now defunct and must be - * destroyed. - * @section page_iface_zwp_locked_pointer_v1_api API - * See @ref iface_zwp_locked_pointer_v1. - */ -/** - * @defgroup iface_zwp_locked_pointer_v1 The zwp_locked_pointer_v1 interface - * - * The wp_locked_pointer interface represents a locked pointer state. - * - * While the lock of this object is active, the wl_pointer objects of the - * associated seat will not emit any wl_pointer.motion events. - * - * This object will send the event 'locked' when the lock is activated. - * Whenever the lock is activated, it is guaranteed that the locked surface - * will already have received pointer focus and that the pointer will be - * within the region passed to the request creating this object. - * - * To unlock the pointer, send the destroy request. This will also destroy - * the wp_locked_pointer object. - * - * If the compositor decides to unlock the pointer the unlocked event is - * sent. See wp_locked_pointer.unlock for details. - * - * When unlocking, the compositor may warp the cursor position to the set - * cursor position hint. If it does, it will not result in any relative - * motion events emitted via wp_relative_pointer. - * - * If the surface the lock was requested on is destroyed and the lock is not - * yet activated, the wp_locked_pointer object is now defunct and must be - * destroyed. - */ extern const struct wl_interface zwp_locked_pointer_v1_interface; #endif #ifndef ZWP_CONFINED_POINTER_V1_INTERFACE #define ZWP_CONFINED_POINTER_V1_INTERFACE -/** - * @page page_iface_zwp_confined_pointer_v1 zwp_confined_pointer_v1 - * @section page_iface_zwp_confined_pointer_v1_desc Description - * - * The wp_confined_pointer interface represents a confined pointer state. - * - * This object will send the event 'confined' when the confinement is - * activated. Whenever the confinement is activated, it is guaranteed that - * the surface the pointer is confined to will already have received pointer - * focus and that the pointer will be within the region passed to the request - * creating this object. It is up to the compositor to decide whether this - * requires some user interaction and if the pointer will warp to within the - * passed region if outside. - * - * To unconfine the pointer, send the destroy request. This will also destroy - * the wp_confined_pointer object. - * - * If the compositor decides to unconfine the pointer the unconfined event is - * sent. The wp_confined_pointer object is at this point defunct and should - * be destroyed. - * @section page_iface_zwp_confined_pointer_v1_api API - * See @ref iface_zwp_confined_pointer_v1. - */ -/** - * @defgroup iface_zwp_confined_pointer_v1 The zwp_confined_pointer_v1 interface - * - * The wp_confined_pointer interface represents a confined pointer state. - * - * This object will send the event 'confined' when the confinement is - * activated. Whenever the confinement is activated, it is guaranteed that - * the surface the pointer is confined to will already have received pointer - * focus and that the pointer will be within the region passed to the request - * creating this object. It is up to the compositor to decide whether this - * requires some user interaction and if the pointer will warp to within the - * passed region if outside. - * - * To unconfine the pointer, send the destroy request. This will also destroy - * the wp_confined_pointer object. - * - * If the compositor decides to unconfine the pointer the unconfined event is - * sent. The wp_confined_pointer object is at this point defunct and should - * be destroyed. - */ extern const struct wl_interface zwp_confined_pointer_v1_interface; #endif #ifndef ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM #define ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM -/** - * @ingroup iface_zwp_pointer_constraints_v1 - * wp_pointer_constraints error values - * - * These errors can be emitted in response to wp_pointer_constraints - * requests. - */ enum zwp_pointer_constraints_v1_error { - /** - * pointer constraint already requested on that surface - */ ZWP_POINTER_CONSTRAINTS_V1_ERROR_ALREADY_CONSTRAINED = 1, }; -#endif /* ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM */ +#endif #ifndef ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM_IS_VALID #define ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_zwp_pointer_constraints_v1 - * Validate a zwp_pointer_constraints_v1 error value. - * - * @return true on success, false on error. - * @ref zwp_pointer_constraints_v1_error - */ static inline bool zwp_pointer_constraints_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -261,49 +106,18 @@ zwp_pointer_constraints_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_POINTER_CONSTRAINTS_V1_ERROR_ENUM_IS_VALID */ +#endif #ifndef ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM #define ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM -/** - * @ingroup iface_zwp_pointer_constraints_v1 - * constraint lifetime - * - * These values represent different lifetime semantics. They are passed - * as arguments to the factory requests to specify how the constraint - * lifetimes should be managed. - */ enum zwp_pointer_constraints_v1_lifetime { - /** - * the pointer constraint is defunct once deactivated - * - * A oneshot pointer constraint will never reactivate once it has - * been deactivated. See the corresponding deactivation event - * (wp_locked_pointer.unlocked and wp_confined_pointer.unconfined) - * for details. - */ ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ONESHOT = 1, - /** - * the pointer constraint may reactivate - * - * A persistent pointer constraint may again reactivate once it - * has been deactivated. See the corresponding deactivation event - * (wp_locked_pointer.unlocked and wp_confined_pointer.unconfined) - * for details. - */ ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT = 2, }; -#endif /* ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM */ +#endif #ifndef ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM_IS_VALID #define ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM_IS_VALID -/** - * @ingroup iface_zwp_pointer_constraints_v1 - * Validate a zwp_pointer_constraints_v1 lifetime value. - * - * @return true on success, false on error. - * @ref zwp_pointer_constraints_v1_lifetime - */ static inline bool zwp_pointer_constraints_v1_lifetime_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -315,67 +129,11 @@ zwp_pointer_constraints_v1_lifetime_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_zwp_pointer_constraints_v1 - * @struct zwp_pointer_constraints_v1_interface - */ struct zwp_pointer_constraints_v1_interface { - /** - * destroy the pointer constraints manager object - * - * Used by the client to notify the server that it will no longer - * use this pointer constraints object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * lock pointer to a position - * - * The lock_pointer request lets the client request to disable - * movements of the virtual pointer (i.e. the cursor), effectively - * locking the pointer to a position. This request may not take - * effect immediately; in the future, when the compositor deems - * implementation-specific constraints are satisfied, the pointer - * lock will be activated and the compositor sends a locked event. - * - * The protocol provides no guarantee that the constraints are ever - * satisfied, and does not require the compositor to send an error - * if the constraints cannot ever be satisfied. It is thus possible - * to request a lock that will never activate. - * - * There may not be another pointer constraint of any kind - * requested or active on the surface for any of the wl_pointer - * objects of the seat of the passed pointer when requesting a - * lock. If there is, an error will be raised. See general pointer - * lock documentation for more details. - * - * The intersection of the region passed with this request and the - * input region of the surface is used to determine where the - * pointer must be in order for the lock to activate. It is up to - * the compositor whether to warp the pointer or require some kind - * of user interaction for the lock to activate. If the region is - * null the surface input region is used. - * - * A surface may receive pointer focus without the lock being - * activated. - * - * The request creates a new object wp_locked_pointer which is used - * to interact with the lock as well as receive updates about its - * state. See the the description of wp_locked_pointer for further - * information. - * - * Note that while a pointer is locked, the wl_pointer objects of - * the corresponding seat will not emit any wl_pointer.motion - * events, but relative motion events will still be emitted via - * wp_relative_pointer objects of the same seat. wl_pointer.axis - * and wl_pointer.button events are unaffected. - * @param surface surface to lock pointer to - * @param pointer the pointer that should be locked - * @param region region of surface - * @param lifetime lock lifetime - */ void (*lock_pointer)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -383,32 +141,6 @@ struct zwp_pointer_constraints_v1_interface { struct wl_resource *pointer, struct wl_resource *region, uint32_t lifetime); - /** - * confine pointer to a region - * - * The confine_pointer request lets the client request to confine - * the pointer cursor to a given region. This request may not take - * effect immediately; in the future, when the compositor deems - * implementation- specific constraints are satisfied, the pointer - * confinement will be activated and the compositor sends a - * confined event. - * - * The intersection of the region passed with this request and the - * input region of the surface is used to determine where the - * pointer must be in order for the confinement to activate. It is - * up to the compositor whether to warp the pointer or require some - * kind of user interaction for the confinement to activate. If the - * region is null the surface input region is used. - * - * The request will create a new object wp_confined_pointer which - * is used to interact with the confinement as well as receive - * updates about its state. See the the description of - * wp_confined_pointer for further information. - * @param surface surface to lock pointer to - * @param pointer the pointer that should be confined - * @param region region of surface - * @param lifetime confinement lifetime - */ void (*confine_pointer)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -419,65 +151,17 @@ struct zwp_pointer_constraints_v1_interface { }; -/** - * @ingroup iface_zwp_pointer_constraints_v1 - */ #define ZWP_POINTER_CONSTRAINTS_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_pointer_constraints_v1 - */ #define ZWP_POINTER_CONSTRAINTS_V1_LOCK_POINTER_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_pointer_constraints_v1 - */ #define ZWP_POINTER_CONSTRAINTS_V1_CONFINE_POINTER_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - * @struct zwp_locked_pointer_v1_interface - */ struct zwp_locked_pointer_v1_interface { - /** - * destroy the locked pointer object - * - * Destroy the locked pointer object. If applicable, the - * compositor will unlock the pointer. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the pointer cursor position hint - * - * Set the cursor position hint relative to the top left corner - * of the surface. - * - * If the client is drawing its own cursor, it should update the - * position hint to the position of its own cursor. A compositor - * may use this information to warp the pointer upon unlock in - * order to avoid pointer jumps. - * - * The cursor position hint is double buffered. The new hint will - * only take effect when the associated surface gets it pending - * state applied. See wl_surface.commit for details. - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - */ void (*set_cursor_position_hint)(struct wl_client *client, struct wl_resource *resource, wl_fixed_t surface_x, wl_fixed_t surface_y); - /** - * set a new lock region - * - * Set a new region used to lock the pointer. - * - * The new lock region is double-buffered. The new lock region will - * only take effect when the associated surface gets its pending - * state applied. See wl_surface.commit for details. - * - * For details about the lock region, see wp_locked_pointer. - * @param region region of surface - */ void (*set_region)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *region); @@ -486,84 +170,28 @@ struct zwp_locked_pointer_v1_interface { #define ZWP_LOCKED_POINTER_V1_LOCKED 0 #define ZWP_LOCKED_POINTER_V1_UNLOCKED 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - */ #define ZWP_LOCKED_POINTER_V1_LOCKED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - */ #define ZWP_LOCKED_POINTER_V1_UNLOCKED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - */ #define ZWP_LOCKED_POINTER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - */ #define ZWP_LOCKED_POINTER_V1_SET_CURSOR_POSITION_HINT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - */ #define ZWP_LOCKED_POINTER_V1_SET_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_locked_pointer_v1 - * Sends an locked event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_locked_pointer_v1_send_locked(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWP_LOCKED_POINTER_V1_LOCKED); } -/** - * @ingroup iface_zwp_locked_pointer_v1 - * Sends an unlocked event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_locked_pointer_v1_send_unlocked(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWP_LOCKED_POINTER_V1_UNLOCKED); } -/** - * @ingroup iface_zwp_confined_pointer_v1 - * @struct zwp_confined_pointer_v1_interface - */ struct zwp_confined_pointer_v1_interface { - /** - * destroy the confined pointer object - * - * Destroy the confined pointer object. If applicable, the - * compositor will unconfine the pointer. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set a new confine region - * - * Set a new region used to confine the pointer. - * - * The new confine region is double-buffered. The new confine - * region will only take effect when the associated surface gets - * its pending state applied. See wl_surface.commit for details. - * - * If the confinement is active when the new confinement region is - * applied and the pointer ends up outside of newly applied region, - * the pointer may warped to a position within the new confinement - * region. If warped, a wl_pointer.motion event will be emitted, - * but no wp_relative_pointer.relative_motion event. - * - * The compositor may also, instead of using the new region, - * unconfine the pointer. - * - * For details about the confine region, see wp_confined_pointer. - * @param region region of surface - */ void (*set_region)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *region); @@ -572,40 +200,18 @@ struct zwp_confined_pointer_v1_interface { #define ZWP_CONFINED_POINTER_V1_CONFINED 0 #define ZWP_CONFINED_POINTER_V1_UNCONFINED 1 -/** - * @ingroup iface_zwp_confined_pointer_v1 - */ #define ZWP_CONFINED_POINTER_V1_CONFINED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_confined_pointer_v1 - */ #define ZWP_CONFINED_POINTER_V1_UNCONFINED_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_confined_pointer_v1 - */ #define ZWP_CONFINED_POINTER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_confined_pointer_v1 - */ #define ZWP_CONFINED_POINTER_V1_SET_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_confined_pointer_v1 - * Sends an confined event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_confined_pointer_v1_send_confined(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWP_CONFINED_POINTER_V1_CONFINED); } -/** - * @ingroup iface_zwp_confined_pointer_v1 - * Sends an unconfined event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_confined_pointer_v1_send_unconfined(struct wl_resource *resource_) { diff --git a/app/src/main/cpp/waylandcomp/generated/presentation-time-protocol.c b/app/src/main/cpp/waylandcomp/generated/presentation-time-protocol.c index 63eea910d..8db001733 100644 --- a/app/src/main/cpp/waylandcomp/generated/presentation-time-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/presentation-time-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2013-2014 Collabora, Ltd. * @@ -29,7 +27,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/presentation-time-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/presentation-time-server-protocol.h index 1e1a39c07..828051726 100644 --- a/app/src/main/cpp/waylandcomp/generated/presentation-time-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/presentation-time-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef PRESENTATION_TIME_SERVER_PROTOCOL_H #define PRESENTATION_TIME_SERVER_PROTOCOL_H @@ -14,13 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_presentation_time The presentation_time protocol - * @section page_ifaces_presentation_time Interfaces - * - @subpage page_iface_wp_presentation - timed presentation related wl_surface requests - * - @subpage page_iface_wp_presentation_feedback - presentation time feedback event +/* * @section page_copyright_presentation_time Copyright - *
  *
  * Copyright © 2013-2014 Collabora, Ltd.
  *
@@ -42,8 +36,13 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + struct wl_output; struct wl_surface; struct wp_presentation; @@ -51,131 +50,23 @@ struct wp_presentation_feedback; #ifndef WP_PRESENTATION_INTERFACE #define WP_PRESENTATION_INTERFACE -/** - * @page page_iface_wp_presentation wp_presentation - * @section page_iface_wp_presentation_desc Description - * - * - * - * - * The main feature of this interface is accurate presentation - * timing feedback to ensure smooth video playback while maintaining - * audio/video synchronization. Some features use the concept of a - * presentation clock, which is defined in the - * presentation.clock_id event. - * - * A content update for a wl_surface is submitted by a - * wl_surface.commit request. Request 'feedback' associates with - * the wl_surface.commit and provides feedback on the content - * update, particularly the final realized presentation time. - * - * - * - * When the final realized presentation time is available, e.g. - * after a framebuffer flip completes, the requested - * presentation_feedback.presented events are sent. The final - * presentation time can differ from the compositor's predicted - * display update time and the update's target time, especially - * when the compositor misses its target vertical blanking period. - * @section page_iface_wp_presentation_api API - * See @ref iface_wp_presentation. - */ -/** - * @defgroup iface_wp_presentation The wp_presentation interface - * - * - * - * - * The main feature of this interface is accurate presentation - * timing feedback to ensure smooth video playback while maintaining - * audio/video synchronization. Some features use the concept of a - * presentation clock, which is defined in the - * presentation.clock_id event. - * - * A content update for a wl_surface is submitted by a - * wl_surface.commit request. Request 'feedback' associates with - * the wl_surface.commit and provides feedback on the content - * update, particularly the final realized presentation time. - * - * - * - * When the final realized presentation time is available, e.g. - * after a framebuffer flip completes, the requested - * presentation_feedback.presented events are sent. The final - * presentation time can differ from the compositor's predicted - * display update time and the update's target time, especially - * when the compositor misses its target vertical blanking period. - */ extern const struct wl_interface wp_presentation_interface; #endif #ifndef WP_PRESENTATION_FEEDBACK_INTERFACE #define WP_PRESENTATION_FEEDBACK_INTERFACE -/** - * @page page_iface_wp_presentation_feedback wp_presentation_feedback - * @section page_iface_wp_presentation_feedback_desc Description - * - * A presentation_feedback object returns an indication that a - * wl_surface content update has become visible to the user. - * One object corresponds to one content update submission - * (wl_surface.commit). There are two possible outcomes: the - * content update is presented to the user, and a presentation - * timestamp delivered; or, the user did not see the content - * update because it was superseded or its surface destroyed, - * and the content update is discarded. - * - * Once a presentation_feedback object has delivered a 'presented' - * or 'discarded' event it is automatically destroyed. - * @section page_iface_wp_presentation_feedback_api API - * See @ref iface_wp_presentation_feedback. - */ -/** - * @defgroup iface_wp_presentation_feedback The wp_presentation_feedback interface - * - * A presentation_feedback object returns an indication that a - * wl_surface content update has become visible to the user. - * One object corresponds to one content update submission - * (wl_surface.commit). There are two possible outcomes: the - * content update is presented to the user, and a presentation - * timestamp delivered; or, the user did not see the content - * update because it was superseded or its surface destroyed, - * and the content update is discarded. - * - * Once a presentation_feedback object has delivered a 'presented' - * or 'discarded' event it is automatically destroyed. - */ extern const struct wl_interface wp_presentation_feedback_interface; #endif #ifndef WP_PRESENTATION_ERROR_ENUM #define WP_PRESENTATION_ERROR_ENUM -/** - * @ingroup iface_wp_presentation - * fatal presentation errors - * - * These fatal protocol errors may be emitted in response to - * illegal presentation requests. - */ enum wp_presentation_error { - /** - * invalid value in tv_nsec - */ WP_PRESENTATION_ERROR_INVALID_TIMESTAMP = 0, - /** - * invalid flag - */ WP_PRESENTATION_ERROR_INVALID_FLAG = 1, }; -#endif /* WP_PRESENTATION_ERROR_ENUM */ +#endif #ifndef WP_PRESENTATION_ERROR_ENUM_IS_VALID #define WP_PRESENTATION_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_presentation - * Validate a wp_presentation error value. - * - * @return true on success, false on error. - * @ref wp_presentation_error - */ static inline bool wp_presentation_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -187,37 +78,11 @@ wp_presentation_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_PRESENTATION_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_presentation - * @struct wp_presentation_interface - */ struct wp_presentation_interface { - /** - * unbind from the presentation interface - * - * Informs the server that the client will no longer be using - * this protocol object. Existing objects created by this object - * are not affected. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * request presentation feedback information - * - * Request presentation feedback for the current content - * submission on the given surface. This creates a new - * presentation_feedback object, which will deliver the feedback - * information once. If multiple presentation_feedback objects are - * created for the same submission, they will all deliver the same - * information. - * - * For details on what information is returned, see the - * presentation_feedback interface. - * @param surface target surface - * @param callback new feedback object - */ void (*feedback)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *surface, @@ -226,26 +91,11 @@ struct wp_presentation_interface { #define WP_PRESENTATION_CLOCK_ID 0 -/** - * @ingroup iface_wp_presentation - */ #define WP_PRESENTATION_CLOCK_ID_SINCE_VERSION 1 -/** - * @ingroup iface_wp_presentation - */ #define WP_PRESENTATION_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_presentation - */ #define WP_PRESENTATION_FEEDBACK_SINCE_VERSION 1 -/** - * @ingroup iface_wp_presentation - * Sends an clock_id event to the client owning the resource. - * @param resource_ The client's resource - * @param clk_id platform clock identifier - */ static inline void wp_presentation_send_clock_id(struct wl_resource *resource_, uint32_t clk_id) { @@ -254,66 +104,16 @@ wp_presentation_send_clock_id(struct wl_resource *resource_, uint32_t clk_id) #ifndef WP_PRESENTATION_FEEDBACK_KIND_ENUM #define WP_PRESENTATION_FEEDBACK_KIND_ENUM -/** - * @ingroup iface_wp_presentation_feedback - * bitmask of flags in presented event - * - * These flags provide information about how the presentation of - * the related content update was done. The intent is to help - * clients assess the reliability of the feedback and the visual - * quality with respect to possible tearing and timings. - */ enum wp_presentation_feedback_kind { - /** - * presentation was vsync'd - * - * The presentation was synchronized to the "vertical retrace" by - * the display hardware such that tearing does not happen. Relying - * on software scheduling is not acceptable for this flag. If - * presentation is done by a copy to the active frontbuffer, then - * it must guarantee that tearing cannot happen. - */ WP_PRESENTATION_FEEDBACK_KIND_VSYNC = 0x1, - /** - * hardware provided the presentation timestamp - * - * The display hardware provided measurements that the hardware - * driver converted into a presentation timestamp. Sampling a clock - * in software is not acceptable for this flag. - */ WP_PRESENTATION_FEEDBACK_KIND_HW_CLOCK = 0x2, - /** - * hardware signalled the start of the presentation - * - * The display hardware signalled that it started using the new - * image content. The opposite of this is e.g. a timer being used - * to guess when the display hardware has switched to the new image - * content. - */ WP_PRESENTATION_FEEDBACK_KIND_HW_COMPLETION = 0x4, - /** - * presentation was done zero-copy - * - * The presentation of this update was done zero-copy. This means - * the buffer from the client was given to display hardware as is, - * without copying it. Compositing with OpenGL counts as copying, - * even if textured directly from the client buffer. Possible - * zero-copy cases include direct scanout of a fullscreen surface - * and a surface on a hardware overlay. - */ WP_PRESENTATION_FEEDBACK_KIND_ZERO_COPY = 0x8, }; -#endif /* WP_PRESENTATION_FEEDBACK_KIND_ENUM */ +#endif #ifndef WP_PRESENTATION_FEEDBACK_KIND_ENUM_IS_VALID #define WP_PRESENTATION_FEEDBACK_KIND_ENUM_IS_VALID -/** - * @ingroup iface_wp_presentation_feedback - * Validate a wp_presentation_feedback kind value. - * - * @return true on success, false on error. - * @ref wp_presentation_feedback_kind - */ static inline bool wp_presentation_feedback_kind_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -327,61 +127,29 @@ wp_presentation_feedback_kind_is_valid(uint32_t value, uint32_t version) { valid |= WP_PRESENTATION_FEEDBACK_KIND_ZERO_COPY; return (value & ~valid) == 0; } -#endif /* WP_PRESENTATION_FEEDBACK_KIND_ENUM_IS_VALID */ +#endif #define WP_PRESENTATION_FEEDBACK_SYNC_OUTPUT 0 #define WP_PRESENTATION_FEEDBACK_PRESENTED 1 #define WP_PRESENTATION_FEEDBACK_DISCARDED 2 -/** - * @ingroup iface_wp_presentation_feedback - */ #define WP_PRESENTATION_FEEDBACK_SYNC_OUTPUT_SINCE_VERSION 1 -/** - * @ingroup iface_wp_presentation_feedback - */ #define WP_PRESENTATION_FEEDBACK_PRESENTED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_presentation_feedback - */ #define WP_PRESENTATION_FEEDBACK_DISCARDED_SINCE_VERSION 1 -/** - * @ingroup iface_wp_presentation_feedback - * Sends an sync_output event to the client owning the resource. - * @param resource_ The client's resource - * @param output presentation output - */ static inline void wp_presentation_feedback_send_sync_output(struct wl_resource *resource_, struct wl_resource *output) { wl_resource_post_event(resource_, WP_PRESENTATION_FEEDBACK_SYNC_OUTPUT, output); } -/** - * @ingroup iface_wp_presentation_feedback - * Sends an presented event to the client owning the resource. - * @param resource_ The client's resource - * @param tv_sec_hi high 32 bits of the seconds part of the presentation timestamp - * @param tv_sec_lo low 32 bits of the seconds part of the presentation timestamp - * @param tv_nsec nanoseconds part of the presentation timestamp - * @param refresh nanoseconds till next refresh - * @param seq_hi high 32 bits of refresh counter - * @param seq_lo low 32 bits of refresh counter - * @param flags combination of 'kind' values - */ static inline void wp_presentation_feedback_send_presented(struct wl_resource *resource_, uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec, uint32_t refresh, uint32_t seq_hi, uint32_t seq_lo, uint32_t flags) { wl_resource_post_event(resource_, WP_PRESENTATION_FEEDBACK_PRESENTED, tv_sec_hi, tv_sec_lo, tv_nsec, refresh, seq_hi, seq_lo, flags); } -/** - * @ingroup iface_wp_presentation_feedback - * Sends an discarded event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wp_presentation_feedback_send_discarded(struct wl_resource *resource_) { diff --git a/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-protocol.c index 454f95556..eb9254f31 100644 --- a/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2014 Jonas Ådahl * Copyright © 2015 Red Hat Inc. @@ -30,7 +28,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-server-protocol.h index f40c97498..a25e2b18c 100644 --- a/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/relative-pointer-unstable-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef RELATIVE_POINTER_UNSTABLE_V1_SERVER_PROTOCOL_H #define RELATIVE_POINTER_UNSTABLE_V1_SERVER_PROTOCOL_H @@ -14,40 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_relative_pointer_unstable_v1 The relative_pointer_unstable_v1 protocol - * protocol for relative pointer motion events - * - * @section page_desc_relative_pointer_unstable_v1 Description - * - * This protocol specifies a set of interfaces used for making clients able to - * receive relative pointer events not obstructed by barriers (such as the - * monitor edge or other pointer barriers). - * - * To start receiving relative pointer events, a client must first bind the - * global interface "wp_relative_pointer_manager" which, if a compositor - * supports relative pointer motion events, is exposed by the registry. After - * having created the relative pointer manager proxy object, the client uses - * it to create the actual relative pointer object using the - * "get_relative_pointer" request given a wl_pointer. The relative pointer - * motion events will then, when applicable, be transmitted via the proxy of - * the newly created relative pointer object. See the documentation of the - * relative pointer interface for more details. - * - * Warning! The protocol described in this file is experimental and backward - * incompatible changes may be made. Backward compatible changes may be added - * together with the corresponding interface version bump. Backward - * incompatible changes are done by bumping the version number in the protocol - * and interface names and resetting the interface version. Once the protocol - * is to be declared stable, the 'z' prefix and the version number in the - * protocol and interface names are removed and the interface version number is - * reset. - * - * @section page_ifaces_relative_pointer_unstable_v1 Interfaces - * - @subpage page_iface_zwp_relative_pointer_manager_v1 - get relative pointer objects - * - @subpage page_iface_zwp_relative_pointer_v1 - relative pointer object +/* * @section page_copyright_relative_pointer_unstable_v1 Copyright - *
  *
  * Copyright © 2014      Jonas Ådahl
  * Copyright © 2015      Red Hat Inc.
@@ -70,74 +37,56 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_pointer; struct zwp_relative_pointer_manager_v1; struct zwp_relative_pointer_v1; #ifndef ZWP_RELATIVE_POINTER_MANAGER_V1_INTERFACE #define ZWP_RELATIVE_POINTER_MANAGER_V1_INTERFACE -/** - * @page page_iface_zwp_relative_pointer_manager_v1 zwp_relative_pointer_manager_v1 - * @section page_iface_zwp_relative_pointer_manager_v1_desc Description - * - * A global interface used for getting the relative pointer object for a - * given pointer. - * @section page_iface_zwp_relative_pointer_manager_v1_api API - * See @ref iface_zwp_relative_pointer_manager_v1. - */ -/** - * @defgroup iface_zwp_relative_pointer_manager_v1 The zwp_relative_pointer_manager_v1 interface - * - * A global interface used for getting the relative pointer object for a - * given pointer. - */ extern const struct wl_interface zwp_relative_pointer_manager_v1_interface; #endif #ifndef ZWP_RELATIVE_POINTER_V1_INTERFACE #define ZWP_RELATIVE_POINTER_V1_INTERFACE -/** - * @page page_iface_zwp_relative_pointer_v1 zwp_relative_pointer_v1 - * @section page_iface_zwp_relative_pointer_v1_desc Description - * - * A wp_relative_pointer object is an extension to the wl_pointer interface - * used for emitting relative pointer events. It shares the same focus as - * wl_pointer objects of the same seat and will only emit events when it has - * focus. - * @section page_iface_zwp_relative_pointer_v1_api API - * See @ref iface_zwp_relative_pointer_v1. - */ -/** - * @defgroup iface_zwp_relative_pointer_v1 The zwp_relative_pointer_v1 interface - * - * A wp_relative_pointer object is an extension to the wl_pointer interface - * used for emitting relative pointer events. It shares the same focus as - * wl_pointer objects of the same seat and will only emit events when it has - * focus. - */ extern const struct wl_interface zwp_relative_pointer_v1_interface; #endif -/** - * @ingroup iface_zwp_relative_pointer_manager_v1 - * @struct zwp_relative_pointer_manager_v1_interface - */ struct zwp_relative_pointer_manager_v1_interface { - /** - * destroy the relative pointer manager object - * - * Used by the client to notify the server that it will no longer - * use this relative pointer manager object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * get a relative pointer object - * - * Create a relative pointer interface given a wl_pointer object. - * See the wp_relative_pointer interface for more details. - */ void (*get_relative_pointer)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -145,52 +94,20 @@ struct zwp_relative_pointer_manager_v1_interface { }; -/** - * @ingroup iface_zwp_relative_pointer_manager_v1 - */ #define ZWP_RELATIVE_POINTER_MANAGER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_relative_pointer_manager_v1 - */ #define ZWP_RELATIVE_POINTER_MANAGER_V1_GET_RELATIVE_POINTER_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_relative_pointer_v1 - * @struct zwp_relative_pointer_v1_interface - */ struct zwp_relative_pointer_v1_interface { - /** - * release the relative pointer object - * - * - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); }; #define ZWP_RELATIVE_POINTER_V1_RELATIVE_MOTION 0 -/** - * @ingroup iface_zwp_relative_pointer_v1 - */ #define ZWP_RELATIVE_POINTER_V1_RELATIVE_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_relative_pointer_v1 - */ #define ZWP_RELATIVE_POINTER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_relative_pointer_v1 - * Sends an relative_motion event to the client owning the resource. - * @param resource_ The client's resource - * @param utime_hi high 32 bits of a 64 bit timestamp with microsecond granularity - * @param utime_lo low 32 bits of a 64 bit timestamp with microsecond granularity - * @param dx the x component of the motion vector - * @param dy the y component of the motion vector - * @param dx_unaccel the x component of the unaccelerated motion vector - * @param dy_unaccel the y component of the unaccelerated motion vector - */ static inline void zwp_relative_pointer_v1_send_relative_motion(struct wl_resource *resource_, uint32_t utime_hi, uint32_t utime_lo, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t dx_unaccel, wl_fixed_t dy_unaccel) { diff --git a/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-protocol.c index 01434221c..2543c95cf 100644 --- a/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.26.0 */ - /* * Copyright © 2022 Simon Ser * @@ -29,7 +27,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-server-protocol.h index a030d4775..a081bc417 100644 --- a/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/single-pixel-buffer-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.26.0 */ #ifndef SINGLE_PIXEL_BUFFER_V1_SERVER_PROTOCOL_H #define SINGLE_PIXEL_BUFFER_V1_SERVER_PROTOCOL_H @@ -14,27 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_single_pixel_buffer_v1 The single_pixel_buffer_v1 protocol - * single pixel buffer factory - * - * @section page_desc_single_pixel_buffer_v1 Description - * - * This protocol extension allows clients to create single-pixel buffers. - * - * Compositors supporting this protocol extension should also support the - * viewporter protocol extension. Clients may use viewporter to scale a - * single-pixel buffer to a desired size. - * - * Warning! The protocol described in this file is currently in the testing - * phase. Backward compatible changes may be added together with the - * corresponding interface version bump. Backward incompatible changes can - * only be done by creating a new major version of the extension. - * - * @section page_ifaces_single_pixel_buffer_v1 Interfaces - * - @subpage page_iface_wp_single_pixel_buffer_manager_v1 - global factory for single-pixel buffers +/* * @section page_copyright_single_pixel_buffer_v1 Copyright - *
  *
  * Copyright © 2022 Simon Ser
  *
@@ -56,66 +36,38 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + struct wl_buffer; struct wp_single_pixel_buffer_manager_v1; #ifndef WP_SINGLE_PIXEL_BUFFER_MANAGER_V1_INTERFACE #define WP_SINGLE_PIXEL_BUFFER_MANAGER_V1_INTERFACE -/** - * @page page_iface_wp_single_pixel_buffer_manager_v1 wp_single_pixel_buffer_manager_v1 - * @section page_iface_wp_single_pixel_buffer_manager_v1_desc Description - * - * The wp_single_pixel_buffer_manager_v1 interface is a factory for - * single-pixel buffers. - * @section page_iface_wp_single_pixel_buffer_manager_v1_api API - * See @ref iface_wp_single_pixel_buffer_manager_v1. - */ -/** - * @defgroup iface_wp_single_pixel_buffer_manager_v1 The wp_single_pixel_buffer_manager_v1 interface - * - * The wp_single_pixel_buffer_manager_v1 interface is a factory for - * single-pixel buffers. - */ extern const struct wl_interface wp_single_pixel_buffer_manager_v1_interface; #endif -/** - * @ingroup iface_wp_single_pixel_buffer_manager_v1 - * @struct wp_single_pixel_buffer_manager_v1_interface - */ struct wp_single_pixel_buffer_manager_v1_interface { - /** - * destroy the manager - * - * Destroy the wp_single_pixel_buffer_manager_v1 object. - * - * The child objects created via this interface are unaffected. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * create a 1×1 buffer from 32-bit RGBA values - * - * Create a single-pixel buffer from four 32-bit RGBA values. - * - * Unless specified in another protocol extension, the RGBA values - * use pre-multiplied alpha. - * - * The width and height of the buffer are 1. - * - * The r, g, b and a arguments valid range is from UINT32_MIN (0) - * to UINT32_MAX (0xffffffff). - * - * These arguments should be interpreted as a percentage, i.e. - - * UINT32_MIN = 0% of the given color component - UINT32_MAX = 100% - * of the given color component - * @param r value of the buffer's red channel - * @param g value of the buffer's green channel - * @param b value of the buffer's blue channel - * @param a value of the buffer's alpha channel - */ void (*create_u32_rgba_buffer)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -126,13 +78,7 @@ struct wp_single_pixel_buffer_manager_v1_interface { }; -/** - * @ingroup iface_wp_single_pixel_buffer_manager_v1 - */ #define WP_SINGLE_PIXEL_BUFFER_MANAGER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_single_pixel_buffer_manager_v1 - */ #define WP_SINGLE_PIXEL_BUFFER_MANAGER_V1_CREATE_U32_RGBA_BUFFER_SINCE_VERSION 1 #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-protocol.c b/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-protocol.c index a6030f8df..557da50a4 100644 --- a/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2012, 2013 Intel Corporation * Copyright © 2015, 2016 Jan Arne Petersen @@ -34,7 +32,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-server-protocol.h index c8473dd6a..e7e142ce2 100644 --- a/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/text-input-unstable-v3-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef TEXT_INPUT_UNSTABLE_V3_SERVER_PROTOCOL_H #define TEXT_INPUT_UNSTABLE_V3_SERVER_PROTOCOL_H @@ -14,33 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_text_input_unstable_v3 The text_input_unstable_v3 protocol - * Protocol for composing text - * - * @section page_desc_text_input_unstable_v3 Description - * - * This protocol allows compositors to act as input methods and to send text - * to applications. A text input object is used to manage state of what are - * typically text entry fields in the application. - * - * This document adheres to the RFC 2119 when using words like "must", - * "should", "may", etc. - * - * Warning! The protocol described in this file is experimental and - * backward incompatible changes may be made. Backward compatible changes - * may be added together with the corresponding interface version bump. - * Backward incompatible changes are done by bumping the version number in - * the protocol and interface names and resetting the interface version. - * Once the protocol is to be declared stable, the 'z' prefix and the - * version number in the protocol and interface names are removed and the - * interface version number is reset. - * - * @section page_ifaces_text_input_unstable_v3 Interfaces - * - @subpage page_iface_zwp_text_input_v3 - text input - * - @subpage page_iface_zwp_text_input_manager_v3 - text input manager +/* * @section page_copyright_text_input_unstable_v3 Copyright - *
  *
  * Copyright © 2012, 2013 Intel Corporation
  * Copyright © 2015, 2016 Jan Arne Petersen
@@ -67,8 +41,33 @@ struct wl_resource;
  * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
  * THIS SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_seat; struct wl_surface; struct zwp_text_input_manager_v3; @@ -76,118 +75,23 @@ struct zwp_text_input_v3; #ifndef ZWP_TEXT_INPUT_V3_INTERFACE #define ZWP_TEXT_INPUT_V3_INTERFACE -/** - * @page page_iface_zwp_text_input_v3 zwp_text_input_v3 - * @section page_iface_zwp_text_input_v3_desc Description - * - * The zwp_text_input_v3 interface represents text input and input methods - * associated with a seat. It provides enter/leave events to follow the - * text input focus for a seat. - * - * Requests are used to enable/disable the text-input object and set - * state information like surrounding and selected text or the content type. - * The information about the entered text is sent to the text-input object - * via the preedit_string and commit_string events. - * - * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices - * must not point to middle bytes inside a code point: they must either - * point to the first byte of a code point or to the end of the buffer. - * Lengths must be measured between two valid indices. - * - * Focus moving throughout surfaces will result in the emission of - * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused - * surface must commit zwp_text_input_v3.enable and - * zwp_text_input_v3.disable requests as the keyboard focus moves across - * editable and non-editable elements of the UI. Those two requests are not - * expected to be paired with each other, the compositor must be able to - * handle consecutive series of the same request. - * - * State is sent by the state requests (set_surrounding_text, - * set_content_type and set_cursor_rectangle) and a commit request. After an - * enter event or disable request all state information is invalidated and - * needs to be resent by the client. - * @section page_iface_zwp_text_input_v3_api API - * See @ref iface_zwp_text_input_v3. - */ -/** - * @defgroup iface_zwp_text_input_v3 The zwp_text_input_v3 interface - * - * The zwp_text_input_v3 interface represents text input and input methods - * associated with a seat. It provides enter/leave events to follow the - * text input focus for a seat. - * - * Requests are used to enable/disable the text-input object and set - * state information like surrounding and selected text or the content type. - * The information about the entered text is sent to the text-input object - * via the preedit_string and commit_string events. - * - * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices - * must not point to middle bytes inside a code point: they must either - * point to the first byte of a code point or to the end of the buffer. - * Lengths must be measured between two valid indices. - * - * Focus moving throughout surfaces will result in the emission of - * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused - * surface must commit zwp_text_input_v3.enable and - * zwp_text_input_v3.disable requests as the keyboard focus moves across - * editable and non-editable elements of the UI. Those two requests are not - * expected to be paired with each other, the compositor must be able to - * handle consecutive series of the same request. - * - * State is sent by the state requests (set_surrounding_text, - * set_content_type and set_cursor_rectangle) and a commit request. After an - * enter event or disable request all state information is invalidated and - * needs to be resent by the client. - */ extern const struct wl_interface zwp_text_input_v3_interface; #endif #ifndef ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE #define ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE -/** - * @page page_iface_zwp_text_input_manager_v3 zwp_text_input_manager_v3 - * @section page_iface_zwp_text_input_manager_v3_desc Description - * - * A factory for text-input objects. This object is a global singleton. - * @section page_iface_zwp_text_input_manager_v3_api API - * See @ref iface_zwp_text_input_manager_v3. - */ -/** - * @defgroup iface_zwp_text_input_manager_v3 The zwp_text_input_manager_v3 interface - * - * A factory for text-input objects. This object is a global singleton. - */ extern const struct wl_interface zwp_text_input_manager_v3_interface; #endif #ifndef ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM #define ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM -/** - * @ingroup iface_zwp_text_input_v3 - * text change reason - * - * Reason for the change of surrounding text or cursor posision. - */ enum zwp_text_input_v3_change_cause { - /** - * input method caused the change - */ ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD = 0, - /** - * something else than the input method caused the change - */ ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER = 1, }; -#endif /* ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM */ +#endif #ifndef ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM_IS_VALID #define ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM_IS_VALID -/** - * @ingroup iface_zwp_text_input_v3 - * Validate a zwp_text_input_v3 change_cause value. - * - * @return true on success, false on error. - * @ref zwp_text_input_v3_change_cause - */ static inline bool zwp_text_input_v3_change_cause_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -199,101 +103,33 @@ zwp_text_input_v3_change_cause_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM_IS_VALID */ +#endif #ifndef ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM #define ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM -/** - * @ingroup iface_zwp_text_input_v3 - * content hint - * - * Content hint is a bitmask to allow to modify the behavior of the text - * input. - */ enum zwp_text_input_v3_content_hint { - /** - * no special behavior - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE = 0x0, - /** - * suggest word completions - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION = 0x1, - /** - * suggest word corrections - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK = 0x2, - /** - * switch to uppercase letters at the start of a sentence - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION = 0x4, - /** - * prefer lowercase letters - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE = 0x8, - /** - * prefer uppercase letters - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE = 0x10, - /** - * prefer casing for titles and headings (can be language dependent) - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE = 0x20, - /** - * characters should be hidden - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT = 0x40, - /** - * typed text should not be stored - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA = 0x80, - /** - * just Latin characters should be entered - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_LATIN = 0x100, - /** - * the text input is multiline - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE = 0x200, - /** - * an on-screen way to fill in the input is already provided by the client - * @since 2 - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_ON_SCREEN_INPUT_PROVIDED = 0x400, - /** - * prefer not offering emoji support - * @since 2 - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_NO_EMOJI = 0x800, - /** - * the text input will display preedit text in place - * @since 2 - */ ZWP_TEXT_INPUT_V3_CONTENT_HINT_PREEDIT_SHOWN = 0x1000, }; -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_CONTENT_HINT_ON_SCREEN_INPUT_PROVIDED_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_CONTENT_HINT_NO_EMOJI_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_CONTENT_HINT_PREEDIT_SHOWN_SINCE_VERSION 2 -#endif /* ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM */ +#endif #ifndef ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM_IS_VALID #define ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM_IS_VALID -/** - * @ingroup iface_zwp_text_input_v3 - * Validate a zwp_text_input_v3 content_hint value. - * - * @return true on success, false on error. - * @ref zwp_text_input_v3_content_hint - */ static inline bool zwp_text_input_v3_content_hint_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -327,89 +163,30 @@ zwp_text_input_v3_content_hint_is_valid(uint32_t value, uint32_t version) { valid |= ZWP_TEXT_INPUT_V3_CONTENT_HINT_PREEDIT_SHOWN; return (value & ~valid) == 0; } -#endif /* ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM_IS_VALID */ +#endif #ifndef ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM #define ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM -/** - * @ingroup iface_zwp_text_input_v3 - * content purpose - * - * The content purpose allows to specify the primary purpose of a text - * input. - * - * This allows an input method to show special purpose input panels with - * extra characters or to disallow some characters. - */ enum zwp_text_input_v3_content_purpose { - /** - * default input, allowing all characters - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL = 0, - /** - * allow only alphabetic characters - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA = 1, - /** - * allow only digits - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS = 2, - /** - * input a number (including decimal separator and sign) - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER = 3, - /** - * input a phone number - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE = 4, - /** - * input an URL - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL = 5, - /** - * input an email address - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL = 6, - /** - * input a name of a person - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME = 7, - /** - * input a password (combine with sensitive_data hint) - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD = 8, - /** - * input is a numeric password (combine with sensitive_data hint) - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN = 9, - /** - * input a date - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE = 10, - /** - * input a time - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME = 11, - /** - * input a date and time - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME = 12, - /** - * input for a terminal - */ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL = 13, }; -#endif /* ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM */ +#endif #ifndef ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM_IS_VALID #define ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM_IS_VALID -/** - * @ingroup iface_zwp_text_input_v3 - * Validate a zwp_text_input_v3 content_purpose value. - * - * @return true on success, false on error. - * @ref zwp_text_input_v3_content_purpose - */ static inline bool zwp_text_input_v3_content_purpose_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -445,27 +222,17 @@ zwp_text_input_v3_content_purpose_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM_IS_VALID */ +#endif #ifndef ZWP_TEXT_INPUT_V3_ERROR_ENUM #define ZWP_TEXT_INPUT_V3_ERROR_ENUM enum zwp_text_input_v3_error { - /** - * an invalid or duplicate action was specified - */ ZWP_TEXT_INPUT_V3_ERROR_INVALID_ACTION = 0, }; -#endif /* ZWP_TEXT_INPUT_V3_ERROR_ENUM */ +#endif #ifndef ZWP_TEXT_INPUT_V3_ERROR_ENUM_IS_VALID #define ZWP_TEXT_INPUT_V3_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_zwp_text_input_v3 - * Validate a zwp_text_input_v3 error value. - * - * @return true on success, false on error. - * @ref zwp_text_input_v3_error - */ static inline bool zwp_text_input_v3_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -475,40 +242,18 @@ zwp_text_input_v3_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_TEXT_INPUT_V3_ERROR_ENUM_IS_VALID */ +#endif #ifndef ZWP_TEXT_INPUT_V3_ACTION_ENUM #define ZWP_TEXT_INPUT_V3_ACTION_ENUM -/** - * @ingroup iface_zwp_text_input_v3 - * action - * - * A possible action to perform on a text input. - * - * The submit action is intended for input entries that expect some sort of - * activation after user interaction, e.g. the URL entry in a browser. - */ enum zwp_text_input_v3_action { - /** - * no action - */ ZWP_TEXT_INPUT_V3_ACTION_NONE = 0, - /** - * the action is submitted - */ ZWP_TEXT_INPUT_V3_ACTION_SUBMIT = 1, }; -#endif /* ZWP_TEXT_INPUT_V3_ACTION_ENUM */ +#endif #ifndef ZWP_TEXT_INPUT_V3_ACTION_ENUM_IS_VALID #define ZWP_TEXT_INPUT_V3_ACTION_ENUM_IS_VALID -/** - * @ingroup iface_zwp_text_input_v3 - * Validate a zwp_text_input_v3 action value. - * - * @return true on success, false on error. - * @ref zwp_text_input_v3_action - */ static inline bool zwp_text_input_v3_action_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -520,57 +265,23 @@ zwp_text_input_v3_action_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_TEXT_INPUT_V3_ACTION_ENUM_IS_VALID */ +#endif #ifndef ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM #define ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM -/** - * @ingroup iface_zwp_text_input_v3 - * preedit style hint - * - * Style hints for the preedit string. - */ enum zwp_text_input_v3_preedit_hint { - /** - * simple pre-edit text style, typically underlined - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_WHOLE = 1, - /** - * hint for a selected piece of text, e.g. per-character navigation and composition - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SELECTION = 2, - /** - * predicted text, not typed by the user - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_PREDICTION = 3, - /** - * prefixed text not being currently edited, e.g. prior to a 'selection' section - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_PREFIX = 4, - /** - * suffixed text not being currently edited, e.g. after a 'selection' section - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SUFFIX = 5, - /** - * spelling error - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SPELLING_ERROR = 6, - /** - * wrong composition, e.g. user input that can not be transliterated - */ ZWP_TEXT_INPUT_V3_PREEDIT_HINT_COMPOSE_ERROR = 7, }; -#endif /* ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM */ +#endif #ifndef ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM_IS_VALID #define ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM_IS_VALID -/** - * @ingroup iface_zwp_text_input_v3 - * Validate a zwp_text_input_v3 preedit_hint value. - * - * @return true on success, false on error. - * @ref zwp_text_input_v3_preedit_hint - */ static inline bool zwp_text_input_v3_preedit_hint_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -592,269 +303,40 @@ zwp_text_input_v3_preedit_hint_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWP_TEXT_INPUT_V3_PREEDIT_HINT_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_zwp_text_input_v3 - * @struct zwp_text_input_v3_interface - */ struct zwp_text_input_v3_interface { - /** - * Destroy the wp_text_input - * - * Destroy the wp_text_input object. Also disables all surfaces - * enabled through this wp_text_input object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * Request text input to be enabled - * - * Requests text input on the surface previously obtained from - * the enter event. - * - * This request must be issued every time the focused text input - * changes to a new one, including within the current surface. Use - * zwp_text_input_v3.disable when there is no longer any input - * focus on the current surface. - * - * Clients must not enable more than one text input on the single - * seat and should disable the current text input before enabling - * the new one. Requests to enable a text input when another text - * input is enabled on the same seat must be ignored by compositor. - * - * This request resets all state associated with previous enable, - * disable, set_surrounding_text, set_text_change_cause, - * set_content_type, and set_cursor_rectangle requests, as well as - * the state associated with preedit_string, commit_string, and - * delete_surrounding_text events. - * - * The set_surrounding_text, set_content_type and - * set_cursor_rectangle requests must follow if the text input - * supports the necessary functionality. - * - * State set with this request is double-buffered. It will get - * applied on the next zwp_text_input_v3.commit request, and stay - * valid until the next committed enable or disable request. - * - * The changes must be applied by the compositor after issuing a - * zwp_text_input_v3.commit request. - */ void (*enable)(struct wl_client *client, struct wl_resource *resource); - /** - * Disable text input on a surface - * - * Explicitly disable text input on the current surface - * (typically when there is no focus on any text entry inside the - * surface). - * - * State set with this request is double-buffered. It will get - * applied on the next zwp_text_input_v3.commit request. - */ void (*disable)(struct wl_client *client, struct wl_resource *resource); - /** - * sets the surrounding text - * - * Sets the surrounding plain text around the input, excluding - * the preedit text. - * - * The client should notify the compositor of any changes in any of - * the values carried with this request, including changes caused - * by handling incoming text-input events as well as changes caused - * by other mechanisms like keyboard typing. - * - * If the client is unaware of the text around the cursor, it - * should not issue this request, to signify lack of support to the - * compositor. - * - * Text is UTF-8 encoded, and should include the cursor position, - * the complete selection and additional characters before and - * after them. There is a maximum length of wayland messages, so - * text can not be longer than 4000 bytes. - * - * Cursor is the byte offset of the cursor within text buffer. - * - * Anchor is the byte offset of the selection anchor within text - * buffer. If there is no selected text, anchor is the same as - * cursor. - * - * If any preedit text is present, it is replaced with a cursor for - * the purpose of this event. - * - * Values set with this request are double-buffered. They will get - * applied on the next zwp_text_input_v3.commit request, and stay - * valid until the next committed enable or disable request. - * - * The initial state for affected fields is empty, meaning that the - * text input does not support sending surrounding text. If the - * empty values get applied, subsequent attempts to change them may - * have no effect. - */ void (*set_surrounding_text)(struct wl_client *client, struct wl_resource *resource, const char *text, int32_t cursor, int32_t anchor); - /** - * indicates the cause of surrounding text change - * - * Tells the compositor why the text surrounding the cursor - * changed. - * - * Whenever the client detects an external change in text, cursor, - * or anchor posision, it must issue this request to the - * compositor. This request is intended to give the input method a - * chance to update the preedit text in an appropriate way, e.g. by - * removing it when the user starts typing with a keyboard. - * - * cause describes the source of the change. - * - * The value set with this request is double-buffered. It must be - * applied and reset to initial at the next - * zwp_text_input_v3.commit request. - * - * The initial value of cause is input_method. - */ void (*set_text_change_cause)(struct wl_client *client, struct wl_resource *resource, uint32_t cause); - /** - * set content purpose and hint - * - * Sets the content purpose and content hint. While the purpose - * is the basic purpose of an input field, the hint flags allow to - * modify some of the behavior. - * - * Values set with this request are double-buffered. They will get - * applied on the next zwp_text_input_v3.commit request. Subsequent - * attempts to update them may have no effect. The values remain - * valid until the next committed enable or disable request. - * - * The initial value for hint is none, and the initial value for - * purpose is normal. - */ void (*set_content_type)(struct wl_client *client, struct wl_resource *resource, uint32_t hint, uint32_t purpose); - /** - * set cursor position - * - * Marks an area around the cursor as a x, y, width, height - * rectangle in surface local coordinates. - * - * Allows the compositor to put a window with word suggestions near - * the cursor, without obstructing the text being input. - * - * If the client is unaware of the position of edited text, it - * should not issue this request, to signify lack of support to the - * compositor. - * - * Values set with this request are double-buffered. They will get - * applied on the next zwp_text_input_v3.commit request, and stay - * valid until the next committed enable or disable request. - * - * The initial values describing a cursor rectangle are empty. That - * means the text input does not support describing the cursor - * area. If the empty values get applied, subsequent attempts to - * change them may have no effect. - * - * As of version 2, the zwp_text_input_v3.commit request does not - * apply values sent with this request. Instead, it stores them in - * a separate "committed" area. The committed values, if still - * valid, get applied on the next wl_surface.commit request on the - * surface with text-input focus. Both committed and applied values - * get invalidated on: - * - * - the next committed enable or disable request, or - a change of - * the focused surface of the text-input (leave or enter events). - * - * This double stage application allows the compositor to position - * the input method popup in the same frame as the contents of the - * text on the surface are updated. - */ void (*set_cursor_rectangle)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * commit state - * - * Atomically applies state changes recently sent to the - * compositor. - * - * The commit request establishes and updates the state of the - * client, and must be issued after any changes to apply them. - * - * Text input state (enabled status, content purpose, content hint, - * surrounding text and change cause, cursor rectangle) is - * conceptually double-buffered within the context of a text input, - * i.e. between a committed enable request and the following - * committed enable or disable request. - * - * Protocol requests modify the pending state, as opposed to the - * current state in use by the input method. A commit request - * atomically applies all pending state, replacing the current - * state. After commit, the new pending state is as documented for - * each related request. - * - * Requests are applied in the order of arrival. - * - * Neither current nor pending state are modified unless noted - * otherwise. - * - * The compositor must count the number of commit requests coming - * from each zwp_text_input_v3 object and use the count as the - * serial in done events. - */ void (*commit)(struct wl_client *client, struct wl_resource *resource); - /** - * set the available actions - * - * Set the actions available for this text input. - * - * Values set with this request are double-buffered. They will get - * applied on the next zwp_text_input_v3.commit request. - * - * If the available_actions array contains the none action, or - * contains the same action multiple times, the compositor must - * raise the invalid_action protocol error. - * - * Initially, no actions are available. - * @param available_actions available actions - * @since 2 - */ void (*set_available_actions)(struct wl_client *client, struct wl_resource *resource, struct wl_array *available_actions); - /** - * show input panel - * - * Requests an input panel to be shown (e.g. a on-screen - * keyboard). - * - * This request only hints the desired interaction pattern from the - * client side, and its effect may be ignored by compositors given - * other environmental factors. Repeated calls will be ignored. - * @since 2 - */ void (*show_input_panel)(struct wl_client *client, struct wl_resource *resource); - /** - * hide input panel - * - * Requests an input panel to be hidden. - * - * This request only hints the desired interaction pattern from the - * client side, and its effect may be ignored by compositors given - * other environmental factors. Repeated calls will be ignored. - * @since 2 - */ void (*hide_input_panel)(struct wl_client *client, struct wl_resource *resource); }; @@ -869,212 +351,85 @@ struct zwp_text_input_v3_interface { #define ZWP_TEXT_INPUT_V3_LANGUAGE 7 #define ZWP_TEXT_INPUT_V3_PREEDIT_HINT 8 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_PREEDIT_STRING_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_COMMIT_STRING_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_DELETE_SURROUNDING_TEXT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_ACTION_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_LANGUAGE_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_PREEDIT_HINT_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_ENABLE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_DISABLE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_COMMIT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_SET_AVAILABLE_ACTIONS_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_SHOW_INPUT_PANEL_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - */ #define ZWP_TEXT_INPUT_V3_HIDE_INPUT_PANEL_SINCE_VERSION 2 -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an enter event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_text_input_v3_send_enter(struct wl_resource *resource_, struct wl_resource *surface) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_ENTER, surface); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an leave event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_text_input_v3_send_leave(struct wl_resource *resource_, struct wl_resource *surface) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_LEAVE, surface); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an preedit_string event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_text_input_v3_send_preedit_string(struct wl_resource *resource_, const char *text, int32_t cursor_begin, int32_t cursor_end) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_PREEDIT_STRING, text, cursor_begin, cursor_end); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an commit_string event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_text_input_v3_send_commit_string(struct wl_resource *resource_, const char *text) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_COMMIT_STRING, text); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an delete_surrounding_text event to the client owning the resource. - * @param resource_ The client's resource - * @param before_length length of text before current cursor position - * @param after_length length of text after current cursor position - */ static inline void zwp_text_input_v3_send_delete_surrounding_text(struct wl_resource *resource_, uint32_t before_length, uint32_t after_length) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_DELETE_SURROUNDING_TEXT, before_length, after_length); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwp_text_input_v3_send_done(struct wl_resource *resource_, uint32_t serial) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_DONE, serial); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an action event to the client owning the resource. - * @param resource_ The client's resource - * @param action action performed - * @param serial serial number of the action event - */ static inline void zwp_text_input_v3_send_action(struct wl_resource *resource_, uint32_t action, uint32_t serial) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_ACTION, action, serial); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an language event to the client owning the resource. - * @param resource_ The client's resource - * @param language new language set by IME - */ static inline void zwp_text_input_v3_send_language(struct wl_resource *resource_, const char *language) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_LANGUAGE, language); } -/** - * @ingroup iface_zwp_text_input_v3 - * Sends an preedit_hint event to the client owning the resource. - * @param resource_ The client's resource - * @param start starting point of the affected substring - * @param end end point of the affected substring - * @param hint hint to apply - */ static inline void zwp_text_input_v3_send_preedit_hint(struct wl_resource *resource_, uint32_t start, uint32_t end, uint32_t hint) { wl_resource_post_event(resource_, ZWP_TEXT_INPUT_V3_PREEDIT_HINT, start, end, hint); } -/** - * @ingroup iface_zwp_text_input_manager_v3 - * @struct zwp_text_input_manager_v3_interface - */ struct zwp_text_input_manager_v3_interface { - /** - * Destroy the wp_text_input_manager - * - * Destroy the wp_text_input_manager object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * create a new text input object - * - * Creates a new text-input object for a given seat. - */ void (*get_text_input)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -1082,13 +437,7 @@ struct zwp_text_input_manager_v3_interface { }; -/** - * @ingroup iface_zwp_text_input_manager_v3 - */ #define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_manager_v3 - */ #define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT_SINCE_VERSION 1 #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/generated/viewporter-protocol.c b/app/src/main/cpp/waylandcomp/generated/viewporter-protocol.c index d6aaa71e7..8efb3ee3d 100644 --- a/app/src/main/cpp/waylandcomp/generated/viewporter-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/viewporter-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2013-2016 Collabora, Ltd. * @@ -29,7 +27,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/viewporter-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/viewporter-server-protocol.h index 51957565c..9495136e6 100644 --- a/app/src/main/cpp/waylandcomp/generated/viewporter-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/viewporter-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef VIEWPORTER_SERVER_PROTOCOL_H #define VIEWPORTER_SERVER_PROTOCOL_H @@ -14,13 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_viewporter The viewporter protocol - * @section page_ifaces_viewporter Interfaces - * - @subpage page_iface_wp_viewporter - surface cropping and scaling - * - @subpage page_iface_wp_viewport - crop and scale interface to a wl_surface +/* * @section page_copyright_viewporter Copyright - *
  *
  * Copyright © 2013-2016 Collabora, Ltd.
  *
@@ -42,184 +36,35 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + struct wl_surface; struct wp_viewport; struct wp_viewporter; #ifndef WP_VIEWPORTER_INTERFACE #define WP_VIEWPORTER_INTERFACE -/** - * @page page_iface_wp_viewporter wp_viewporter - * @section page_iface_wp_viewporter_desc Description - * - * The global interface exposing surface cropping and scaling - * capabilities is used to instantiate an interface extension for a - * wl_surface object. This extended interface will then allow - * cropping and scaling the surface contents, effectively - * disconnecting the direct relationship between the buffer and the - * surface size. - * @section page_iface_wp_viewporter_api API - * See @ref iface_wp_viewporter. - */ -/** - * @defgroup iface_wp_viewporter The wp_viewporter interface - * - * The global interface exposing surface cropping and scaling - * capabilities is used to instantiate an interface extension for a - * wl_surface object. This extended interface will then allow - * cropping and scaling the surface contents, effectively - * disconnecting the direct relationship between the buffer and the - * surface size. - */ extern const struct wl_interface wp_viewporter_interface; #endif #ifndef WP_VIEWPORT_INTERFACE #define WP_VIEWPORT_INTERFACE -/** - * @page page_iface_wp_viewport wp_viewport - * @section page_iface_wp_viewport_desc Description - * - * An additional interface to a wl_surface object, which allows the - * client to specify the cropping and scaling of the surface - * contents. - * - * This interface works with two concepts: the source rectangle (src_x, - * src_y, src_width, src_height), and the destination size (dst_width, - * dst_height). The contents of the source rectangle are scaled to the - * destination size, and content outside the source rectangle is ignored. - * This state is double-buffered, and is applied on the next - * wl_surface.commit. - * - * The two parts of crop and scale state are independent: the source - * rectangle, and the destination size. Initially both are unset, that - * is, no scaling is applied. The whole of the current wl_buffer is - * used as the source, and the surface size is as defined in - * wl_surface.attach. - * - * If the destination size is set, it causes the surface size to become - * dst_width, dst_height. The source (rectangle) is scaled to exactly - * this size. This overrides whatever the attached wl_buffer size is, - * unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface - * has no content and therefore no size. Otherwise, the size is always - * at least 1x1 in surface local coordinates. - * - * If the source rectangle is set, it defines what area of the wl_buffer is - * taken as the source. If the source rectangle is set and the destination - * size is not set, then src_width and src_height must be integers, and the - * surface size becomes the source rectangle size. This results in cropping - * without scaling. If src_width or src_height are not integers and - * destination size is not set, the bad_size protocol error is raised when - * the surface state is applied. - * - * The coordinate transformations from buffer pixel coordinates up to - * the surface-local coordinates happen in the following order: - * 1. buffer_transform (wl_surface.set_buffer_transform) - * 2. buffer_scale (wl_surface.set_buffer_scale) - * 3. crop and scale (wp_viewport.set*) - * This means, that the source rectangle coordinates of crop and scale - * are given in the coordinates after the buffer transform and scale, - * i.e. in the coordinates that would be the surface-local coordinates - * if the crop and scale was not applied. - * - * If src_x or src_y are negative, the bad_value protocol error is raised. - * Otherwise, if the source rectangle is partially or completely outside of - * the non-NULL wl_buffer, then the out_of_buffer protocol error is raised - * when the surface state is applied. A NULL wl_buffer does not raise the - * out_of_buffer error. - * - * If the wl_surface associated with the wp_viewport is destroyed, - * all wp_viewport requests except 'destroy' raise the protocol error - * no_surface. - * - * If the wp_viewport object is destroyed, the crop and scale - * state is removed from the wl_surface. The change will be applied - * on the next wl_surface.commit. - * @section page_iface_wp_viewport_api API - * See @ref iface_wp_viewport. - */ -/** - * @defgroup iface_wp_viewport The wp_viewport interface - * - * An additional interface to a wl_surface object, which allows the - * client to specify the cropping and scaling of the surface - * contents. - * - * This interface works with two concepts: the source rectangle (src_x, - * src_y, src_width, src_height), and the destination size (dst_width, - * dst_height). The contents of the source rectangle are scaled to the - * destination size, and content outside the source rectangle is ignored. - * This state is double-buffered, and is applied on the next - * wl_surface.commit. - * - * The two parts of crop and scale state are independent: the source - * rectangle, and the destination size. Initially both are unset, that - * is, no scaling is applied. The whole of the current wl_buffer is - * used as the source, and the surface size is as defined in - * wl_surface.attach. - * - * If the destination size is set, it causes the surface size to become - * dst_width, dst_height. The source (rectangle) is scaled to exactly - * this size. This overrides whatever the attached wl_buffer size is, - * unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface - * has no content and therefore no size. Otherwise, the size is always - * at least 1x1 in surface local coordinates. - * - * If the source rectangle is set, it defines what area of the wl_buffer is - * taken as the source. If the source rectangle is set and the destination - * size is not set, then src_width and src_height must be integers, and the - * surface size becomes the source rectangle size. This results in cropping - * without scaling. If src_width or src_height are not integers and - * destination size is not set, the bad_size protocol error is raised when - * the surface state is applied. - * - * The coordinate transformations from buffer pixel coordinates up to - * the surface-local coordinates happen in the following order: - * 1. buffer_transform (wl_surface.set_buffer_transform) - * 2. buffer_scale (wl_surface.set_buffer_scale) - * 3. crop and scale (wp_viewport.set*) - * This means, that the source rectangle coordinates of crop and scale - * are given in the coordinates after the buffer transform and scale, - * i.e. in the coordinates that would be the surface-local coordinates - * if the crop and scale was not applied. - * - * If src_x or src_y are negative, the bad_value protocol error is raised. - * Otherwise, if the source rectangle is partially or completely outside of - * the non-NULL wl_buffer, then the out_of_buffer protocol error is raised - * when the surface state is applied. A NULL wl_buffer does not raise the - * out_of_buffer error. - * - * If the wl_surface associated with the wp_viewport is destroyed, - * all wp_viewport requests except 'destroy' raise the protocol error - * no_surface. - * - * If the wp_viewport object is destroyed, the crop and scale - * state is removed from the wl_surface. The change will be applied - * on the next wl_surface.commit. - */ extern const struct wl_interface wp_viewport_interface; #endif #ifndef WP_VIEWPORTER_ERROR_ENUM #define WP_VIEWPORTER_ERROR_ENUM enum wp_viewporter_error { - /** - * the surface already has a viewport object associated - */ WP_VIEWPORTER_ERROR_VIEWPORT_EXISTS = 0, }; -#endif /* WP_VIEWPORTER_ERROR_ENUM */ +#endif #ifndef WP_VIEWPORTER_ERROR_ENUM_IS_VALID #define WP_VIEWPORTER_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_viewporter - * Validate a wp_viewporter error value. - * - * @return true on success, false on error. - * @ref wp_viewporter_error - */ static inline bool wp_viewporter_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -229,32 +74,11 @@ wp_viewporter_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_VIEWPORTER_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_viewporter - * @struct wp_viewporter_interface - */ struct wp_viewporter_interface { - /** - * unbind from the cropping and scaling interface - * - * Informs the server that the client will not be using this - * protocol object anymore. This does not affect any other objects, - * wp_viewport objects included. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * extend surface interface for crop and scale - * - * Instantiate an interface extension for the given wl_surface to - * crop and scale its content. If the given wl_surface already has - * a wp_viewport object associated, the viewport_exists protocol - * error is raised. - * @param id the new viewport interface id - * @param surface the surface - */ void (*get_viewport)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -262,46 +86,21 @@ struct wp_viewporter_interface { }; -/** - * @ingroup iface_wp_viewporter - */ #define WP_VIEWPORTER_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_viewporter - */ #define WP_VIEWPORTER_GET_VIEWPORT_SINCE_VERSION 1 #ifndef WP_VIEWPORT_ERROR_ENUM #define WP_VIEWPORT_ERROR_ENUM enum wp_viewport_error { - /** - * negative or zero values in width or height - */ WP_VIEWPORT_ERROR_BAD_VALUE = 0, - /** - * destination size is not integer - */ WP_VIEWPORT_ERROR_BAD_SIZE = 1, - /** - * source rectangle extends outside of the content area - */ WP_VIEWPORT_ERROR_OUT_OF_BUFFER = 2, - /** - * the wl_surface was destroyed - */ WP_VIEWPORT_ERROR_NO_SURFACE = 3, }; -#endif /* WP_VIEWPORT_ERROR_ENUM */ +#endif #ifndef WP_VIEWPORT_ERROR_ENUM_IS_VALID #define WP_VIEWPORT_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wp_viewport - * Validate a wp_viewport error value. - * - * @return true on success, false on error. - * @ref wp_viewport_error - */ static inline bool wp_viewport_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -317,63 +116,17 @@ wp_viewport_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WP_VIEWPORT_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wp_viewport - * @struct wp_viewport_interface - */ struct wp_viewport_interface { - /** - * remove scaling and cropping from the surface - * - * The associated wl_surface's crop and scale state is removed. - * The change is applied on the next wl_surface.commit. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the source rectangle for cropping - * - * Set the source rectangle of the associated wl_surface. See - * wp_viewport for the description, and relation to the wl_buffer - * size. - * - * If all of x, y, width and height are -1.0, the source rectangle - * is unset instead. Any other set of values where width or height - * are zero or negative, or x or y are negative, raise the - * bad_value protocol error. - * - * The crop and scale state is double-buffered state, and will be - * applied on the next wl_surface.commit. - * @param x source rectangle x - * @param y source rectangle y - * @param width source rectangle width - * @param height source rectangle height - */ void (*set_source)(struct wl_client *client, struct wl_resource *resource, wl_fixed_t x, wl_fixed_t y, wl_fixed_t width, wl_fixed_t height); - /** - * set the surface size for scaling - * - * Set the destination size of the associated wl_surface. See - * wp_viewport for the description, and relation to the wl_buffer - * size. - * - * If width is -1 and height is -1, the destination size is unset - * instead. Any other pair of values for width and height that - * contains zero or negative values raises the bad_value protocol - * error. - * - * The crop and scale state is double-buffered state, and will be - * applied on the next wl_surface.commit. - * @param width surface width - * @param height surface height - */ void (*set_destination)(struct wl_client *client, struct wl_resource *resource, int32_t width, @@ -381,17 +134,8 @@ struct wp_viewport_interface { }; -/** - * @ingroup iface_wp_viewport - */ #define WP_VIEWPORT_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_viewport - */ #define WP_VIEWPORT_SET_SOURCE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_viewport - */ #define WP_VIEWPORT_SET_DESTINATION_SINCE_VERSION 1 #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-protocol.c index 4bb2945af..8e8cafaf3 100644 --- a/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2018 Simon Ser * Copyright © 2019 Ivan Molodetskikh @@ -32,7 +30,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-server-protocol.h index a48487584..f8d847ce6 100644 --- a/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/wlr-data-control-unstable-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef WLR_DATA_CONTROL_UNSTABLE_V1_SERVER_PROTOCOL_H #define WLR_DATA_CONTROL_UNSTABLE_V1_SERVER_PROTOCOL_H @@ -14,35 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_wlr_data_control_unstable_v1 The wlr_data_control_unstable_v1 protocol - * control data devices - * - * @section page_desc_wlr_data_control_unstable_v1 Description - * - * This protocol allows a privileged client to control data devices. In - * particular, the client will be able to manage the current selection and take - * the role of a clipboard manager. - * - * Warning! The protocol described in this file is experimental and - * backward incompatible changes may be made. Backward compatible changes - * may be added together with the corresponding interface version bump. - * Backward incompatible changes are done by bumping the version number in - * the protocol and interface names and resetting the interface version. - * Once the protocol is to be declared stable, the 'z' prefix and the - * version number in the protocol and interface names are removed and the - * interface version number is reset. - * - * Note! This protocol is deprecated and not intended for production use. - * For clipboard management, use the ext-data-control-v1 protocol. - * - * @section page_ifaces_wlr_data_control_unstable_v1 Interfaces - * - @subpage page_iface_zwlr_data_control_manager_v1 - manager to control data devices - * - @subpage page_iface_zwlr_data_control_device_v1 - manage a data device for a seat - * - @subpage page_iface_zwlr_data_control_source_v1 - offer to transfer data - * - @subpage page_iface_zwlr_data_control_offer_v1 - offer to transfer data +/* * @section page_copyright_wlr_data_control_unstable_v1 Copyright - *
  *
  * Copyright © 2018 Simon Ser
  * Copyright © 2019 Ivan Molodetskikh
@@ -67,8 +39,35 @@ struct wl_resource;
  * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
  * THIS SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_seat; struct zwlr_data_control_device_v1; struct zwlr_data_control_manager_v1; @@ -77,158 +76,47 @@ struct zwlr_data_control_source_v1; #ifndef ZWLR_DATA_CONTROL_MANAGER_V1_INTERFACE #define ZWLR_DATA_CONTROL_MANAGER_V1_INTERFACE -/** - * @page page_iface_zwlr_data_control_manager_v1 zwlr_data_control_manager_v1 - * @section page_iface_zwlr_data_control_manager_v1_desc Description - * - * This interface is a manager that allows creating per-seat data device - * controls. - * @section page_iface_zwlr_data_control_manager_v1_api API - * See @ref iface_zwlr_data_control_manager_v1. - */ -/** - * @defgroup iface_zwlr_data_control_manager_v1 The zwlr_data_control_manager_v1 interface - * - * This interface is a manager that allows creating per-seat data device - * controls. - */ extern const struct wl_interface zwlr_data_control_manager_v1_interface; #endif #ifndef ZWLR_DATA_CONTROL_DEVICE_V1_INTERFACE #define ZWLR_DATA_CONTROL_DEVICE_V1_INTERFACE -/** - * @page page_iface_zwlr_data_control_device_v1 zwlr_data_control_device_v1 - * @section page_iface_zwlr_data_control_device_v1_desc Description - * - * This interface allows a client to manage a seat's selection. - * - * When the seat is destroyed, this object becomes inert. - * @section page_iface_zwlr_data_control_device_v1_api API - * See @ref iface_zwlr_data_control_device_v1. - */ -/** - * @defgroup iface_zwlr_data_control_device_v1 The zwlr_data_control_device_v1 interface - * - * This interface allows a client to manage a seat's selection. - * - * When the seat is destroyed, this object becomes inert. - */ extern const struct wl_interface zwlr_data_control_device_v1_interface; #endif #ifndef ZWLR_DATA_CONTROL_SOURCE_V1_INTERFACE #define ZWLR_DATA_CONTROL_SOURCE_V1_INTERFACE -/** - * @page page_iface_zwlr_data_control_source_v1 zwlr_data_control_source_v1 - * @section page_iface_zwlr_data_control_source_v1_desc Description - * - * The wlr_data_control_source object is the source side of a - * wlr_data_control_offer. It is created by the source client in a data - * transfer and provides a way to describe the offered data and a way to - * respond to requests to transfer the data. - * @section page_iface_zwlr_data_control_source_v1_api API - * See @ref iface_zwlr_data_control_source_v1. - */ -/** - * @defgroup iface_zwlr_data_control_source_v1 The zwlr_data_control_source_v1 interface - * - * The wlr_data_control_source object is the source side of a - * wlr_data_control_offer. It is created by the source client in a data - * transfer and provides a way to describe the offered data and a way to - * respond to requests to transfer the data. - */ extern const struct wl_interface zwlr_data_control_source_v1_interface; #endif #ifndef ZWLR_DATA_CONTROL_OFFER_V1_INTERFACE #define ZWLR_DATA_CONTROL_OFFER_V1_INTERFACE -/** - * @page page_iface_zwlr_data_control_offer_v1 zwlr_data_control_offer_v1 - * @section page_iface_zwlr_data_control_offer_v1_desc Description - * - * A wlr_data_control_offer represents a piece of data offered for transfer - * by another client (the source client). The offer describes the different - * MIME types that the data can be converted to and provides the mechanism - * for transferring the data directly from the source client. - * @section page_iface_zwlr_data_control_offer_v1_api API - * See @ref iface_zwlr_data_control_offer_v1. - */ -/** - * @defgroup iface_zwlr_data_control_offer_v1 The zwlr_data_control_offer_v1 interface - * - * A wlr_data_control_offer represents a piece of data offered for transfer - * by another client (the source client). The offer describes the different - * MIME types that the data can be converted to and provides the mechanism - * for transferring the data directly from the source client. - */ extern const struct wl_interface zwlr_data_control_offer_v1_interface; #endif -/** - * @ingroup iface_zwlr_data_control_manager_v1 - * @struct zwlr_data_control_manager_v1_interface - */ struct zwlr_data_control_manager_v1_interface { - /** - * create a new data source - * - * Create a new data source. - * @param id data source to create - */ void (*create_data_source)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * get a data device for a seat - * - * Create a data device that can be used to manage a seat's - * selection. - */ void (*get_data_device)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *seat); - /** - * destroy the manager - * - * All objects created by the manager will still remain valid, - * until their appropriate destroy request has been called. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); }; -/** - * @ingroup iface_zwlr_data_control_manager_v1 - */ #define ZWLR_DATA_CONTROL_MANAGER_V1_CREATE_DATA_SOURCE_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_manager_v1 - */ #define ZWLR_DATA_CONTROL_MANAGER_V1_GET_DATA_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_manager_v1 - */ #define ZWLR_DATA_CONTROL_MANAGER_V1_DESTROY_SINCE_VERSION 1 #ifndef ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_ENUM #define ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_ENUM enum zwlr_data_control_device_v1_error { - /** - * source given to set_selection or set_primary_selection was already used before - */ ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_USED_SOURCE = 1, }; -#endif /* ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_ENUM */ +#endif #ifndef ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_ENUM_IS_VALID #define ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_zwlr_data_control_device_v1 - * Validate a zwlr_data_control_device_v1 error value. - * - * @return true on success, false on error. - * @ref zwlr_data_control_device_v1_error - */ static inline bool zwlr_data_control_device_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -238,51 +126,14 @@ zwlr_data_control_device_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWLR_DATA_CONTROL_DEVICE_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_zwlr_data_control_device_v1 - * @struct zwlr_data_control_device_v1_interface - */ struct zwlr_data_control_device_v1_interface { - /** - * copy data to the selection - * - * This request asks the compositor to set the selection to the - * data from the source on behalf of the client. - * - * The given source may not be used in any further set_selection or - * set_primary_selection requests. Attempting to use a previously - * used source is a protocol error. - * - * To unset the selection, set the source to NULL. - */ void (*set_selection)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *source); - /** - * destroy this data device - * - * Destroys the data device object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * copy data to the primary selection - * - * This request asks the compositor to set the primary selection - * to the data from the source on behalf of the client. - * - * The given source may not be used in any further set_selection or - * set_primary_selection requests. Attempting to use a previously - * used source is a protocol error. - * - * To unset the primary selection, set the source to NULL. - * - * The compositor will ignore this request if it does not support - * primary selection. - * @since 2 - */ void (*set_primary_selection)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *source); @@ -293,74 +144,33 @@ struct zwlr_data_control_device_v1_interface { #define ZWLR_DATA_CONTROL_DEVICE_V1_FINISHED 2 #define ZWLR_DATA_CONTROL_DEVICE_V1_PRIMARY_SELECTION 3 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_DATA_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_SELECTION_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_FINISHED_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_PRIMARY_SELECTION_SINCE_VERSION 2 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_SET_SELECTION_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_device_v1 - */ #define ZWLR_DATA_CONTROL_DEVICE_V1_SET_PRIMARY_SELECTION_SINCE_VERSION 2 -/** - * @ingroup iface_zwlr_data_control_device_v1 - * Sends an data_offer event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwlr_data_control_device_v1_send_data_offer(struct wl_resource *resource_, struct wl_resource *id) { wl_resource_post_event(resource_, ZWLR_DATA_CONTROL_DEVICE_V1_DATA_OFFER, id); } -/** - * @ingroup iface_zwlr_data_control_device_v1 - * Sends an selection event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwlr_data_control_device_v1_send_selection(struct wl_resource *resource_, struct wl_resource *id) { wl_resource_post_event(resource_, ZWLR_DATA_CONTROL_DEVICE_V1_SELECTION, id); } -/** - * @ingroup iface_zwlr_data_control_device_v1 - * Sends an finished event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwlr_data_control_device_v1_send_finished(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWLR_DATA_CONTROL_DEVICE_V1_FINISHED); } -/** - * @ingroup iface_zwlr_data_control_device_v1 - * Sends an primary_selection event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwlr_data_control_device_v1_send_primary_selection(struct wl_resource *resource_, struct wl_resource *id) { @@ -370,22 +180,12 @@ zwlr_data_control_device_v1_send_primary_selection(struct wl_resource *resource_ #ifndef ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_ENUM #define ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_ENUM enum zwlr_data_control_source_v1_error { - /** - * offer sent after wlr_data_control_device.set_selection - */ ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_INVALID_OFFER = 1, }; -#endif /* ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_ENUM */ +#endif #ifndef ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_ENUM_IS_VALID #define ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_zwlr_data_control_source_v1 - * Validate a zwlr_data_control_source_v1 error value. - * - * @return true on success, false on error. - * @ref zwlr_data_control_source_v1_error - */ static inline bool zwlr_data_control_source_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -395,32 +195,12 @@ zwlr_data_control_source_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* ZWLR_DATA_CONTROL_SOURCE_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_zwlr_data_control_source_v1 - * @struct zwlr_data_control_source_v1_interface - */ struct zwlr_data_control_source_v1_interface { - /** - * add an offered MIME type - * - * This request adds a MIME type to the set of MIME types - * advertised to targets. Can be called several times to offer - * multiple types. - * - * Calling this after wlr_data_control_device.set_selection is a - * protocol error. - * @param mime_type MIME type offered by the data source - */ void (*offer)(struct wl_client *client, struct wl_resource *resource, const char *mime_type); - /** - * destroy this source - * - * Destroys the data source object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); }; @@ -428,106 +208,40 @@ struct zwlr_data_control_source_v1_interface { #define ZWLR_DATA_CONTROL_SOURCE_V1_SEND 0 #define ZWLR_DATA_CONTROL_SOURCE_V1_CANCELLED 1 -/** - * @ingroup iface_zwlr_data_control_source_v1 - */ #define ZWLR_DATA_CONTROL_SOURCE_V1_SEND_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_source_v1 - */ #define ZWLR_DATA_CONTROL_SOURCE_V1_CANCELLED_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_source_v1 - */ #define ZWLR_DATA_CONTROL_SOURCE_V1_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_source_v1 - */ #define ZWLR_DATA_CONTROL_SOURCE_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_source_v1 - * Sends an send event to the client owning the resource. - * @param resource_ The client's resource - * @param mime_type MIME type for the data - * @param fd file descriptor for the data - */ static inline void zwlr_data_control_source_v1_send_send(struct wl_resource *resource_, const char *mime_type, int32_t fd) { wl_resource_post_event(resource_, ZWLR_DATA_CONTROL_SOURCE_V1_SEND, mime_type, fd); } -/** - * @ingroup iface_zwlr_data_control_source_v1 - * Sends an cancelled event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void zwlr_data_control_source_v1_send_cancelled(struct wl_resource *resource_) { wl_resource_post_event(resource_, ZWLR_DATA_CONTROL_SOURCE_V1_CANCELLED); } -/** - * @ingroup iface_zwlr_data_control_offer_v1 - * @struct zwlr_data_control_offer_v1_interface - */ struct zwlr_data_control_offer_v1_interface { - /** - * request that the data is transferred - * - * To transfer the offered data, the client issues this request - * and indicates the MIME type it wants to receive. The transfer - * happens through the passed file descriptor (typically created - * with the pipe system call). The source client writes the data in - * the MIME type representation requested and then closes the file - * descriptor. - * - * The receiving client reads from the read end of the pipe until - * EOF and then closes its end, at which point the transfer is - * complete. - * - * This request may happen multiple times for different MIME types. - * @param mime_type MIME type desired by receiver - * @param fd file descriptor for data transfer - */ void (*receive)(struct wl_client *client, struct wl_resource *resource, const char *mime_type, int32_t fd); - /** - * destroy this offer - * - * Destroys the data offer object. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); }; #define ZWLR_DATA_CONTROL_OFFER_V1_OFFER 0 -/** - * @ingroup iface_zwlr_data_control_offer_v1 - */ #define ZWLR_DATA_CONTROL_OFFER_V1_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_offer_v1 - */ #define ZWLR_DATA_CONTROL_OFFER_V1_RECEIVE_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_offer_v1 - */ #define ZWLR_DATA_CONTROL_OFFER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwlr_data_control_offer_v1 - * Sends an offer event to the client owning the resource. - * @param resource_ The client's resource - * @param mime_type offered MIME type - */ static inline void zwlr_data_control_offer_v1_send_offer(struct wl_resource *resource_, const char *mime_type) { diff --git a/app/src/main/cpp/waylandcomp/generated/xdg-shell-client-protocol.h b/app/src/main/cpp/waylandcomp/generated/xdg-shell-client-protocol.h index 5280ba38b..c73ba799a 100644 --- a/app/src/main/cpp/waylandcomp/generated/xdg-shell-client-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/xdg-shell-client-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef XDG_SHELL_CLIENT_PROTOCOL_H #define XDG_SHELL_CLIENT_PROTOCOL_H @@ -11,16 +10,8 @@ extern "C" { #endif -/** - * @page page_xdg_shell The xdg_shell protocol - * @section page_ifaces_xdg_shell Interfaces - * - @subpage page_iface_xdg_wm_base - create desktop-style surfaces - * - @subpage page_iface_xdg_positioner - child surface positioner - * - @subpage page_iface_xdg_surface - desktop user interface surface base interface - * - @subpage page_iface_xdg_toplevel - toplevel surface - * - @subpage page_iface_xdg_popup - short-lived, popup surfaces for menus +/* * @section page_copyright_xdg_shell Copyright - *
  *
  * Copyright © 2008-2013 Kristian Høgsberg
  * Copyright © 2013      Rafael Antognolli
@@ -47,8 +38,16 @@ extern "C" {
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + struct wl_output; struct wl_seat; struct wl_surface; @@ -60,366 +59,44 @@ struct xdg_wm_base; #ifndef XDG_WM_BASE_INTERFACE #define XDG_WM_BASE_INTERFACE -/** - * @page page_iface_xdg_wm_base xdg_wm_base - * @section page_iface_xdg_wm_base_desc Description - * - * The xdg_wm_base interface is exposed as a global object enabling clients - * to turn their wl_surfaces into windows in a desktop environment. It - * defines the basic functionality needed for clients and the compositor to - * create windows that can be dragged, resized, maximized, etc, as well as - * creating transient windows such as popup menus. - * @section page_iface_xdg_wm_base_api API - * See @ref iface_xdg_wm_base. - */ -/** - * @defgroup iface_xdg_wm_base The xdg_wm_base interface - * - * The xdg_wm_base interface is exposed as a global object enabling clients - * to turn their wl_surfaces into windows in a desktop environment. It - * defines the basic functionality needed for clients and the compositor to - * create windows that can be dragged, resized, maximized, etc, as well as - * creating transient windows such as popup menus. - */ extern const struct wl_interface xdg_wm_base_interface; #endif #ifndef XDG_POSITIONER_INTERFACE #define XDG_POSITIONER_INTERFACE -/** - * @page page_iface_xdg_positioner xdg_positioner - * @section page_iface_xdg_positioner_desc Description - * - * The xdg_positioner provides a collection of rules for the placement of a - * child surface relative to a parent surface. Rules can be defined to ensure - * the child surface remains within the visible area's borders, and to - * specify how the child surface changes its position, such as sliding along - * an axis, or flipping around a rectangle. These positioner-created rules are - * constrained by the requirement that a child surface must intersect with or - * be at least partially adjacent to its parent surface. - * - * See the various requests for details about possible rules. - * - * At the time of the request, the compositor makes a copy of the rules - * specified by the xdg_positioner. Thus, after the request is complete the - * xdg_positioner object can be destroyed or reused; further changes to the - * object will have no effect on previous usages. - * - * For an xdg_positioner object to be considered complete, it must have a - * non-zero size set by set_size, and a non-zero anchor rectangle set by - * set_anchor_rect. Passing an incomplete xdg_positioner object when - * positioning a surface raises an invalid_positioner error. - * @section page_iface_xdg_positioner_api API - * See @ref iface_xdg_positioner. - */ -/** - * @defgroup iface_xdg_positioner The xdg_positioner interface - * - * The xdg_positioner provides a collection of rules for the placement of a - * child surface relative to a parent surface. Rules can be defined to ensure - * the child surface remains within the visible area's borders, and to - * specify how the child surface changes its position, such as sliding along - * an axis, or flipping around a rectangle. These positioner-created rules are - * constrained by the requirement that a child surface must intersect with or - * be at least partially adjacent to its parent surface. - * - * See the various requests for details about possible rules. - * - * At the time of the request, the compositor makes a copy of the rules - * specified by the xdg_positioner. Thus, after the request is complete the - * xdg_positioner object can be destroyed or reused; further changes to the - * object will have no effect on previous usages. - * - * For an xdg_positioner object to be considered complete, it must have a - * non-zero size set by set_size, and a non-zero anchor rectangle set by - * set_anchor_rect. Passing an incomplete xdg_positioner object when - * positioning a surface raises an invalid_positioner error. - */ extern const struct wl_interface xdg_positioner_interface; #endif #ifndef XDG_SURFACE_INTERFACE #define XDG_SURFACE_INTERFACE -/** - * @page page_iface_xdg_surface xdg_surface - * @section page_iface_xdg_surface_desc Description - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides a base set of functionality required to construct user - * interface elements requiring management by the compositor, such as - * toplevel windows, menus, etc. The types of functionality are split into - * xdg_surface roles. - * - * Creating an xdg_surface does not set the role for a wl_surface. In order - * to map an xdg_surface, the client must create a role-specific object - * using, e.g., get_toplevel, get_popup. The wl_surface for any given - * xdg_surface can have at most one role, and may not be assigned any role - * not based on xdg_surface. - * - * A role must be assigned before any other requests are made to the - * xdg_surface object. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_surface state to take effect. - * - * Creating an xdg_surface from a wl_surface which has a buffer attached or - * committed is a client error, and any attempts by a client to attach or - * manipulate a buffer prior to the first xdg_surface.configure call must - * also be treated as errors. - * - * After creating a role-specific object and setting it up, the client must - * perform an initial commit without any buffer attached. The compositor - * will reply with an xdg_surface.configure event. The client must - * acknowledge it and is then allowed to attach a buffer to map the surface. - * - * Mapping an xdg_surface-based role surface is defined as making it - * possible for the surface to be shown by the compositor. Note that - * a mapped surface is not guaranteed to be visible once it is mapped. - * - * For an xdg_surface to be mapped by the compositor, the following - * conditions must be met: - * (1) the client has assigned an xdg_surface-based role to the surface - * (2) the client has set and committed the xdg_surface state and the - * role-dependent state to the surface - * (3) the client has committed a buffer to the surface - * - * A newly-unmapped surface is considered to have met condition (1) out - * of the 3 required conditions for mapping a surface if its role surface - * has not been destroyed, i.e. the client must perform the initial commit - * again before attaching a buffer. - * @section page_iface_xdg_surface_api API - * See @ref iface_xdg_surface. - */ -/** - * @defgroup iface_xdg_surface The xdg_surface interface - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides a base set of functionality required to construct user - * interface elements requiring management by the compositor, such as - * toplevel windows, menus, etc. The types of functionality are split into - * xdg_surface roles. - * - * Creating an xdg_surface does not set the role for a wl_surface. In order - * to map an xdg_surface, the client must create a role-specific object - * using, e.g., get_toplevel, get_popup. The wl_surface for any given - * xdg_surface can have at most one role, and may not be assigned any role - * not based on xdg_surface. - * - * A role must be assigned before any other requests are made to the - * xdg_surface object. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_surface state to take effect. - * - * Creating an xdg_surface from a wl_surface which has a buffer attached or - * committed is a client error, and any attempts by a client to attach or - * manipulate a buffer prior to the first xdg_surface.configure call must - * also be treated as errors. - * - * After creating a role-specific object and setting it up, the client must - * perform an initial commit without any buffer attached. The compositor - * will reply with an xdg_surface.configure event. The client must - * acknowledge it and is then allowed to attach a buffer to map the surface. - * - * Mapping an xdg_surface-based role surface is defined as making it - * possible for the surface to be shown by the compositor. Note that - * a mapped surface is not guaranteed to be visible once it is mapped. - * - * For an xdg_surface to be mapped by the compositor, the following - * conditions must be met: - * (1) the client has assigned an xdg_surface-based role to the surface - * (2) the client has set and committed the xdg_surface state and the - * role-dependent state to the surface - * (3) the client has committed a buffer to the surface - * - * A newly-unmapped surface is considered to have met condition (1) out - * of the 3 required conditions for mapping a surface if its role surface - * has not been destroyed, i.e. the client must perform the initial commit - * again before attaching a buffer. - */ extern const struct wl_interface xdg_surface_interface; #endif #ifndef XDG_TOPLEVEL_INTERFACE #define XDG_TOPLEVEL_INTERFACE -/** - * @page page_iface_xdg_toplevel xdg_toplevel - * @section page_iface_xdg_toplevel_desc Description - * - * This interface defines an xdg_surface role which allows a surface to, - * among other things, set window-like properties such as maximize, - * fullscreen, and minimize, set application-specific metadata like title and - * id, and well as trigger user interactive operations such as interactive - * resize and move. - * - * Unmapping an xdg_toplevel means that the surface cannot be shown - * by the compositor until it is explicitly mapped again. - * All active operations (e.g., move, resize) are canceled and all - * attributes (e.g. title, state, stacking, ...) are discarded for - * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to - * the state it had right after xdg_surface.get_toplevel. The client - * can re-map the toplevel by performing a commit without any buffer - * attached, waiting for a configure event and handling it as usual (see - * xdg_surface description). - * - * Attaching a null buffer to a toplevel unmaps the surface. - * @section page_iface_xdg_toplevel_api API - * See @ref iface_xdg_toplevel. - */ -/** - * @defgroup iface_xdg_toplevel The xdg_toplevel interface - * - * This interface defines an xdg_surface role which allows a surface to, - * among other things, set window-like properties such as maximize, - * fullscreen, and minimize, set application-specific metadata like title and - * id, and well as trigger user interactive operations such as interactive - * resize and move. - * - * Unmapping an xdg_toplevel means that the surface cannot be shown - * by the compositor until it is explicitly mapped again. - * All active operations (e.g., move, resize) are canceled and all - * attributes (e.g. title, state, stacking, ...) are discarded for - * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to - * the state it had right after xdg_surface.get_toplevel. The client - * can re-map the toplevel by performing a commit without any buffer - * attached, waiting for a configure event and handling it as usual (see - * xdg_surface description). - * - * Attaching a null buffer to a toplevel unmaps the surface. - */ extern const struct wl_interface xdg_toplevel_interface; #endif #ifndef XDG_POPUP_INTERFACE #define XDG_POPUP_INTERFACE -/** - * @page page_iface_xdg_popup xdg_popup - * @section page_iface_xdg_popup_desc Description - * - * A popup surface is a short-lived, temporary surface. It can be used to - * implement for example menus, popovers, tooltips and other similar user - * interface concepts. - * - * A popup can be made to take an explicit grab. See xdg_popup.grab for - * details. - * - * When the popup is dismissed, a popup_done event will be sent out, and at - * the same time the surface will be unmapped. See the xdg_popup.popup_done - * event for details. - * - * Explicitly destroying the xdg_popup object will also dismiss the popup and - * unmap the surface. Clients that want to dismiss the popup when another - * surface of their own is clicked should dismiss the popup using the destroy - * request. - * - * A newly created xdg_popup will be stacked on top of all previously created - * xdg_popup surfaces associated with the same xdg_toplevel. - * - * The parent of an xdg_popup must be mapped (see the xdg_surface - * description) before the xdg_popup itself. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_popup state to take effect. - * @section page_iface_xdg_popup_api API - * See @ref iface_xdg_popup. - */ -/** - * @defgroup iface_xdg_popup The xdg_popup interface - * - * A popup surface is a short-lived, temporary surface. It can be used to - * implement for example menus, popovers, tooltips and other similar user - * interface concepts. - * - * A popup can be made to take an explicit grab. See xdg_popup.grab for - * details. - * - * When the popup is dismissed, a popup_done event will be sent out, and at - * the same time the surface will be unmapped. See the xdg_popup.popup_done - * event for details. - * - * Explicitly destroying the xdg_popup object will also dismiss the popup and - * unmap the surface. Clients that want to dismiss the popup when another - * surface of their own is clicked should dismiss the popup using the destroy - * request. - * - * A newly created xdg_popup will be stacked on top of all previously created - * xdg_popup surfaces associated with the same xdg_toplevel. - * - * The parent of an xdg_popup must be mapped (see the xdg_surface - * description) before the xdg_popup itself. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_popup state to take effect. - */ extern const struct wl_interface xdg_popup_interface; #endif #ifndef XDG_WM_BASE_ERROR_ENUM #define XDG_WM_BASE_ERROR_ENUM enum xdg_wm_base_error { - /** - * given wl_surface has another role - */ XDG_WM_BASE_ERROR_ROLE = 0, - /** - * xdg_wm_base was destroyed before children - */ XDG_WM_BASE_ERROR_DEFUNCT_SURFACES = 1, - /** - * the client tried to map or destroy a non-topmost popup - */ XDG_WM_BASE_ERROR_NOT_THE_TOPMOST_POPUP = 2, - /** - * the client specified an invalid popup parent surface - */ XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT = 3, - /** - * the client provided an invalid surface state - */ XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE = 4, - /** - * the client provided an invalid positioner - */ XDG_WM_BASE_ERROR_INVALID_POSITIONER = 5, - /** - * the client didn’t respond to a ping event in time - */ XDG_WM_BASE_ERROR_UNRESPONSIVE = 6, }; -#endif /* XDG_WM_BASE_ERROR_ENUM */ +#endif -/** - * @ingroup iface_xdg_wm_base - * @struct xdg_wm_base_listener - */ struct xdg_wm_base_listener { - /** - * check if the client is alive - * - * The ping event asks the client if it's still alive. Pass the - * serial specified in the event back to the compositor by sending - * a "pong" request back with the specified serial. See - * xdg_wm_base.pong. - * - * Compositors can use this to determine if the client is still - * alive. It's unspecified what will happen if the client doesn't - * respond to the ping request, or in what timeframe. Clients - * should try to respond in a reasonable amount of time. The - * “unresponsive” error is provided for compositors that wish - * to disconnect unresponsive clients. - * - * A compositor is free to ping in any way it wants, but a client - * must always respond to any xdg_wm_base object it created. - * @param serial pass this to the pong request - */ void (*ping)(void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial); }; -/** - * @ingroup iface_xdg_wm_base - */ static inline int xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base, const struct xdg_wm_base_listener *listener, void *data) @@ -433,36 +110,19 @@ xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base, #define XDG_WM_BASE_GET_XDG_SURFACE 2 #define XDG_WM_BASE_PONG 3 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_PING_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_CREATE_POSITIONER_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_PONG_SINCE_VERSION 1 -/** @ingroup iface_xdg_wm_base */ static inline void xdg_wm_base_set_user_data(struct xdg_wm_base *xdg_wm_base, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) xdg_wm_base, user_data); } -/** @ingroup iface_xdg_wm_base */ static inline void * xdg_wm_base_get_user_data(struct xdg_wm_base *xdg_wm_base) { @@ -475,15 +135,6 @@ xdg_wm_base_get_version(struct xdg_wm_base *xdg_wm_base) return wl_proxy_get_version((struct wl_proxy *) xdg_wm_base); } -/** - * @ingroup iface_xdg_wm_base - * - * Destroy this xdg_wm_base object. - * - * Destroying a bound xdg_wm_base object while there are surfaces - * still alive created by this xdg_wm_base object instance is illegal - * and will result in a defunct_surfaces error. - */ static inline void xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base) { @@ -491,13 +142,6 @@ xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base) XDG_WM_BASE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_xdg_wm_base - * - * Create a positioner object. A positioner object is used to position - * surfaces relative to some parent surface. See the interface description - * and xdg_surface.get_popup for details. - */ static inline struct xdg_positioner * xdg_wm_base_create_positioner(struct xdg_wm_base *xdg_wm_base) { @@ -509,23 +153,6 @@ xdg_wm_base_create_positioner(struct xdg_wm_base *xdg_wm_base) return (struct xdg_positioner *) id; } -/** - * @ingroup iface_xdg_wm_base - * - * This creates an xdg_surface for the given surface. While xdg_surface - * itself is not a role, the corresponding surface may only be assigned - * a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is - * illegal to create an xdg_surface for a wl_surface which already has an - * assigned role and this will result in a role error. - * - * This creates an xdg_surface for the given surface. An xdg_surface is - * used as basis to define a role to a given surface, such as xdg_toplevel - * or xdg_popup. It also manages functionality shared between xdg_surface - * based surface roles. - * - * See the documentation of xdg_surface for more details about what an - * xdg_surface is and how it is used. - */ static inline struct xdg_surface * xdg_wm_base_get_xdg_surface(struct xdg_wm_base *xdg_wm_base, struct wl_surface *surface) { @@ -537,13 +164,6 @@ xdg_wm_base_get_xdg_surface(struct xdg_wm_base *xdg_wm_base, struct wl_surface * return (struct xdg_surface *) id; } -/** - * @ingroup iface_xdg_wm_base - * - * A client must respond to a ping event with a pong request or - * the client may be deemed unresponsive. See xdg_wm_base.ping - * and xdg_wm_base.error.unresponsive. - */ static inline void xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial) { @@ -554,12 +174,9 @@ xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial) #ifndef XDG_POSITIONER_ERROR_ENUM #define XDG_POSITIONER_ERROR_ENUM enum xdg_positioner_error { - /** - * invalid input provided - */ XDG_POSITIONER_ERROR_INVALID_INPUT = 0, }; -#endif /* XDG_POSITIONER_ERROR_ENUM */ +#endif #ifndef XDG_POSITIONER_ANCHOR_ENUM #define XDG_POSITIONER_ANCHOR_ENUM @@ -574,7 +191,7 @@ enum xdg_positioner_anchor { XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7, XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8, }; -#endif /* XDG_POSITIONER_ANCHOR_ENUM */ +#endif #ifndef XDG_POSITIONER_GRAVITY_ENUM #define XDG_POSITIONER_GRAVITY_ENUM @@ -589,115 +206,20 @@ enum xdg_positioner_gravity { XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7, XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8, }; -#endif /* XDG_POSITIONER_GRAVITY_ENUM */ +#endif #ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM #define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM -/** - * @ingroup iface_xdg_positioner - * constraint adjustments - * - * The constraint adjustment value define ways the compositor will adjust - * the position of the surface, if the unadjusted position would result - * in the surface being partly constrained. - * - * Whether a surface is considered 'constrained' is left to the compositor - * to determine. For example, the surface may be partly outside the - * compositor's defined 'work area', thus necessitating the child surface's - * position be adjusted until it is entirely inside the work area. - * - * The adjustments can be combined, according to a defined precedence: 1) - * Flip, 2) Slide, 3) Resize. - */ enum xdg_positioner_constraint_adjustment { - /** - * don't move the child surface when constrained - * - * Don't alter the surface position even if it is constrained on - * some axis, for example partially outside the edge of an output. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0, - /** - * move along the x axis until unconstrained - * - * Slide the surface along the x axis until it is no longer - * constrained. - * - * First try to slide towards the direction of the gravity on the x - * axis until either the edge in the opposite direction of the - * gravity is unconstrained or the edge in the direction of the - * gravity is constrained. - * - * Then try to slide towards the opposite direction of the gravity - * on the x axis until either the edge in the direction of the - * gravity is unconstrained or the edge in the opposite direction - * of the gravity is constrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1, - /** - * move along the y axis until unconstrained - * - * Slide the surface along the y axis until it is no longer - * constrained. - * - * First try to slide towards the direction of the gravity on the y - * axis until either the edge in the opposite direction of the - * gravity is unconstrained or the edge in the direction of the - * gravity is constrained. - * - * Then try to slide towards the opposite direction of the gravity - * on the y axis until either the edge in the direction of the - * gravity is unconstrained or the edge in the opposite direction - * of the gravity is constrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2, - /** - * invert the anchor and gravity on the x axis - * - * Invert the anchor and gravity on the x axis if the surface is - * constrained on the x axis. For example, if the left edge of the - * surface is constrained, the gravity is 'left' and the anchor is - * 'left', change the gravity to 'right' and the anchor to 'right'. - * - * If the adjusted position also ends up being constrained, the - * resulting position of the flip_x adjustment will be the one - * before the adjustment. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4, - /** - * invert the anchor and gravity on the y axis - * - * Invert the anchor and gravity on the y axis if the surface is - * constrained on the y axis. For example, if the bottom edge of - * the surface is constrained, the gravity is 'bottom' and the - * anchor is 'bottom', change the gravity to 'top' and the anchor - * to 'top'. - * - * The adjusted position is calculated given the original anchor - * rectangle and offset, but with the new flipped anchor and - * gravity values. - * - * If the adjusted position also ends up being constrained, the - * resulting position of the flip_y adjustment will be the one - * before the adjustment. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8, - /** - * horizontally resize the surface - * - * Resize the surface horizontally so that it is completely - * unconstrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16, - /** - * vertically resize the surface - * - * Resize the surface vertically so that it is completely - * unconstrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32, }; -#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM */ +#endif #define XDG_POSITIONER_DESTROY 0 #define XDG_POSITIONER_SET_SIZE 1 @@ -711,55 +233,23 @@ enum xdg_positioner_constraint_adjustment { #define XDG_POSITIONER_SET_PARENT_CONFIGURE 9 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_ANCHOR_RECT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_ANCHOR_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_GRAVITY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_OFFSET_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_PARENT_SIZE_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_PARENT_CONFIGURE_SINCE_VERSION 3 -/** @ingroup iface_xdg_positioner */ static inline void xdg_positioner_set_user_data(struct xdg_positioner *xdg_positioner, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) xdg_positioner, user_data); } -/** @ingroup iface_xdg_positioner */ static inline void * xdg_positioner_get_user_data(struct xdg_positioner *xdg_positioner) { @@ -772,11 +262,6 @@ xdg_positioner_get_version(struct xdg_positioner *xdg_positioner) return wl_proxy_get_version((struct wl_proxy *) xdg_positioner); } -/** - * @ingroup iface_xdg_positioner - * - * Notify the compositor that the xdg_positioner will no longer be used. - */ static inline void xdg_positioner_destroy(struct xdg_positioner *xdg_positioner) { @@ -784,15 +269,6 @@ xdg_positioner_destroy(struct xdg_positioner *xdg_positioner) XDG_POSITIONER_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_xdg_positioner - * - * Set the size of the surface that is to be positioned with the positioner - * object. The size is in surface-local coordinates and corresponds to the - * window geometry. See xdg_surface.set_window_geometry. - * - * If a zero or negative size is set the invalid_input error is raised. - */ static inline void xdg_positioner_set_size(struct xdg_positioner *xdg_positioner, int32_t width, int32_t height) { @@ -800,20 +276,6 @@ xdg_positioner_set_size(struct xdg_positioner *xdg_positioner, int32_t width, in XDG_POSITIONER_SET_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, width, height); } -/** - * @ingroup iface_xdg_positioner - * - * Specify the anchor rectangle within the parent surface that the child - * surface will be placed relative to. The rectangle is relative to the - * window geometry as defined by xdg_surface.set_window_geometry of the - * parent surface. - * - * When the xdg_positioner object is used to position a child surface, the - * anchor rectangle may not extend outside the window geometry of the - * positioned child's parent surface. - * - * If a negative size is set the invalid_input error is raised. - */ static inline void xdg_positioner_set_anchor_rect(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y, int32_t width, int32_t height) { @@ -821,16 +283,6 @@ xdg_positioner_set_anchor_rect(struct xdg_positioner *xdg_positioner, int32_t x, XDG_POSITIONER_SET_ANCHOR_RECT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y, width, height); } -/** - * @ingroup iface_xdg_positioner - * - * Defines the anchor point for the anchor rectangle. The specified anchor - * is used derive an anchor point that the child surface will be - * positioned relative to. If a corner anchor is set (e.g. 'top_left' or - * 'bottom_right'), the anchor point will be at the specified corner; - * otherwise, the derived anchor point will be centered on the specified - * edge, or in the center of the anchor rectangle if no edge is specified. - */ static inline void xdg_positioner_set_anchor(struct xdg_positioner *xdg_positioner, uint32_t anchor) { @@ -838,17 +290,6 @@ xdg_positioner_set_anchor(struct xdg_positioner *xdg_positioner, uint32_t anchor XDG_POSITIONER_SET_ANCHOR, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, anchor); } -/** - * @ingroup iface_xdg_positioner - * - * Defines in what direction a surface should be positioned, relative to - * the anchor point of the parent surface. If a corner gravity is - * specified (e.g. 'bottom_right' or 'top_left'), then the child surface - * will be placed towards the specified gravity; otherwise, the child - * surface will be centered over the anchor point on any axis that had no - * gravity specified. If the gravity is not in the ‘gravity’ enum, an - * invalid_input error is raised. - */ static inline void xdg_positioner_set_gravity(struct xdg_positioner *xdg_positioner, uint32_t gravity) { @@ -856,23 +297,6 @@ xdg_positioner_set_gravity(struct xdg_positioner *xdg_positioner, uint32_t gravi XDG_POSITIONER_SET_GRAVITY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, gravity); } -/** - * @ingroup iface_xdg_positioner - * - * Specify how the window should be positioned if the originally intended - * position caused the surface to be constrained, meaning at least - * partially outside positioning boundaries set by the compositor. The - * adjustment is set by constructing a bitmask describing the adjustment to - * be made when the surface is constrained on that axis. - * - * If no bit for one axis is set, the compositor will assume that the child - * surface should not change its position on that axis when constrained. - * - * If more than one bit for one axis is set, the order of how adjustments - * are applied is specified in the corresponding adjustment descriptions. - * - * The default adjustment is none. - */ static inline void xdg_positioner_set_constraint_adjustment(struct xdg_positioner *xdg_positioner, uint32_t constraint_adjustment) { @@ -880,21 +304,6 @@ xdg_positioner_set_constraint_adjustment(struct xdg_positioner *xdg_positioner, XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, constraint_adjustment); } -/** - * @ingroup iface_xdg_positioner - * - * Specify the surface position offset relative to the position of the - * anchor on the anchor rectangle and the anchor on the surface. For - * example if the anchor of the anchor rectangle is at (x, y), the surface - * has the gravity bottom|right, and the offset is (ox, oy), the calculated - * surface position will be (x + ox, y + oy). The offset position of the - * surface is the one used for constraint testing. See - * set_constraint_adjustment. - * - * An example use case is placing a popup menu on top of a user interface - * element, while aligning the user interface element of the parent surface - * with some user interface element placed somewhere in the popup surface. - */ static inline void xdg_positioner_set_offset(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y) { @@ -902,16 +311,6 @@ xdg_positioner_set_offset(struct xdg_positioner *xdg_positioner, int32_t x, int3 XDG_POSITIONER_SET_OFFSET, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y); } -/** - * @ingroup iface_xdg_positioner - * - * When set reactive, the surface is reconstrained if the conditions used - * for constraining changed, e.g. the parent window moved. - * - * If the conditions changed and the popup was reconstrained, an - * xdg_popup.configure event is sent with updated geometry, followed by an - * xdg_surface.configure event. - */ static inline void xdg_positioner_set_reactive(struct xdg_positioner *xdg_positioner) { @@ -919,17 +318,6 @@ xdg_positioner_set_reactive(struct xdg_positioner *xdg_positioner) XDG_POSITIONER_SET_REACTIVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0); } -/** - * @ingroup iface_xdg_positioner - * - * Set the parent window geometry the compositor should use when - * positioning the popup. The compositor may use this information to - * determine the future state the popup should be constrained using. If - * this doesn't match the dimension of the parent the popup is eventually - * positioned against, the behavior is undefined. - * - * The arguments are given in the surface-local coordinate space. - */ static inline void xdg_positioner_set_parent_size(struct xdg_positioner *xdg_positioner, int32_t parent_width, int32_t parent_height) { @@ -937,14 +325,6 @@ xdg_positioner_set_parent_size(struct xdg_positioner *xdg_positioner, int32_t pa XDG_POSITIONER_SET_PARENT_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, parent_width, parent_height); } -/** - * @ingroup iface_xdg_positioner - * - * Set the serial of an xdg_surface.configure event this positioner will be - * used in response to. The compositor may use this information together - * with set_parent_size to determine what future state the popup should be - * constrained using. - */ static inline void xdg_positioner_set_parent_configure(struct xdg_positioner *xdg_positioner, uint32_t serial) { @@ -955,70 +335,21 @@ xdg_positioner_set_parent_configure(struct xdg_positioner *xdg_positioner, uint3 #ifndef XDG_SURFACE_ERROR_ENUM #define XDG_SURFACE_ERROR_ENUM enum xdg_surface_error { - /** - * Surface was not fully constructed - */ XDG_SURFACE_ERROR_NOT_CONSTRUCTED = 1, - /** - * Surface was already constructed - */ XDG_SURFACE_ERROR_ALREADY_CONSTRUCTED = 2, - /** - * Attaching a buffer to an unconfigured surface - */ XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER = 3, - /** - * Invalid serial number when acking a configure event - */ XDG_SURFACE_ERROR_INVALID_SERIAL = 4, - /** - * Width or height was zero or negative - */ XDG_SURFACE_ERROR_INVALID_SIZE = 5, - /** - * Surface was destroyed before its role object - */ XDG_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 6, }; -#endif /* XDG_SURFACE_ERROR_ENUM */ +#endif -/** - * @ingroup iface_xdg_surface - * @struct xdg_surface_listener - */ struct xdg_surface_listener { - /** - * suggest a surface change - * - * The configure event marks the end of a configure sequence. A - * configure sequence is a set of one or more events configuring - * the state of the xdg_surface, including the final - * xdg_surface.configure event. - * - * Where applicable, xdg_surface surface roles will during a - * configure sequence extend this event as a latched state sent as - * events before the xdg_surface.configure event. Such events - * should be considered to make up a set of atomically applied - * configuration states, where the xdg_surface.configure commits - * the accumulated state. - * - * Clients should arrange their surface for the new states, and - * then send an ack_configure request with the serial sent in this - * configure event at some point before committing the new surface. - * - * If the client receives multiple configure events before it can - * respond to one, it is free to discard all but the last event it - * received. - * @param serial serial of the configure event - */ void (*configure)(void *data, struct xdg_surface *xdg_surface, uint32_t serial); }; -/** - * @ingroup iface_xdg_surface - */ static inline int xdg_surface_add_listener(struct xdg_surface *xdg_surface, const struct xdg_surface_listener *listener, void *data) @@ -1033,40 +364,20 @@ xdg_surface_add_listener(struct xdg_surface *xdg_surface, #define XDG_SURFACE_SET_WINDOW_GEOMETRY 3 #define XDG_SURFACE_ACK_CONFIGURE 4 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_GET_TOPLEVEL_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_GET_POPUP_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_SET_WINDOW_GEOMETRY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_ACK_CONFIGURE_SINCE_VERSION 1 -/** @ingroup iface_xdg_surface */ static inline void xdg_surface_set_user_data(struct xdg_surface *xdg_surface, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) xdg_surface, user_data); } -/** @ingroup iface_xdg_surface */ static inline void * xdg_surface_get_user_data(struct xdg_surface *xdg_surface) { @@ -1079,13 +390,6 @@ xdg_surface_get_version(struct xdg_surface *xdg_surface) return wl_proxy_get_version((struct wl_proxy *) xdg_surface); } -/** - * @ingroup iface_xdg_surface - * - * Destroy the xdg_surface object. An xdg_surface must only be destroyed - * after its role object has been destroyed, otherwise - * a defunct_role_object error is raised. - */ static inline void xdg_surface_destroy(struct xdg_surface *xdg_surface) { @@ -1093,15 +397,6 @@ xdg_surface_destroy(struct xdg_surface *xdg_surface) XDG_SURFACE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_xdg_surface - * - * This creates an xdg_toplevel object for the given xdg_surface and gives - * the associated wl_surface the xdg_toplevel role. - * - * See the documentation of xdg_toplevel for more details about what an - * xdg_toplevel is and how it is used. - */ static inline struct xdg_toplevel * xdg_surface_get_toplevel(struct xdg_surface *xdg_surface) { @@ -1113,18 +408,6 @@ xdg_surface_get_toplevel(struct xdg_surface *xdg_surface) return (struct xdg_toplevel *) id; } -/** - * @ingroup iface_xdg_surface - * - * This creates an xdg_popup object for the given xdg_surface and gives - * the associated wl_surface the xdg_popup role. - * - * If null is passed as a parent, a parent surface must be specified using - * some other protocol, before committing the initial state. - * - * See the documentation of xdg_popup for more details about what an - * xdg_popup is and how it is used. - */ static inline struct xdg_popup * xdg_surface_get_popup(struct xdg_surface *xdg_surface, struct xdg_surface *parent, struct xdg_positioner *positioner) { @@ -1136,39 +419,6 @@ xdg_surface_get_popup(struct xdg_surface *xdg_surface, struct xdg_surface *paren return (struct xdg_popup *) id; } -/** - * @ingroup iface_xdg_surface - * - * The window geometry of a surface is its "visible bounds" from the - * user's perspective. Client-side decorations often have invisible - * portions like drop-shadows which should be ignored for the - * purposes of aligning, placing and constraining windows. - * - * The window geometry is double buffered, and will be applied at the - * time wl_surface.commit of the corresponding wl_surface is called. - * - * When maintaining a position, the compositor should treat the (x, y) - * coordinate of the window geometry as the top left corner of the window. - * A client changing the (x, y) window geometry coordinate should in - * general not alter the position of the window. - * - * Once the window geometry of the surface is set, it is not possible to - * unset it, and it will remain the same until set_window_geometry is - * called again, even if a new subsurface or buffer is attached. - * - * If never set, the value is the full bounds of the surface, - * including any subsurfaces. This updates dynamically on every - * commit. This unset is meant for extremely simple clients. - * - * The arguments are given in the surface-local coordinate space of - * the wl_surface associated with this xdg_surface. - * - * The width and height must be greater than zero. Setting an invalid size - * will raise an invalid_size error. When applied, the effective window - * geometry will be the set window geometry clamped to the bounding - * rectangle of the combined geometry of the surface of the xdg_surface and - * the associated subsurfaces. - */ static inline void xdg_surface_set_window_geometry(struct xdg_surface *xdg_surface, int32_t x, int32_t y, int32_t width, int32_t height) { @@ -1176,42 +426,6 @@ xdg_surface_set_window_geometry(struct xdg_surface *xdg_surface, int32_t x, int3 XDG_SURFACE_SET_WINDOW_GEOMETRY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, x, y, width, height); } -/** - * @ingroup iface_xdg_surface - * - * When a configure event is received, if a client commits the - * surface in response to the configure event, then the client - * must make an ack_configure request sometime before the commit - * request, passing along the serial of the configure event. - * - * For instance, for toplevel surfaces the compositor might use this - * information to move a surface to the top left only when the client has - * drawn itself for the maximized or fullscreen state. - * - * If the client receives multiple configure events before it - * can respond to one, it only has to ack the last configure event. - * Acking a configure event that was never sent raises an invalid_serial - * error. - * - * A client is not required to commit immediately after sending - * an ack_configure request - it may even ack_configure several times - * before its next surface commit. - * - * A client may send multiple ack_configure requests before committing, but - * only the last request sent before a commit indicates which configure - * event the client really is responding to. - * - * Sending an ack_configure request consumes the serial number sent with - * the request, as well as serial numbers sent by all configure events - * sent on this xdg_surface prior to the configure event referenced by - * the committed serial. - * - * It is an error to issue multiple ack_configure requests referencing a - * serial from the same configure event, or to issue an ack_configure - * request referencing a serial from a configure event issued before the - * event identified by the last ack_configure request for the same - * xdg_surface. Doing so will raise an invalid_serial error. - */ static inline void xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial) { @@ -1222,30 +436,14 @@ xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial) #ifndef XDG_TOPLEVEL_ERROR_ENUM #define XDG_TOPLEVEL_ERROR_ENUM enum xdg_toplevel_error { - /** - * provided value is not a valid variant of the resize_edge enum - */ XDG_TOPLEVEL_ERROR_INVALID_RESIZE_EDGE = 0, - /** - * invalid parent toplevel - */ XDG_TOPLEVEL_ERROR_INVALID_PARENT = 1, - /** - * client provided an invalid min or max size - */ XDG_TOPLEVEL_ERROR_INVALID_SIZE = 2, }; -#endif /* XDG_TOPLEVEL_ERROR_ENUM */ +#endif #ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM #define XDG_TOPLEVEL_RESIZE_EDGE_ENUM -/** - * @ingroup iface_xdg_toplevel - * edge values for resizing - * - * These values are used to indicate which edge of a surface - * is being dragged in a resize operation. - */ enum xdg_toplevel_resize_edge { XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0, XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1, @@ -1257,246 +455,53 @@ enum xdg_toplevel_resize_edge { XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9, XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10, }; -#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM */ +#endif #ifndef XDG_TOPLEVEL_STATE_ENUM #define XDG_TOPLEVEL_STATE_ENUM -/** - * @ingroup iface_xdg_toplevel - * types of state on the surface - * - * The different state values used on the surface. This is designed for - * state values like maximized, fullscreen. It is paired with the - * configure event to ensure that both the client and the compositor - * setting the state can be synchronized. - * - * States set in this way are double-buffered. They will get applied on - * the next commit. - */ enum xdg_toplevel_state { - /** - * the surface is maximized - * the surface is maximized - * - * The surface is maximized. The window geometry specified in the - * configure event must be obeyed by the client. - * - * The client should draw without shadow or other decoration - * outside of the window geometry. - */ XDG_TOPLEVEL_STATE_MAXIMIZED = 1, - /** - * the surface is fullscreen - * the surface is fullscreen - * - * The surface is fullscreen. The window geometry specified in - * the configure event is a maximum; the client cannot resize - * beyond it. For a surface to cover the whole fullscreened area, - * the geometry dimensions must be obeyed by the client. For more - * details, see xdg_toplevel.set_fullscreen. - */ XDG_TOPLEVEL_STATE_FULLSCREEN = 2, - /** - * the surface is being resized - * the surface is being resized - * - * The surface is being resized. The window geometry specified in - * the configure event is a maximum; the client cannot resize - * beyond it. Clients that have aspect ratio or cell sizing - * configuration can use a smaller size, however. - */ XDG_TOPLEVEL_STATE_RESIZING = 3, - /** - * the surface is now activated - * the surface is now activated - * - * Client window decorations should be painted as if the window - * is active. Do not assume this means that the window actually has - * keyboard or pointer focus. - */ XDG_TOPLEVEL_STATE_ACTIVATED = 4, - /** - * the surface’s left edge is tiled - * - * The window is currently in a tiled layout and the left edge is - * considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_LEFT = 5, - /** - * the surface’s right edge is tiled - * - * The window is currently in a tiled layout and the right edge - * is considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_RIGHT = 6, - /** - * the surface’s top edge is tiled - * - * The window is currently in a tiled layout and the top edge is - * considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_TOP = 7, - /** - * the surface’s bottom edge is tiled - * - * The window is currently in a tiled layout and the bottom edge - * is considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8, }; -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_RIGHT_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_TOP_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_BOTTOM_SINCE_VERSION 2 -#endif /* XDG_TOPLEVEL_STATE_ENUM */ +#endif #ifndef XDG_TOPLEVEL_WM_CAPABILITIES_ENUM #define XDG_TOPLEVEL_WM_CAPABILITIES_ENUM enum xdg_toplevel_wm_capabilities { - /** - * show_window_menu is available - */ XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU = 1, - /** - * set_maximized and unset_maximized are available - */ XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE = 2, - /** - * set_fullscreen and unset_fullscreen are available - */ XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN = 3, - /** - * set_minimized is available - */ XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE = 4, }; -#endif /* XDG_TOPLEVEL_WM_CAPABILITIES_ENUM */ +#endif -/** - * @ingroup iface_xdg_toplevel - * @struct xdg_toplevel_listener - */ struct xdg_toplevel_listener { - /** - * suggest a surface change - * - * This configure event asks the client to resize its toplevel - * surface or to change its state. The configured state should not - * be applied immediately. See xdg_surface.configure for details. - * - * The width and height arguments specify a hint to the window - * about how its surface should be resized in window geometry - * coordinates. See set_window_geometry. - * - * If the width or height arguments are zero, it means the client - * should decide its own window dimension. This may happen when the - * compositor needs to configure the state of the surface but - * doesn't have any information about any previous or expected - * dimension. - * - * The states listed in the event specify how the width/height - * arguments should be interpreted, and possibly how it should be - * drawn. - * - * Clients must send an ack_configure in response to this event. - * See xdg_surface.configure and xdg_surface.ack_configure for - * details. - */ void (*configure)(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height, struct wl_array *states); - /** - * surface wants to be closed - * - * The close event is sent by the compositor when the user wants - * the surface to be closed. This should be equivalent to the user - * clicking the close button in client-side decorations, if your - * application has any. - * - * This is only a request that the user intends to close the - * window. The client may choose to ignore this request, or show a - * dialog to ask the user to save their data, etc. - */ void (*close)(void *data, struct xdg_toplevel *xdg_toplevel); - /** - * recommended window geometry bounds - * - * The configure_bounds event may be sent prior to a - * xdg_toplevel.configure event to communicate the bounds a window - * geometry size is recommended to constrain to. - * - * The passed width and height are in surface coordinate space. If - * width and height are 0, it means bounds is unknown and - * equivalent to as if no configure_bounds event was ever sent for - * this surface. - * - * The bounds can for example correspond to the size of a monitor - * excluding any panels or other shell components, so that a - * surface isn't created in a way that it cannot fit. - * - * The bounds may change at any point, and in such a case, a new - * xdg_toplevel.configure_bounds will be sent, followed by - * xdg_toplevel.configure and xdg_surface.configure. - * @since 4 - */ void (*configure_bounds)(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height); - /** - * compositor capabilities - * - * This event advertises the capabilities supported by the - * compositor. If a capability isn't supported, clients should hide - * or disable the UI elements that expose this functionality. For - * instance, if the compositor doesn't advertise support for - * minimized toplevels, a button triggering the set_minimized - * request should not be displayed. - * - * The compositor will ignore requests it doesn't support. For - * instance, a compositor which doesn't advertise support for - * minimized will ignore set_minimized requests. - * - * Compositors must send this event once before the first - * xdg_surface.configure event. When the capabilities change, - * compositors must send this event again and then send an - * xdg_surface.configure event. - * - * The configured state should not be applied immediately. See - * xdg_surface.configure for details. - * - * The capabilities are sent as an array of 32-bit unsigned - * integers in native endianness. - * @param capabilities array of 32-bit capabilities - * @since 5 - */ void (*wm_capabilities)(void *data, struct xdg_toplevel *xdg_toplevel, struct wl_array *capabilities); }; -/** - * @ingroup iface_xdg_toplevel - */ static inline int xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel, const struct xdg_toplevel_listener *listener, void *data) @@ -1520,88 +525,32 @@ xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel, #define XDG_TOPLEVEL_UNSET_FULLSCREEN 12 #define XDG_TOPLEVEL_SET_MINIMIZED 13 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_CLOSE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION 4 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION 5 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_PARENT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_TITLE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_APP_ID_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SHOW_WINDOW_MENU_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_MOVE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_RESIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MAX_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MIN_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_UNSET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_UNSET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MINIMIZED_SINCE_VERSION 1 -/** @ingroup iface_xdg_toplevel */ static inline void xdg_toplevel_set_user_data(struct xdg_toplevel *xdg_toplevel, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) xdg_toplevel, user_data); } -/** @ingroup iface_xdg_toplevel */ static inline void * xdg_toplevel_get_user_data(struct xdg_toplevel *xdg_toplevel) { @@ -1614,12 +563,6 @@ xdg_toplevel_get_version(struct xdg_toplevel *xdg_toplevel) return wl_proxy_get_version((struct wl_proxy *) xdg_toplevel); } -/** - * @ingroup iface_xdg_toplevel - * - * This request destroys the role surface and unmaps the surface; - * see "Unmapping" behavior in interface section for details. - */ static inline void xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel) { @@ -1627,30 +570,6 @@ xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel) XDG_TOPLEVEL_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_xdg_toplevel - * - * Set the "parent" of this surface. This surface should be stacked - * above the parent surface and all other ancestor surfaces. - * - * Parent surfaces should be set on dialogs, toolboxes, or other - * "auxiliary" surfaces, so that the parent is raised when the dialog - * is raised. - * - * Setting a null parent for a child surface unsets its parent. Setting - * a null parent for a surface which currently has no parent is a no-op. - * - * Only mapped surfaces can have child surfaces. Setting a parent which - * is not mapped is equivalent to setting a null parent. If a surface - * becomes unmapped, its children's parent is set to the parent of - * the now-unmapped surface. If the now-unmapped surface has no parent, - * its children's parent is unset. If the now-unmapped surface becomes - * mapped again, its parent-child relationship is not restored. - * - * The parent toplevel must not be one of the child toplevel's - * descendants, and the parent must be different from the child toplevel, - * otherwise the invalid_parent protocol error is raised. - */ static inline void xdg_toplevel_set_parent(struct xdg_toplevel *xdg_toplevel, struct xdg_toplevel *parent) { @@ -1658,17 +577,6 @@ xdg_toplevel_set_parent(struct xdg_toplevel *xdg_toplevel, struct xdg_toplevel * XDG_TOPLEVEL_SET_PARENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, parent); } -/** - * @ingroup iface_xdg_toplevel - * - * Set a short title for the surface. - * - * This string may be used to identify the surface in a task bar, - * window list, or other user interface elements provided by the - * compositor. - * - * The string must be encoded in UTF-8. - */ static inline void xdg_toplevel_set_title(struct xdg_toplevel *xdg_toplevel, const char *title) { @@ -1676,33 +584,6 @@ xdg_toplevel_set_title(struct xdg_toplevel *xdg_toplevel, const char *title) XDG_TOPLEVEL_SET_TITLE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, title); } -/** - * @ingroup iface_xdg_toplevel - * - * Set an application identifier for the surface. - * - * The app ID identifies the general class of applications to which - * the surface belongs. The compositor can use this to group multiple - * surfaces together, or to determine how to launch a new application. - * - * For D-Bus activatable applications, the app ID is used as the D-Bus - * service name. - * - * The compositor shell will try to group application surfaces together - * by their app ID. As a best practice, it is suggested to select app - * ID's that match the basename of the application's .desktop file. - * For example, "org.freedesktop.FooViewer" where the .desktop file is - * "org.freedesktop.FooViewer.desktop". - * - * Like other properties, a set_app_id request can be sent after the - * xdg_toplevel has been mapped to update the property. - * - * See the desktop-entry specification [0] for more details on - * application identifiers and how they relate to well-known D-Bus - * names and .desktop files. - * - * [0] https://standards.freedesktop.org/desktop-entry-spec/ - */ static inline void xdg_toplevel_set_app_id(struct xdg_toplevel *xdg_toplevel, const char *app_id) { @@ -1710,22 +591,6 @@ xdg_toplevel_set_app_id(struct xdg_toplevel *xdg_toplevel, const char *app_id) XDG_TOPLEVEL_SET_APP_ID, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, app_id); } -/** - * @ingroup iface_xdg_toplevel - * - * Clients implementing client-side decorations might want to show - * a context menu when right-clicking on the decorations, giving the - * user a menu that they can use to maximize or minimize the window. - * - * This request asks the compositor to pop up such a window menu at - * the given position, relative to the local surface coordinates of - * the parent surface. There are no guarantees as to what menu items - * the window menu contains, or even if a window menu will be drawn - * at all. - * - * This request must be used in response to some sort of user action - * like a button press, key press, or touch down event. - */ static inline void xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, int32_t x, int32_t y) { @@ -1733,26 +598,6 @@ xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, struct wl_seat XDG_TOPLEVEL_SHOW_WINDOW_MENU, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, x, y); } -/** - * @ingroup iface_xdg_toplevel - * - * Start an interactive, user-driven move of the surface. - * - * This request must be used in response to some sort of user action - * like a button press, key press, or touch down event. The passed - * serial is used to determine the type of interactive move (touch, - * pointer, etc). - * - * The server may ignore move requests depending on the state of - * the surface (e.g. fullscreen or maximized), or if the passed serial - * is no longer valid. - * - * If triggered, the surface will lose the focus of the device - * (wl_pointer, wl_touch, etc) used for the move. It is up to the - * compositor to visually indicate that the move is taking place, such as - * updating a pointer cursor, during the move. There is no guarantee - * that the device focus will return when the move is completed. - */ static inline void xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial) { @@ -1760,41 +605,6 @@ xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint3 XDG_TOPLEVEL_MOVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial); } -/** - * @ingroup iface_xdg_toplevel - * - * Start a user-driven, interactive resize of the surface. - * - * This request must be used in response to some sort of user action - * like a button press, key press, or touch down event. The passed - * serial is used to determine the type of interactive resize (touch, - * pointer, etc). - * - * The server may ignore resize requests depending on the state of - * the surface (e.g. fullscreen or maximized). - * - * If triggered, the client will receive configure events with the - * "resize" state enum value and the expected sizes. See the "resize" - * enum value for more details about what is required. The client - * must also acknowledge configure events using "ack_configure". After - * the resize is completed, the client will receive another "configure" - * event without the resize state. - * - * If triggered, the surface also will lose the focus of the device - * (wl_pointer, wl_touch, etc) used for the resize. It is up to the - * compositor to visually indicate that the resize is taking place, - * such as updating a pointer cursor, during the resize. There is no - * guarantee that the device focus will return when the resize is - * completed. - * - * The edges parameter specifies how the surface should be resized, and - * is one of the values of the resize_edge enum. Values not matching - * a variant of the enum will cause a protocol error. The compositor - * may use this information to update the surface position for example - * when dragging the top left corner. The compositor may also use - * this information to adapt its behavior, e.g. choose an appropriate - * cursor image. - */ static inline void xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, uint32_t edges) { @@ -1802,44 +612,6 @@ xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uin XDG_TOPLEVEL_RESIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, edges); } -/** - * @ingroup iface_xdg_toplevel - * - * Set a maximum size for the window. - * - * The client can specify a maximum size so that the compositor does - * not try to configure the window beyond this size. - * - * The width and height arguments are in window geometry coordinates. - * See xdg_surface.set_window_geometry. - * - * Values set in this way are double-buffered. They will get applied - * on the next commit. - * - * The compositor can use this information to allow or disallow - * different states like maximize or fullscreen and draw accurate - * animations. - * - * Similarly, a tiling window manager may use this information to - * place and resize client windows in a more effective way. - * - * The client should not rely on the compositor to obey the maximum - * size. The compositor may decide to ignore the values set by the - * client and request a larger size. - * - * If never set, or a value of zero in the request, means that the - * client has no expected maximum size in the given dimension. - * As a result, a client wishing to reset the maximum size - * to an unspecified state can use zero for width and height in the - * request. - * - * Requesting a maximum size to be smaller than the minimum size of - * a surface is illegal and will result in an invalid_size error. - * - * The width and height must be greater than or equal to zero. Using - * strictly negative values for width or height will result in a - * invalid_size error. - */ static inline void xdg_toplevel_set_max_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) { @@ -1847,44 +619,6 @@ xdg_toplevel_set_max_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int3 XDG_TOPLEVEL_SET_MAX_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); } -/** - * @ingroup iface_xdg_toplevel - * - * Set a minimum size for the window. - * - * The client can specify a minimum size so that the compositor does - * not try to configure the window below this size. - * - * The width and height arguments are in window geometry coordinates. - * See xdg_surface.set_window_geometry. - * - * Values set in this way are double-buffered. They will get applied - * on the next commit. - * - * The compositor can use this information to allow or disallow - * different states like maximize or fullscreen and draw accurate - * animations. - * - * Similarly, a tiling window manager may use this information to - * place and resize client windows in a more effective way. - * - * The client should not rely on the compositor to obey the minimum - * size. The compositor may decide to ignore the values set by the - * client and request a smaller size. - * - * If never set, or a value of zero in the request, means that the - * client has no expected minimum size in the given dimension. - * As a result, a client wishing to reset the minimum size - * to an unspecified state can use zero for width and height in the - * request. - * - * Requesting a minimum size to be larger than the maximum size of - * a surface is illegal and will result in an invalid_size error. - * - * The width and height must be greater than or equal to zero. Using - * strictly negative values for width and height will result in a - * invalid_size error. - */ static inline void xdg_toplevel_set_min_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) { @@ -1892,29 +626,6 @@ xdg_toplevel_set_min_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int3 XDG_TOPLEVEL_SET_MIN_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); } -/** - * @ingroup iface_xdg_toplevel - * - * Maximize the surface. - * - * After requesting that the surface should be maximized, the compositor - * will respond by emitting a configure event. Whether this configure - * actually sets the window maximized is subject to compositor policies. - * The client must then update its content, drawing in the configured - * state. The client must also acknowledge the configure when committing - * the new content (see ack_configure). - * - * It is up to the compositor to decide how and where to maximize the - * surface, for example which output and what region of the screen should - * be used. - * - * If the surface was already maximized, the compositor will still emit - * a configure event with the "maximized" state. - * - * If the surface is in a fullscreen state, this request has no direct - * effect. It may alter the state the surface is returned to when - * unmaximized unless overridden by the compositor. - */ static inline void xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel) { @@ -1922,31 +633,6 @@ xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel) XDG_TOPLEVEL_SET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); } -/** - * @ingroup iface_xdg_toplevel - * - * Unmaximize the surface. - * - * After requesting that the surface should be unmaximized, the compositor - * will respond by emitting a configure event. Whether this actually - * un-maximizes the window is subject to compositor policies. - * If available and applicable, the compositor will include the window - * geometry dimensions the window had prior to being maximized in the - * configure event. The client must then update its content, drawing it in - * the configured state. The client must also acknowledge the configure - * when committing the new content (see ack_configure). - * - * It is up to the compositor to position the surface after it was - * unmaximized; usually the position the surface had before maximizing, if - * applicable. - * - * If the surface was already not maximized, the compositor will still - * emit a configure event without the "maximized" state. - * - * If the surface is in a fullscreen state, this request has no direct - * effect. It may alter the state the surface is returned to when - * unmaximized unless overridden by the compositor. - */ static inline void xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel) { @@ -1954,33 +640,6 @@ xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel) XDG_TOPLEVEL_UNSET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); } -/** - * @ingroup iface_xdg_toplevel - * - * Make the surface fullscreen. - * - * After requesting that the surface should be fullscreened, the - * compositor will respond by emitting a configure event. Whether the - * client is actually put into a fullscreen state is subject to compositor - * policies. The client must also acknowledge the configure when - * committing the new content (see ack_configure). - * - * The output passed by the request indicates the client's preference as - * to which display it should be set fullscreen on. If this value is NULL, - * it's up to the compositor to choose which display will be used to map - * this surface. - * - * If the surface doesn't cover the whole output, the compositor will - * position the surface in the center of the output and compensate with - * with border fill covering the rest of the output. The content of the - * border fill is undefined, but should be assumed to be in some way that - * attempts to blend into the surrounding area (e.g. solid black). - * - * If the fullscreened surface is not opaque, the compositor must make - * sure that other screen content not part of the same surface tree (made - * up of subsurfaces, popups or similarly coupled surfaces) are not - * visible below the fullscreened surface. - */ static inline void xdg_toplevel_set_fullscreen(struct xdg_toplevel *xdg_toplevel, struct wl_output *output) { @@ -1988,27 +647,6 @@ xdg_toplevel_set_fullscreen(struct xdg_toplevel *xdg_toplevel, struct wl_output XDG_TOPLEVEL_SET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, output); } -/** - * @ingroup iface_xdg_toplevel - * - * Make the surface no longer fullscreen. - * - * After requesting that the surface should be unfullscreened, the - * compositor will respond by emitting a configure event. - * Whether this actually removes the fullscreen state of the client is - * subject to compositor policies. - * - * Making a surface unfullscreen sets states for the surface based on the following: - * * the state(s) it may have had before becoming fullscreen - * * any state(s) decided by the compositor - * * any state(s) requested by the client while the surface was fullscreen - * - * The compositor may include the previous window geometry dimensions in - * the configure event, if applicable. - * - * The client must also acknowledge the configure when committing the new - * content (see ack_configure). - */ static inline void xdg_toplevel_unset_fullscreen(struct xdg_toplevel *xdg_toplevel) { @@ -2016,18 +654,6 @@ xdg_toplevel_unset_fullscreen(struct xdg_toplevel *xdg_toplevel) XDG_TOPLEVEL_UNSET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); } -/** - * @ingroup iface_xdg_toplevel - * - * Request that the compositor minimize your surface. There is no - * way to know if the surface is currently minimized, nor is there - * any way to unset minimization on this surface. - * - * If you are looking to throttle redrawing when minimized, please - * instead use the wl_surface.frame event for this, as this will - * also work with live previews on windows in Alt-Tab, Expose or - * similar compositor features. - */ static inline void xdg_toplevel_set_minimized(struct xdg_toplevel *xdg_toplevel) { @@ -2038,85 +664,24 @@ xdg_toplevel_set_minimized(struct xdg_toplevel *xdg_toplevel) #ifndef XDG_POPUP_ERROR_ENUM #define XDG_POPUP_ERROR_ENUM enum xdg_popup_error { - /** - * tried to grab after being mapped - */ XDG_POPUP_ERROR_INVALID_GRAB = 0, }; -#endif /* XDG_POPUP_ERROR_ENUM */ +#endif -/** - * @ingroup iface_xdg_popup - * @struct xdg_popup_listener - */ struct xdg_popup_listener { - /** - * configure the popup surface - * - * This event asks the popup surface to configure itself given - * the configuration. The configured state should not be applied - * immediately. See xdg_surface.configure for details. - * - * The x and y arguments represent the position the popup was - * placed at given the xdg_positioner rule, relative to the upper - * left corner of the window geometry of the parent surface. - * - * For version 2 or older, the configure event for an xdg_popup is - * only ever sent once for the initial configuration. Starting with - * version 3, it may be sent again if the popup is setup with an - * xdg_positioner with set_reactive requested, or in response to - * xdg_popup.reposition requests. - * @param x x position relative to parent surface window geometry - * @param y y position relative to parent surface window geometry - * @param width window geometry width - * @param height window geometry height - */ void (*configure)(void *data, struct xdg_popup *xdg_popup, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * popup interaction is done - * - * The popup_done event is sent out when a popup is dismissed by - * the compositor. The client should destroy the xdg_popup object - * at this point. - */ void (*popup_done)(void *data, struct xdg_popup *xdg_popup); - /** - * signal the completion of a repositioned request - * - * The repositioned event is sent as part of a popup - * configuration sequence, together with xdg_popup.configure and - * lastly xdg_surface.configure to notify the completion of a - * reposition request. - * - * The repositioned event is to notify about the completion of a - * xdg_popup.reposition request. The token argument is the token - * passed in the xdg_popup.reposition request. - * - * Immediately after this event is emitted, xdg_popup.configure and - * xdg_surface.configure will be sent with the updated size and - * position, as well as a new configure serial. - * - * The client should optionally update the content of the popup, - * but must acknowledge the new popup configuration for the new - * position to take effect. See xdg_surface.ack_configure for - * details. - * @param token reposition request token - * @since 3 - */ void (*repositioned)(void *data, struct xdg_popup *xdg_popup, uint32_t token); }; -/** - * @ingroup iface_xdg_popup - */ static inline int xdg_popup_add_listener(struct xdg_popup *xdg_popup, const struct xdg_popup_listener *listener, void *data) @@ -2129,40 +694,20 @@ xdg_popup_add_listener(struct xdg_popup *xdg_popup, #define XDG_POPUP_GRAB 1 #define XDG_POPUP_REPOSITION 2 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_POPUP_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_REPOSITIONED_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_GRAB_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_REPOSITION_SINCE_VERSION 3 -/** @ingroup iface_xdg_popup */ static inline void xdg_popup_set_user_data(struct xdg_popup *xdg_popup, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) xdg_popup, user_data); } -/** @ingroup iface_xdg_popup */ static inline void * xdg_popup_get_user_data(struct xdg_popup *xdg_popup) { @@ -2175,15 +720,6 @@ xdg_popup_get_version(struct xdg_popup *xdg_popup) return wl_proxy_get_version((struct wl_proxy *) xdg_popup); } -/** - * @ingroup iface_xdg_popup - * - * This destroys the popup. Explicitly destroying the xdg_popup - * object will also dismiss the popup, and unmap the surface. - * - * If this xdg_popup is not the "topmost" popup, a protocol error - * will be sent. - */ static inline void xdg_popup_destroy(struct xdg_popup *xdg_popup) { @@ -2191,47 +727,6 @@ xdg_popup_destroy(struct xdg_popup *xdg_popup) XDG_POPUP_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_xdg_popup - * - * This request makes the created popup take an explicit grab. An explicit - * grab will be dismissed when the user dismisses the popup, or when the - * client destroys the xdg_popup. This can be done by the user clicking - * outside the surface, using the keyboard, or even locking the screen - * through closing the lid or a timeout. - * - * If the compositor denies the grab, the popup will be immediately - * dismissed. - * - * This request must be used in response to some sort of user action like a - * button press, key press, or touch down event. The serial number of the - * event should be passed as 'serial'. - * - * The parent of a grabbing popup must either be an xdg_toplevel surface or - * another xdg_popup with an explicit grab. If the parent is another - * xdg_popup it means that the popups are nested, with this popup now being - * the topmost popup. - * - * Nested popups must be destroyed in the reverse order they were created - * in, e.g. the only popup you are allowed to destroy at all times is the - * topmost one. - * - * When compositors choose to dismiss a popup, they may dismiss every - * nested grabbing popup as well. When a compositor dismisses popups, it - * will follow the same dismissing order as required from the client. - * - * If the topmost grabbing popup is destroyed, the grab will be returned to - * the parent of the popup, if that parent previously had an explicit grab. - * - * If the parent is a grabbing popup which has already been dismissed, this - * popup will be immediately dismissed. If the parent is a popup that did - * not take an explicit grab, an error will be raised. - * - * During a popup grab, the client owning the grab will receive pointer - * and touch events for all their surfaces as normal (similar to an - * "owner-events" grab in X11 parlance), while the top most grabbing popup - * will always have keyboard focus. - */ static inline void xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t serial) { @@ -2239,33 +734,6 @@ xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t seria XDG_POPUP_GRAB, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, seat, serial); } -/** - * @ingroup iface_xdg_popup - * - * Reposition an already-mapped popup. The popup will be placed given the - * details in the passed xdg_positioner object, and a - * xdg_popup.repositioned followed by xdg_popup.configure and - * xdg_surface.configure will be emitted in response. Any parameters set - * by the previous positioner will be discarded. - * - * The passed token will be sent in the corresponding - * xdg_popup.repositioned event. The new popup position will not take - * effect until the corresponding configure event is acknowledged by the - * client. See xdg_popup.repositioned for details. The token itself is - * opaque, and has no other special meaning. - * - * If multiple reposition requests are sent, the compositor may skip all - * but the last one. - * - * If the popup is repositioned in response to a configure event for its - * parent, the client should send an xdg_positioner.set_parent_configure - * and possibly an xdg_positioner.set_parent_size request to allow the - * compositor to properly constrain the popup. - * - * If the popup is repositioned together with a parent that is being - * resized, but not in response to a configure event, the client should - * send an xdg_positioner.set_parent_size request. - */ static inline void xdg_popup_reposition(struct xdg_popup *xdg_popup, struct xdg_positioner *positioner, uint32_t token) { diff --git a/app/src/main/cpp/waylandcomp/generated/xdg-shell-protocol.c b/app/src/main/cpp/waylandcomp/generated/xdg-shell-protocol.c index 72b394a2b..d6a52b5de 100644 --- a/app/src/main/cpp/waylandcomp/generated/xdg-shell-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/xdg-shell-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2008-2013 Kristian Høgsberg * Copyright © 2013 Rafael Antognolli @@ -34,7 +32,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/xdg-shell-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/xdg-shell-server-protocol.h index 2683694ce..825c971b2 100644 --- a/app/src/main/cpp/waylandcomp/generated/xdg-shell-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/xdg-shell-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef XDG_SHELL_SERVER_PROTOCOL_H #define XDG_SHELL_SERVER_PROTOCOL_H @@ -14,16 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_xdg_shell The xdg_shell protocol - * @section page_ifaces_xdg_shell Interfaces - * - @subpage page_iface_xdg_wm_base - create desktop-style surfaces - * - @subpage page_iface_xdg_positioner - child surface positioner - * - @subpage page_iface_xdg_surface - desktop user interface surface base interface - * - @subpage page_iface_xdg_toplevel - toplevel surface - * - @subpage page_iface_xdg_popup - short-lived, popup surfaces for menus +/* * @section page_copyright_xdg_shell Copyright - *
  *
  * Copyright © 2008-2013 Kristian Høgsberg
  * Copyright © 2013      Rafael Antognolli
@@ -50,8 +41,16 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + struct wl_output; struct wl_seat; struct wl_surface; @@ -63,343 +62,40 @@ struct xdg_wm_base; #ifndef XDG_WM_BASE_INTERFACE #define XDG_WM_BASE_INTERFACE -/** - * @page page_iface_xdg_wm_base xdg_wm_base - * @section page_iface_xdg_wm_base_desc Description - * - * The xdg_wm_base interface is exposed as a global object enabling clients - * to turn their wl_surfaces into windows in a desktop environment. It - * defines the basic functionality needed for clients and the compositor to - * create windows that can be dragged, resized, maximized, etc, as well as - * creating transient windows such as popup menus. - * @section page_iface_xdg_wm_base_api API - * See @ref iface_xdg_wm_base. - */ -/** - * @defgroup iface_xdg_wm_base The xdg_wm_base interface - * - * The xdg_wm_base interface is exposed as a global object enabling clients - * to turn their wl_surfaces into windows in a desktop environment. It - * defines the basic functionality needed for clients and the compositor to - * create windows that can be dragged, resized, maximized, etc, as well as - * creating transient windows such as popup menus. - */ extern const struct wl_interface xdg_wm_base_interface; #endif #ifndef XDG_POSITIONER_INTERFACE #define XDG_POSITIONER_INTERFACE -/** - * @page page_iface_xdg_positioner xdg_positioner - * @section page_iface_xdg_positioner_desc Description - * - * The xdg_positioner provides a collection of rules for the placement of a - * child surface relative to a parent surface. Rules can be defined to ensure - * the child surface remains within the visible area's borders, and to - * specify how the child surface changes its position, such as sliding along - * an axis, or flipping around a rectangle. These positioner-created rules are - * constrained by the requirement that a child surface must intersect with or - * be at least partially adjacent to its parent surface. - * - * See the various requests for details about possible rules. - * - * At the time of the request, the compositor makes a copy of the rules - * specified by the xdg_positioner. Thus, after the request is complete the - * xdg_positioner object can be destroyed or reused; further changes to the - * object will have no effect on previous usages. - * - * For an xdg_positioner object to be considered complete, it must have a - * non-zero size set by set_size, and a non-zero anchor rectangle set by - * set_anchor_rect. Passing an incomplete xdg_positioner object when - * positioning a surface raises an invalid_positioner error. - * @section page_iface_xdg_positioner_api API - * See @ref iface_xdg_positioner. - */ -/** - * @defgroup iface_xdg_positioner The xdg_positioner interface - * - * The xdg_positioner provides a collection of rules for the placement of a - * child surface relative to a parent surface. Rules can be defined to ensure - * the child surface remains within the visible area's borders, and to - * specify how the child surface changes its position, such as sliding along - * an axis, or flipping around a rectangle. These positioner-created rules are - * constrained by the requirement that a child surface must intersect with or - * be at least partially adjacent to its parent surface. - * - * See the various requests for details about possible rules. - * - * At the time of the request, the compositor makes a copy of the rules - * specified by the xdg_positioner. Thus, after the request is complete the - * xdg_positioner object can be destroyed or reused; further changes to the - * object will have no effect on previous usages. - * - * For an xdg_positioner object to be considered complete, it must have a - * non-zero size set by set_size, and a non-zero anchor rectangle set by - * set_anchor_rect. Passing an incomplete xdg_positioner object when - * positioning a surface raises an invalid_positioner error. - */ extern const struct wl_interface xdg_positioner_interface; #endif #ifndef XDG_SURFACE_INTERFACE #define XDG_SURFACE_INTERFACE -/** - * @page page_iface_xdg_surface xdg_surface - * @section page_iface_xdg_surface_desc Description - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides a base set of functionality required to construct user - * interface elements requiring management by the compositor, such as - * toplevel windows, menus, etc. The types of functionality are split into - * xdg_surface roles. - * - * Creating an xdg_surface does not set the role for a wl_surface. In order - * to map an xdg_surface, the client must create a role-specific object - * using, e.g., get_toplevel, get_popup. The wl_surface for any given - * xdg_surface can have at most one role, and may not be assigned any role - * not based on xdg_surface. - * - * A role must be assigned before any other requests are made to the - * xdg_surface object. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_surface state to take effect. - * - * Creating an xdg_surface from a wl_surface which has a buffer attached or - * committed is a client error, and any attempts by a client to attach or - * manipulate a buffer prior to the first xdg_surface.configure call must - * also be treated as errors. - * - * After creating a role-specific object and setting it up, the client must - * perform an initial commit without any buffer attached. The compositor - * will reply with an xdg_surface.configure event. The client must - * acknowledge it and is then allowed to attach a buffer to map the surface. - * - * Mapping an xdg_surface-based role surface is defined as making it - * possible for the surface to be shown by the compositor. Note that - * a mapped surface is not guaranteed to be visible once it is mapped. - * - * For an xdg_surface to be mapped by the compositor, the following - * conditions must be met: - * (1) the client has assigned an xdg_surface-based role to the surface - * (2) the client has set and committed the xdg_surface state and the - * role-dependent state to the surface - * (3) the client has committed a buffer to the surface - * - * A newly-unmapped surface is considered to have met condition (1) out - * of the 3 required conditions for mapping a surface if its role surface - * has not been destroyed, i.e. the client must perform the initial commit - * again before attaching a buffer. - * @section page_iface_xdg_surface_api API - * See @ref iface_xdg_surface. - */ -/** - * @defgroup iface_xdg_surface The xdg_surface interface - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides a base set of functionality required to construct user - * interface elements requiring management by the compositor, such as - * toplevel windows, menus, etc. The types of functionality are split into - * xdg_surface roles. - * - * Creating an xdg_surface does not set the role for a wl_surface. In order - * to map an xdg_surface, the client must create a role-specific object - * using, e.g., get_toplevel, get_popup. The wl_surface for any given - * xdg_surface can have at most one role, and may not be assigned any role - * not based on xdg_surface. - * - * A role must be assigned before any other requests are made to the - * xdg_surface object. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_surface state to take effect. - * - * Creating an xdg_surface from a wl_surface which has a buffer attached or - * committed is a client error, and any attempts by a client to attach or - * manipulate a buffer prior to the first xdg_surface.configure call must - * also be treated as errors. - * - * After creating a role-specific object and setting it up, the client must - * perform an initial commit without any buffer attached. The compositor - * will reply with an xdg_surface.configure event. The client must - * acknowledge it and is then allowed to attach a buffer to map the surface. - * - * Mapping an xdg_surface-based role surface is defined as making it - * possible for the surface to be shown by the compositor. Note that - * a mapped surface is not guaranteed to be visible once it is mapped. - * - * For an xdg_surface to be mapped by the compositor, the following - * conditions must be met: - * (1) the client has assigned an xdg_surface-based role to the surface - * (2) the client has set and committed the xdg_surface state and the - * role-dependent state to the surface - * (3) the client has committed a buffer to the surface - * - * A newly-unmapped surface is considered to have met condition (1) out - * of the 3 required conditions for mapping a surface if its role surface - * has not been destroyed, i.e. the client must perform the initial commit - * again before attaching a buffer. - */ extern const struct wl_interface xdg_surface_interface; #endif #ifndef XDG_TOPLEVEL_INTERFACE #define XDG_TOPLEVEL_INTERFACE -/** - * @page page_iface_xdg_toplevel xdg_toplevel - * @section page_iface_xdg_toplevel_desc Description - * - * This interface defines an xdg_surface role which allows a surface to, - * among other things, set window-like properties such as maximize, - * fullscreen, and minimize, set application-specific metadata like title and - * id, and well as trigger user interactive operations such as interactive - * resize and move. - * - * Unmapping an xdg_toplevel means that the surface cannot be shown - * by the compositor until it is explicitly mapped again. - * All active operations (e.g., move, resize) are canceled and all - * attributes (e.g. title, state, stacking, ...) are discarded for - * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to - * the state it had right after xdg_surface.get_toplevel. The client - * can re-map the toplevel by performing a commit without any buffer - * attached, waiting for a configure event and handling it as usual (see - * xdg_surface description). - * - * Attaching a null buffer to a toplevel unmaps the surface. - * @section page_iface_xdg_toplevel_api API - * See @ref iface_xdg_toplevel. - */ -/** - * @defgroup iface_xdg_toplevel The xdg_toplevel interface - * - * This interface defines an xdg_surface role which allows a surface to, - * among other things, set window-like properties such as maximize, - * fullscreen, and minimize, set application-specific metadata like title and - * id, and well as trigger user interactive operations such as interactive - * resize and move. - * - * Unmapping an xdg_toplevel means that the surface cannot be shown - * by the compositor until it is explicitly mapped again. - * All active operations (e.g., move, resize) are canceled and all - * attributes (e.g. title, state, stacking, ...) are discarded for - * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to - * the state it had right after xdg_surface.get_toplevel. The client - * can re-map the toplevel by performing a commit without any buffer - * attached, waiting for a configure event and handling it as usual (see - * xdg_surface description). - * - * Attaching a null buffer to a toplevel unmaps the surface. - */ extern const struct wl_interface xdg_toplevel_interface; #endif #ifndef XDG_POPUP_INTERFACE #define XDG_POPUP_INTERFACE -/** - * @page page_iface_xdg_popup xdg_popup - * @section page_iface_xdg_popup_desc Description - * - * A popup surface is a short-lived, temporary surface. It can be used to - * implement for example menus, popovers, tooltips and other similar user - * interface concepts. - * - * A popup can be made to take an explicit grab. See xdg_popup.grab for - * details. - * - * When the popup is dismissed, a popup_done event will be sent out, and at - * the same time the surface will be unmapped. See the xdg_popup.popup_done - * event for details. - * - * Explicitly destroying the xdg_popup object will also dismiss the popup and - * unmap the surface. Clients that want to dismiss the popup when another - * surface of their own is clicked should dismiss the popup using the destroy - * request. - * - * A newly created xdg_popup will be stacked on top of all previously created - * xdg_popup surfaces associated with the same xdg_toplevel. - * - * The parent of an xdg_popup must be mapped (see the xdg_surface - * description) before the xdg_popup itself. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_popup state to take effect. - * @section page_iface_xdg_popup_api API - * See @ref iface_xdg_popup. - */ -/** - * @defgroup iface_xdg_popup The xdg_popup interface - * - * A popup surface is a short-lived, temporary surface. It can be used to - * implement for example menus, popovers, tooltips and other similar user - * interface concepts. - * - * A popup can be made to take an explicit grab. See xdg_popup.grab for - * details. - * - * When the popup is dismissed, a popup_done event will be sent out, and at - * the same time the surface will be unmapped. See the xdg_popup.popup_done - * event for details. - * - * Explicitly destroying the xdg_popup object will also dismiss the popup and - * unmap the surface. Clients that want to dismiss the popup when another - * surface of their own is clicked should dismiss the popup using the destroy - * request. - * - * A newly created xdg_popup will be stacked on top of all previously created - * xdg_popup surfaces associated with the same xdg_toplevel. - * - * The parent of an xdg_popup must be mapped (see the xdg_surface - * description) before the xdg_popup itself. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_popup state to take effect. - */ extern const struct wl_interface xdg_popup_interface; #endif #ifndef XDG_WM_BASE_ERROR_ENUM #define XDG_WM_BASE_ERROR_ENUM enum xdg_wm_base_error { - /** - * given wl_surface has another role - */ XDG_WM_BASE_ERROR_ROLE = 0, - /** - * xdg_wm_base was destroyed before children - */ XDG_WM_BASE_ERROR_DEFUNCT_SURFACES = 1, - /** - * the client tried to map or destroy a non-topmost popup - */ XDG_WM_BASE_ERROR_NOT_THE_TOPMOST_POPUP = 2, - /** - * the client specified an invalid popup parent surface - */ XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT = 3, - /** - * the client provided an invalid surface state - */ XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE = 4, - /** - * the client provided an invalid positioner - */ XDG_WM_BASE_ERROR_INVALID_POSITIONER = 5, - /** - * the client didn’t respond to a ping event in time - */ XDG_WM_BASE_ERROR_UNRESPONSIVE = 6, }; -#endif /* XDG_WM_BASE_ERROR_ENUM */ +#endif #ifndef XDG_WM_BASE_ERROR_ENUM_IS_VALID #define XDG_WM_BASE_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_wm_base - * Validate a xdg_wm_base error value. - * - * @return true on success, false on error. - * @ref xdg_wm_base_error - */ static inline bool xdg_wm_base_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -421,64 +117,18 @@ xdg_wm_base_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_WM_BASE_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_xdg_wm_base - * @struct xdg_wm_base_interface - */ struct xdg_wm_base_interface { - /** - * destroy xdg_wm_base - * - * Destroy this xdg_wm_base object. - * - * Destroying a bound xdg_wm_base object while there are surfaces - * still alive created by this xdg_wm_base object instance is - * illegal and will result in a defunct_surfaces error. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * create a positioner object - * - * Create a positioner object. A positioner object is used to - * position surfaces relative to some parent surface. See the - * interface description and xdg_surface.get_popup for details. - */ void (*create_positioner)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * create a shell surface from a surface - * - * This creates an xdg_surface for the given surface. While - * xdg_surface itself is not a role, the corresponding surface may - * only be assigned a role extending xdg_surface, such as - * xdg_toplevel or xdg_popup. It is illegal to create an - * xdg_surface for a wl_surface which already has an assigned role - * and this will result in a role error. - * - * This creates an xdg_surface for the given surface. An - * xdg_surface is used as basis to define a role to a given - * surface, such as xdg_toplevel or xdg_popup. It also manages - * functionality shared between xdg_surface based surface roles. - * - * See the documentation of xdg_surface for more details about what - * an xdg_surface is and how it is used. - */ void (*get_xdg_surface)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *surface); - /** - * respond to a ping event - * - * A client must respond to a ping event with a pong request or - * the client may be deemed unresponsive. See xdg_wm_base.ping and - * xdg_wm_base.error.unresponsive. - * @param serial serial of the ping event - */ void (*pong)(struct wl_client *client, struct wl_resource *resource, uint32_t serial); @@ -486,34 +136,13 @@ struct xdg_wm_base_interface { #define XDG_WM_BASE_PING 0 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_PING_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_CREATE_POSITIONER_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ #define XDG_WM_BASE_PONG_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - * Sends an ping event to the client owning the resource. - * @param resource_ The client's resource - * @param serial pass this to the pong request - */ static inline void xdg_wm_base_send_ping(struct wl_resource *resource_, uint32_t serial) { @@ -523,22 +152,12 @@ xdg_wm_base_send_ping(struct wl_resource *resource_, uint32_t serial) #ifndef XDG_POSITIONER_ERROR_ENUM #define XDG_POSITIONER_ERROR_ENUM enum xdg_positioner_error { - /** - * invalid input provided - */ XDG_POSITIONER_ERROR_INVALID_INPUT = 0, }; -#endif /* XDG_POSITIONER_ERROR_ENUM */ +#endif #ifndef XDG_POSITIONER_ERROR_ENUM_IS_VALID #define XDG_POSITIONER_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_positioner - * Validate a xdg_positioner error value. - * - * @return true on success, false on error. - * @ref xdg_positioner_error - */ static inline bool xdg_positioner_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -548,7 +167,7 @@ xdg_positioner_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_POSITIONER_ERROR_ENUM_IS_VALID */ +#endif #ifndef XDG_POSITIONER_ANCHOR_ENUM #define XDG_POSITIONER_ANCHOR_ENUM @@ -563,17 +182,10 @@ enum xdg_positioner_anchor { XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7, XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8, }; -#endif /* XDG_POSITIONER_ANCHOR_ENUM */ +#endif #ifndef XDG_POSITIONER_ANCHOR_ENUM_IS_VALID #define XDG_POSITIONER_ANCHOR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_positioner - * Validate a xdg_positioner anchor value. - * - * @return true on success, false on error. - * @ref xdg_positioner_anchor - */ static inline bool xdg_positioner_anchor_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -599,7 +211,7 @@ xdg_positioner_anchor_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_POSITIONER_ANCHOR_ENUM_IS_VALID */ +#endif #ifndef XDG_POSITIONER_GRAVITY_ENUM #define XDG_POSITIONER_GRAVITY_ENUM @@ -614,17 +226,10 @@ enum xdg_positioner_gravity { XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7, XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8, }; -#endif /* XDG_POSITIONER_GRAVITY_ENUM */ +#endif #ifndef XDG_POSITIONER_GRAVITY_ENUM_IS_VALID #define XDG_POSITIONER_GRAVITY_ENUM_IS_VALID -/** - * @ingroup iface_xdg_positioner - * Validate a xdg_positioner gravity value. - * - * @return true on success, false on error. - * @ref xdg_positioner_gravity - */ static inline bool xdg_positioner_gravity_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -650,125 +255,23 @@ xdg_positioner_gravity_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_POSITIONER_GRAVITY_ENUM_IS_VALID */ +#endif #ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM #define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM -/** - * @ingroup iface_xdg_positioner - * constraint adjustments - * - * The constraint adjustment value define ways the compositor will adjust - * the position of the surface, if the unadjusted position would result - * in the surface being partly constrained. - * - * Whether a surface is considered 'constrained' is left to the compositor - * to determine. For example, the surface may be partly outside the - * compositor's defined 'work area', thus necessitating the child surface's - * position be adjusted until it is entirely inside the work area. - * - * The adjustments can be combined, according to a defined precedence: 1) - * Flip, 2) Slide, 3) Resize. - */ enum xdg_positioner_constraint_adjustment { - /** - * don't move the child surface when constrained - * - * Don't alter the surface position even if it is constrained on - * some axis, for example partially outside the edge of an output. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0, - /** - * move along the x axis until unconstrained - * - * Slide the surface along the x axis until it is no longer - * constrained. - * - * First try to slide towards the direction of the gravity on the x - * axis until either the edge in the opposite direction of the - * gravity is unconstrained or the edge in the direction of the - * gravity is constrained. - * - * Then try to slide towards the opposite direction of the gravity - * on the x axis until either the edge in the direction of the - * gravity is unconstrained or the edge in the opposite direction - * of the gravity is constrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1, - /** - * move along the y axis until unconstrained - * - * Slide the surface along the y axis until it is no longer - * constrained. - * - * First try to slide towards the direction of the gravity on the y - * axis until either the edge in the opposite direction of the - * gravity is unconstrained or the edge in the direction of the - * gravity is constrained. - * - * Then try to slide towards the opposite direction of the gravity - * on the y axis until either the edge in the direction of the - * gravity is unconstrained or the edge in the opposite direction - * of the gravity is constrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2, - /** - * invert the anchor and gravity on the x axis - * - * Invert the anchor and gravity on the x axis if the surface is - * constrained on the x axis. For example, if the left edge of the - * surface is constrained, the gravity is 'left' and the anchor is - * 'left', change the gravity to 'right' and the anchor to 'right'. - * - * If the adjusted position also ends up being constrained, the - * resulting position of the flip_x adjustment will be the one - * before the adjustment. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4, - /** - * invert the anchor and gravity on the y axis - * - * Invert the anchor and gravity on the y axis if the surface is - * constrained on the y axis. For example, if the bottom edge of - * the surface is constrained, the gravity is 'bottom' and the - * anchor is 'bottom', change the gravity to 'top' and the anchor - * to 'top'. - * - * The adjusted position is calculated given the original anchor - * rectangle and offset, but with the new flipped anchor and - * gravity values. - * - * If the adjusted position also ends up being constrained, the - * resulting position of the flip_y adjustment will be the one - * before the adjustment. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8, - /** - * horizontally resize the surface - * - * Resize the surface horizontally so that it is completely - * unconstrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16, - /** - * vertically resize the surface - * - * Resize the surface vertically so that it is completely - * unconstrained. - */ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32, }; -#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM */ +#endif #ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM_IS_VALID #define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM_IS_VALID -/** - * @ingroup iface_xdg_positioner - * Validate a xdg_positioner constraint_adjustment value. - * - * @return true on success, false on error. - * @ref xdg_positioner_constraint_adjustment - */ static inline bool xdg_positioner_constraint_adjustment_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -788,268 +291,71 @@ xdg_positioner_constraint_adjustment_is_valid(uint32_t value, uint32_t version) valid |= XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y; return (value & ~valid) == 0; } -#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_xdg_positioner - * @struct xdg_positioner_interface - */ struct xdg_positioner_interface { - /** - * destroy the xdg_positioner object - * - * Notify the compositor that the xdg_positioner will no longer - * be used. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the size of the to-be positioned rectangle - * - * Set the size of the surface that is to be positioned with the - * positioner object. The size is in surface-local coordinates and - * corresponds to the window geometry. See - * xdg_surface.set_window_geometry. - * - * If a zero or negative size is set the invalid_input error is - * raised. - * @param width width of positioned rectangle - * @param height height of positioned rectangle - */ void (*set_size)(struct wl_client *client, struct wl_resource *resource, int32_t width, int32_t height); - /** - * set the anchor rectangle within the parent surface - * - * Specify the anchor rectangle within the parent surface that - * the child surface will be placed relative to. The rectangle is - * relative to the window geometry as defined by - * xdg_surface.set_window_geometry of the parent surface. - * - * When the xdg_positioner object is used to position a child - * surface, the anchor rectangle may not extend outside the window - * geometry of the positioned child's parent surface. - * - * If a negative size is set the invalid_input error is raised. - * @param x x position of anchor rectangle - * @param y y position of anchor rectangle - * @param width width of anchor rectangle - * @param height height of anchor rectangle - */ void (*set_anchor_rect)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * set anchor rectangle anchor - * - * Defines the anchor point for the anchor rectangle. The - * specified anchor is used derive an anchor point that the child - * surface will be positioned relative to. If a corner anchor is - * set (e.g. 'top_left' or 'bottom_right'), the anchor point will - * be at the specified corner; otherwise, the derived anchor point - * will be centered on the specified edge, or in the center of the - * anchor rectangle if no edge is specified. - * @param anchor anchor - */ void (*set_anchor)(struct wl_client *client, struct wl_resource *resource, uint32_t anchor); - /** - * set child surface gravity - * - * Defines in what direction a surface should be positioned, - * relative to the anchor point of the parent surface. If a corner - * gravity is specified (e.g. 'bottom_right' or 'top_left'), then - * the child surface will be placed towards the specified gravity; - * otherwise, the child surface will be centered over the anchor - * point on any axis that had no gravity specified. If the gravity - * is not in the ‘gravity’ enum, an invalid_input error is - * raised. - * @param gravity gravity direction - */ void (*set_gravity)(struct wl_client *client, struct wl_resource *resource, uint32_t gravity); - /** - * set the adjustment to be done when constrained - * - * Specify how the window should be positioned if the originally - * intended position caused the surface to be constrained, meaning - * at least partially outside positioning boundaries set by the - * compositor. The adjustment is set by constructing a bitmask - * describing the adjustment to be made when the surface is - * constrained on that axis. - * - * If no bit for one axis is set, the compositor will assume that - * the child surface should not change its position on that axis - * when constrained. - * - * If more than one bit for one axis is set, the order of how - * adjustments are applied is specified in the corresponding - * adjustment descriptions. - * - * The default adjustment is none. - * @param constraint_adjustment bit mask of constraint adjustments - */ void (*set_constraint_adjustment)(struct wl_client *client, struct wl_resource *resource, uint32_t constraint_adjustment); - /** - * set surface position offset - * - * Specify the surface position offset relative to the position - * of the anchor on the anchor rectangle and the anchor on the - * surface. For example if the anchor of the anchor rectangle is at - * (x, y), the surface has the gravity bottom|right, and the offset - * is (ox, oy), the calculated surface position will be (x + ox, y - * + oy). The offset position of the surface is the one used for - * constraint testing. See set_constraint_adjustment. - * - * An example use case is placing a popup menu on top of a user - * interface element, while aligning the user interface element of - * the parent surface with some user interface element placed - * somewhere in the popup surface. - * @param x surface position x offset - * @param y surface position y offset - */ void (*set_offset)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y); - /** - * continuously reconstrain the surface - * - * When set reactive, the surface is reconstrained if the - * conditions used for constraining changed, e.g. the parent window - * moved. - * - * If the conditions changed and the popup was reconstrained, an - * xdg_popup.configure event is sent with updated geometry, - * followed by an xdg_surface.configure event. - * @since 3 - */ void (*set_reactive)(struct wl_client *client, struct wl_resource *resource); - /** - * - * - * Set the parent window geometry the compositor should use when - * positioning the popup. The compositor may use this information - * to determine the future state the popup should be constrained - * using. If this doesn't match the dimension of the parent the - * popup is eventually positioned against, the behavior is - * undefined. - * - * The arguments are given in the surface-local coordinate space. - * @param parent_width future window geometry width of parent - * @param parent_height future window geometry height of parent - * @since 3 - */ void (*set_parent_size)(struct wl_client *client, struct wl_resource *resource, int32_t parent_width, int32_t parent_height); - /** - * set parent configure this is a response to - * - * Set the serial of an xdg_surface.configure event this - * positioner will be used in response to. The compositor may use - * this information together with set_parent_size to determine what - * future state the popup should be constrained using. - * @param serial serial of parent configure event - * @since 3 - */ void (*set_parent_configure)(struct wl_client *client, struct wl_resource *resource, uint32_t serial); }; -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_ANCHOR_RECT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_ANCHOR_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_GRAVITY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_OFFSET_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_PARENT_SIZE_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_positioner - */ #define XDG_POSITIONER_SET_PARENT_CONFIGURE_SINCE_VERSION 3 #ifndef XDG_SURFACE_ERROR_ENUM #define XDG_SURFACE_ERROR_ENUM enum xdg_surface_error { - /** - * Surface was not fully constructed - */ XDG_SURFACE_ERROR_NOT_CONSTRUCTED = 1, - /** - * Surface was already constructed - */ XDG_SURFACE_ERROR_ALREADY_CONSTRUCTED = 2, - /** - * Attaching a buffer to an unconfigured surface - */ XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER = 3, - /** - * Invalid serial number when acking a configure event - */ XDG_SURFACE_ERROR_INVALID_SERIAL = 4, - /** - * Width or height was zero or negative - */ XDG_SURFACE_ERROR_INVALID_SIZE = 5, - /** - * Surface was destroyed before its role object - */ XDG_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 6, }; -#endif /* XDG_SURFACE_ERROR_ENUM */ +#endif #ifndef XDG_SURFACE_ERROR_ENUM_IS_VALID #define XDG_SURFACE_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_surface - * Validate a xdg_surface error value. - * - * @return true on success, false on error. - * @ref xdg_surface_error - */ static inline bool xdg_surface_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1069,133 +375,25 @@ xdg_surface_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_SURFACE_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_xdg_surface - * @struct xdg_surface_interface - */ struct xdg_surface_interface { - /** - * destroy the xdg_surface - * - * Destroy the xdg_surface object. An xdg_surface must only be - * destroyed after its role object has been destroyed, otherwise a - * defunct_role_object error is raised. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * assign the xdg_toplevel surface role - * - * This creates an xdg_toplevel object for the given xdg_surface - * and gives the associated wl_surface the xdg_toplevel role. - * - * See the documentation of xdg_toplevel for more details about - * what an xdg_toplevel is and how it is used. - */ void (*get_toplevel)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * assign the xdg_popup surface role - * - * This creates an xdg_popup object for the given xdg_surface and - * gives the associated wl_surface the xdg_popup role. - * - * If null is passed as a parent, a parent surface must be - * specified using some other protocol, before committing the - * initial state. - * - * See the documentation of xdg_popup for more details about what - * an xdg_popup is and how it is used. - */ void (*get_popup)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *parent, struct wl_resource *positioner); - /** - * set the new window geometry - * - * The window geometry of a surface is its "visible bounds" from - * the user's perspective. Client-side decorations often have - * invisible portions like drop-shadows which should be ignored for - * the purposes of aligning, placing and constraining windows. - * - * The window geometry is double buffered, and will be applied at - * the time wl_surface.commit of the corresponding wl_surface is - * called. - * - * When maintaining a position, the compositor should treat the (x, - * y) coordinate of the window geometry as the top left corner of - * the window. A client changing the (x, y) window geometry - * coordinate should in general not alter the position of the - * window. - * - * Once the window geometry of the surface is set, it is not - * possible to unset it, and it will remain the same until - * set_window_geometry is called again, even if a new subsurface or - * buffer is attached. - * - * If never set, the value is the full bounds of the surface, - * including any subsurfaces. This updates dynamically on every - * commit. This unset is meant for extremely simple clients. - * - * The arguments are given in the surface-local coordinate space of - * the wl_surface associated with this xdg_surface. - * - * The width and height must be greater than zero. Setting an - * invalid size will raise an invalid_size error. When applied, the - * effective window geometry will be the set window geometry - * clamped to the bounding rectangle of the combined geometry of - * the surface of the xdg_surface and the associated subsurfaces. - */ void (*set_window_geometry)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * ack a configure event - * - * When a configure event is received, if a client commits the - * surface in response to the configure event, then the client must - * make an ack_configure request sometime before the commit - * request, passing along the serial of the configure event. - * - * For instance, for toplevel surfaces the compositor might use - * this information to move a surface to the top left only when the - * client has drawn itself for the maximized or fullscreen state. - * - * If the client receives multiple configure events before it can - * respond to one, it only has to ack the last configure event. - * Acking a configure event that was never sent raises an - * invalid_serial error. - * - * A client is not required to commit immediately after sending an - * ack_configure request - it may even ack_configure several times - * before its next surface commit. - * - * A client may send multiple ack_configure requests before - * committing, but only the last request sent before a commit - * indicates which configure event the client really is responding - * to. - * - * Sending an ack_configure request consumes the serial number sent - * with the request, as well as serial numbers sent by all - * configure events sent on this xdg_surface prior to the configure - * event referenced by the committed serial. - * - * It is an error to issue multiple ack_configure requests - * referencing a serial from the same configure event, or to issue - * an ack_configure request referencing a serial from a configure - * event issued before the event identified by the last - * ack_configure request for the same xdg_surface. Doing so will - * raise an invalid_serial error. - * @param serial the serial from the configure event - */ void (*ack_configure)(struct wl_client *client, struct wl_resource *resource, uint32_t serial); @@ -1203,38 +401,14 @@ struct xdg_surface_interface { #define XDG_SURFACE_CONFIGURE 0 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_GET_TOPLEVEL_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_GET_POPUP_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_SET_WINDOW_GEOMETRY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ #define XDG_SURFACE_ACK_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - * Sends an configure event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial of the configure event - */ static inline void xdg_surface_send_configure(struct wl_resource *resource_, uint32_t serial) { @@ -1244,30 +418,14 @@ xdg_surface_send_configure(struct wl_resource *resource_, uint32_t serial) #ifndef XDG_TOPLEVEL_ERROR_ENUM #define XDG_TOPLEVEL_ERROR_ENUM enum xdg_toplevel_error { - /** - * provided value is not a valid variant of the resize_edge enum - */ XDG_TOPLEVEL_ERROR_INVALID_RESIZE_EDGE = 0, - /** - * invalid parent toplevel - */ XDG_TOPLEVEL_ERROR_INVALID_PARENT = 1, - /** - * client provided an invalid min or max size - */ XDG_TOPLEVEL_ERROR_INVALID_SIZE = 2, }; -#endif /* XDG_TOPLEVEL_ERROR_ENUM */ +#endif #ifndef XDG_TOPLEVEL_ERROR_ENUM_IS_VALID #define XDG_TOPLEVEL_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_toplevel - * Validate a xdg_toplevel error value. - * - * @return true on success, false on error. - * @ref xdg_toplevel_error - */ static inline bool xdg_toplevel_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1281,17 +439,10 @@ xdg_toplevel_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_TOPLEVEL_ERROR_ENUM_IS_VALID */ +#endif #ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM #define XDG_TOPLEVEL_RESIZE_EDGE_ENUM -/** - * @ingroup iface_xdg_toplevel - * edge values for resizing - * - * These values are used to indicate which edge of a surface - * is being dragged in a resize operation. - */ enum xdg_toplevel_resize_edge { XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0, XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1, @@ -1303,17 +454,10 @@ enum xdg_toplevel_resize_edge { XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9, XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10, }; -#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM */ +#endif #ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM_IS_VALID #define XDG_TOPLEVEL_RESIZE_EDGE_ENUM_IS_VALID -/** - * @ingroup iface_xdg_toplevel - * Validate a xdg_toplevel resize_edge value. - * - * @return true on success, false on error. - * @ref xdg_toplevel_resize_edge - */ static inline bool xdg_toplevel_resize_edge_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1339,124 +483,28 @@ xdg_toplevel_resize_edge_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM_IS_VALID */ +#endif #ifndef XDG_TOPLEVEL_STATE_ENUM #define XDG_TOPLEVEL_STATE_ENUM -/** - * @ingroup iface_xdg_toplevel - * types of state on the surface - * - * The different state values used on the surface. This is designed for - * state values like maximized, fullscreen. It is paired with the - * configure event to ensure that both the client and the compositor - * setting the state can be synchronized. - * - * States set in this way are double-buffered. They will get applied on - * the next commit. - */ enum xdg_toplevel_state { - /** - * the surface is maximized - * the surface is maximized - * - * The surface is maximized. The window geometry specified in the - * configure event must be obeyed by the client. - * - * The client should draw without shadow or other decoration - * outside of the window geometry. - */ XDG_TOPLEVEL_STATE_MAXIMIZED = 1, - /** - * the surface is fullscreen - * the surface is fullscreen - * - * The surface is fullscreen. The window geometry specified in - * the configure event is a maximum; the client cannot resize - * beyond it. For a surface to cover the whole fullscreened area, - * the geometry dimensions must be obeyed by the client. For more - * details, see xdg_toplevel.set_fullscreen. - */ XDG_TOPLEVEL_STATE_FULLSCREEN = 2, - /** - * the surface is being resized - * the surface is being resized - * - * The surface is being resized. The window geometry specified in - * the configure event is a maximum; the client cannot resize - * beyond it. Clients that have aspect ratio or cell sizing - * configuration can use a smaller size, however. - */ XDG_TOPLEVEL_STATE_RESIZING = 3, - /** - * the surface is now activated - * the surface is now activated - * - * Client window decorations should be painted as if the window - * is active. Do not assume this means that the window actually has - * keyboard or pointer focus. - */ XDG_TOPLEVEL_STATE_ACTIVATED = 4, - /** - * the surface’s left edge is tiled - * - * The window is currently in a tiled layout and the left edge is - * considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_LEFT = 5, - /** - * the surface’s right edge is tiled - * - * The window is currently in a tiled layout and the right edge - * is considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_RIGHT = 6, - /** - * the surface’s top edge is tiled - * - * The window is currently in a tiled layout and the top edge is - * considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_TOP = 7, - /** - * the surface’s bottom edge is tiled - * - * The window is currently in a tiled layout and the bottom edge - * is considered to be adjacent to another part of the tiling grid. - * @since 2 - */ XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8, }; -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_RIGHT_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_TOP_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_STATE_TILED_BOTTOM_SINCE_VERSION 2 -#endif /* XDG_TOPLEVEL_STATE_ENUM */ +#endif #ifndef XDG_TOPLEVEL_STATE_ENUM_IS_VALID #define XDG_TOPLEVEL_STATE_ENUM_IS_VALID -/** - * @ingroup iface_xdg_toplevel - * Validate a xdg_toplevel state value. - * - * @return true on success, false on error. - * @ref xdg_toplevel_state - */ static inline bool xdg_toplevel_state_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1480,39 +528,20 @@ xdg_toplevel_state_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_TOPLEVEL_STATE_ENUM_IS_VALID */ +#endif #ifndef XDG_TOPLEVEL_WM_CAPABILITIES_ENUM #define XDG_TOPLEVEL_WM_CAPABILITIES_ENUM enum xdg_toplevel_wm_capabilities { - /** - * show_window_menu is available - */ XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU = 1, - /** - * set_maximized and unset_maximized are available - */ XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE = 2, - /** - * set_fullscreen and unset_fullscreen are available - */ XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN = 3, - /** - * set_minimized is available - */ XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE = 4, }; -#endif /* XDG_TOPLEVEL_WM_CAPABILITIES_ENUM */ +#endif #ifndef XDG_TOPLEVEL_WM_CAPABILITIES_ENUM_IS_VALID #define XDG_TOPLEVEL_WM_CAPABILITIES_ENUM_IS_VALID -/** - * @ingroup iface_xdg_toplevel - * Validate a xdg_toplevel wm_capabilities value. - * - * @return true on success, false on error. - * @ref xdg_toplevel_wm_capabilities - */ static inline bool xdg_toplevel_wm_capabilities_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1528,398 +557,52 @@ xdg_toplevel_wm_capabilities_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_TOPLEVEL_WM_CAPABILITIES_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_xdg_toplevel - * @struct xdg_toplevel_interface - */ struct xdg_toplevel_interface { - /** - * destroy the xdg_toplevel - * - * This request destroys the role surface and unmaps the surface; - * see "Unmapping" behavior in interface section for details. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the parent of this surface - * - * Set the "parent" of this surface. This surface should be - * stacked above the parent surface and all other ancestor - * surfaces. - * - * Parent surfaces should be set on dialogs, toolboxes, or other - * "auxiliary" surfaces, so that the parent is raised when the - * dialog is raised. - * - * Setting a null parent for a child surface unsets its parent. - * Setting a null parent for a surface which currently has no - * parent is a no-op. - * - * Only mapped surfaces can have child surfaces. Setting a parent - * which is not mapped is equivalent to setting a null parent. If a - * surface becomes unmapped, its children's parent is set to the - * parent of the now-unmapped surface. If the now-unmapped surface - * has no parent, its children's parent is unset. If the - * now-unmapped surface becomes mapped again, its parent-child - * relationship is not restored. - * - * The parent toplevel must not be one of the child toplevel's - * descendants, and the parent must be different from the child - * toplevel, otherwise the invalid_parent protocol error is raised. - */ void (*set_parent)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *parent); - /** - * set surface title - * - * Set a short title for the surface. - * - * This string may be used to identify the surface in a task bar, - * window list, or other user interface elements provided by the - * compositor. - * - * The string must be encoded in UTF-8. - */ void (*set_title)(struct wl_client *client, struct wl_resource *resource, const char *title); - /** - * set application ID - * - * Set an application identifier for the surface. - * - * The app ID identifies the general class of applications to which - * the surface belongs. The compositor can use this to group - * multiple surfaces together, or to determine how to launch a new - * application. - * - * For D-Bus activatable applications, the app ID is used as the - * D-Bus service name. - * - * The compositor shell will try to group application surfaces - * together by their app ID. As a best practice, it is suggested to - * select app ID's that match the basename of the application's - * .desktop file. For example, "org.freedesktop.FooViewer" where - * the .desktop file is "org.freedesktop.FooViewer.desktop". - * - * Like other properties, a set_app_id request can be sent after - * the xdg_toplevel has been mapped to update the property. - * - * See the desktop-entry specification [0] for more details on - * application identifiers and how they relate to well-known D-Bus - * names and .desktop files. - * - * [0] https://standards.freedesktop.org/desktop-entry-spec/ - */ void (*set_app_id)(struct wl_client *client, struct wl_resource *resource, const char *app_id); - /** - * show the window menu - * - * Clients implementing client-side decorations might want to - * show a context menu when right-clicking on the decorations, - * giving the user a menu that they can use to maximize or minimize - * the window. - * - * This request asks the compositor to pop up such a window menu at - * the given position, relative to the local surface coordinates of - * the parent surface. There are no guarantees as to what menu - * items the window menu contains, or even if a window menu will be - * drawn at all. - * - * This request must be used in response to some sort of user - * action like a button press, key press, or touch down event. - * @param seat the wl_seat of the user event - * @param serial the serial of the user event - * @param x the x position to pop up the window menu at - * @param y the y position to pop up the window menu at - */ void (*show_window_menu)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, uint32_t serial, int32_t x, int32_t y); - /** - * start an interactive move - * - * Start an interactive, user-driven move of the surface. - * - * This request must be used in response to some sort of user - * action like a button press, key press, or touch down event. The - * passed serial is used to determine the type of interactive move - * (touch, pointer, etc). - * - * The server may ignore move requests depending on the state of - * the surface (e.g. fullscreen or maximized), or if the passed - * serial is no longer valid. - * - * If triggered, the surface will lose the focus of the device - * (wl_pointer, wl_touch, etc) used for the move. It is up to the - * compositor to visually indicate that the move is taking place, - * such as updating a pointer cursor, during the move. There is no - * guarantee that the device focus will return when the move is - * completed. - * @param seat the wl_seat of the user event - * @param serial the serial of the user event - */ void (*move)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, uint32_t serial); - /** - * start an interactive resize - * - * Start a user-driven, interactive resize of the surface. - * - * This request must be used in response to some sort of user - * action like a button press, key press, or touch down event. The - * passed serial is used to determine the type of interactive - * resize (touch, pointer, etc). - * - * The server may ignore resize requests depending on the state of - * the surface (e.g. fullscreen or maximized). - * - * If triggered, the client will receive configure events with the - * "resize" state enum value and the expected sizes. See the - * "resize" enum value for more details about what is required. The - * client must also acknowledge configure events using - * "ack_configure". After the resize is completed, the client will - * receive another "configure" event without the resize state. - * - * If triggered, the surface also will lose the focus of the device - * (wl_pointer, wl_touch, etc) used for the resize. It is up to the - * compositor to visually indicate that the resize is taking place, - * such as updating a pointer cursor, during the resize. There is - * no guarantee that the device focus will return when the resize - * is completed. - * - * The edges parameter specifies how the surface should be resized, - * and is one of the values of the resize_edge enum. Values not - * matching a variant of the enum will cause a protocol error. The - * compositor may use this information to update the surface - * position for example when dragging the top left corner. The - * compositor may also use this information to adapt its behavior, - * e.g. choose an appropriate cursor image. - * @param seat the wl_seat of the user event - * @param serial the serial of the user event - * @param edges which edge or corner is being dragged - */ void (*resize)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, uint32_t serial, uint32_t edges); - /** - * set the maximum size - * - * Set a maximum size for the window. - * - * The client can specify a maximum size so that the compositor - * does not try to configure the window beyond this size. - * - * The width and height arguments are in window geometry - * coordinates. See xdg_surface.set_window_geometry. - * - * Values set in this way are double-buffered. They will get - * applied on the next commit. - * - * The compositor can use this information to allow or disallow - * different states like maximize or fullscreen and draw accurate - * animations. - * - * Similarly, a tiling window manager may use this information to - * place and resize client windows in a more effective way. - * - * The client should not rely on the compositor to obey the maximum - * size. The compositor may decide to ignore the values set by the - * client and request a larger size. - * - * If never set, or a value of zero in the request, means that the - * client has no expected maximum size in the given dimension. As a - * result, a client wishing to reset the maximum size to an - * unspecified state can use zero for width and height in the - * request. - * - * Requesting a maximum size to be smaller than the minimum size of - * a surface is illegal and will result in an invalid_size error. - * - * The width and height must be greater than or equal to zero. - * Using strictly negative values for width or height will result - * in a invalid_size error. - */ void (*set_max_size)(struct wl_client *client, struct wl_resource *resource, int32_t width, int32_t height); - /** - * set the minimum size - * - * Set a minimum size for the window. - * - * The client can specify a minimum size so that the compositor - * does not try to configure the window below this size. - * - * The width and height arguments are in window geometry - * coordinates. See xdg_surface.set_window_geometry. - * - * Values set in this way are double-buffered. They will get - * applied on the next commit. - * - * The compositor can use this information to allow or disallow - * different states like maximize or fullscreen and draw accurate - * animations. - * - * Similarly, a tiling window manager may use this information to - * place and resize client windows in a more effective way. - * - * The client should not rely on the compositor to obey the minimum - * size. The compositor may decide to ignore the values set by the - * client and request a smaller size. - * - * If never set, or a value of zero in the request, means that the - * client has no expected minimum size in the given dimension. As a - * result, a client wishing to reset the minimum size to an - * unspecified state can use zero for width and height in the - * request. - * - * Requesting a minimum size to be larger than the maximum size of - * a surface is illegal and will result in an invalid_size error. - * - * The width and height must be greater than or equal to zero. - * Using strictly negative values for width and height will result - * in a invalid_size error. - */ void (*set_min_size)(struct wl_client *client, struct wl_resource *resource, int32_t width, int32_t height); - /** - * maximize the window - * - * Maximize the surface. - * - * After requesting that the surface should be maximized, the - * compositor will respond by emitting a configure event. Whether - * this configure actually sets the window maximized is subject to - * compositor policies. The client must then update its content, - * drawing in the configured state. The client must also - * acknowledge the configure when committing the new content (see - * ack_configure). - * - * It is up to the compositor to decide how and where to maximize - * the surface, for example which output and what region of the - * screen should be used. - * - * If the surface was already maximized, the compositor will still - * emit a configure event with the "maximized" state. - * - * If the surface is in a fullscreen state, this request has no - * direct effect. It may alter the state the surface is returned to - * when unmaximized unless overridden by the compositor. - */ void (*set_maximized)(struct wl_client *client, struct wl_resource *resource); - /** - * unmaximize the window - * - * Unmaximize the surface. - * - * After requesting that the surface should be unmaximized, the - * compositor will respond by emitting a configure event. Whether - * this actually un-maximizes the window is subject to compositor - * policies. If available and applicable, the compositor will - * include the window geometry dimensions the window had prior to - * being maximized in the configure event. The client must then - * update its content, drawing it in the configured state. The - * client must also acknowledge the configure when committing the - * new content (see ack_configure). - * - * It is up to the compositor to position the surface after it was - * unmaximized; usually the position the surface had before - * maximizing, if applicable. - * - * If the surface was already not maximized, the compositor will - * still emit a configure event without the "maximized" state. - * - * If the surface is in a fullscreen state, this request has no - * direct effect. It may alter the state the surface is returned to - * when unmaximized unless overridden by the compositor. - */ void (*unset_maximized)(struct wl_client *client, struct wl_resource *resource); - /** - * set the window as fullscreen on an output - * - * Make the surface fullscreen. - * - * After requesting that the surface should be fullscreened, the - * compositor will respond by emitting a configure event. Whether - * the client is actually put into a fullscreen state is subject to - * compositor policies. The client must also acknowledge the - * configure when committing the new content (see ack_configure). - * - * The output passed by the request indicates the client's - * preference as to which display it should be set fullscreen on. - * If this value is NULL, it's up to the compositor to choose which - * display will be used to map this surface. - * - * If the surface doesn't cover the whole output, the compositor - * will position the surface in the center of the output and - * compensate with with border fill covering the rest of the - * output. The content of the border fill is undefined, but should - * be assumed to be in some way that attempts to blend into the - * surrounding area (e.g. solid black). - * - * If the fullscreened surface is not opaque, the compositor must - * make sure that other screen content not part of the same surface - * tree (made up of subsurfaces, popups or similarly coupled - * surfaces) are not visible below the fullscreened surface. - */ void (*set_fullscreen)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *output); - /** - * unset the window as fullscreen - * - * Make the surface no longer fullscreen. - * - * After requesting that the surface should be unfullscreened, the - * compositor will respond by emitting a configure event. Whether - * this actually removes the fullscreen state of the client is - * subject to compositor policies. - * - * Making a surface unfullscreen sets states for the surface based - * on the following: * the state(s) it may have had before becoming - * fullscreen * any state(s) decided by the compositor * any - * state(s) requested by the client while the surface was - * fullscreen - * - * The compositor may include the previous window geometry - * dimensions in the configure event, if applicable. - * - * The client must also acknowledge the configure when committing - * the new content (see ack_configure). - */ void (*unset_fullscreen)(struct wl_client *client, struct wl_resource *resource); - /** - * set the window as minimized - * - * Request that the compositor minimize your surface. There is no - * way to know if the surface is currently minimized, nor is there - * any way to unset minimization on this surface. - * - * If you are looking to throttle redrawing when minimized, please - * instead use the wl_surface.frame event for this, as this will - * also work with live previews on windows in Alt-Tab, Expose or - * similar compositor features. - */ void (*set_minimized)(struct wl_client *client, struct wl_resource *resource); }; @@ -1929,119 +612,44 @@ struct xdg_toplevel_interface { #define XDG_TOPLEVEL_CONFIGURE_BOUNDS 2 #define XDG_TOPLEVEL_WM_CAPABILITIES 3 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_CLOSE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION 4 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION 5 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_PARENT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_TITLE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_APP_ID_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SHOW_WINDOW_MENU_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_MOVE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_RESIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MAX_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MIN_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_UNSET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_UNSET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ #define XDG_TOPLEVEL_SET_MINIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - * Sends an configure event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void xdg_toplevel_send_configure(struct wl_resource *resource_, int32_t width, int32_t height, struct wl_array *states) { wl_resource_post_event(resource_, XDG_TOPLEVEL_CONFIGURE, width, height, states); } -/** - * @ingroup iface_xdg_toplevel - * Sends an close event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void xdg_toplevel_send_close(struct wl_resource *resource_) { wl_resource_post_event(resource_, XDG_TOPLEVEL_CLOSE); } -/** - * @ingroup iface_xdg_toplevel - * Sends an configure_bounds event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void xdg_toplevel_send_configure_bounds(struct wl_resource *resource_, int32_t width, int32_t height) { wl_resource_post_event(resource_, XDG_TOPLEVEL_CONFIGURE_BOUNDS, width, height); } -/** - * @ingroup iface_xdg_toplevel - * Sends an wm_capabilities event to the client owning the resource. - * @param resource_ The client's resource - * @param capabilities array of 32-bit capabilities - */ static inline void xdg_toplevel_send_wm_capabilities(struct wl_resource *resource_, struct wl_array *capabilities) { @@ -2051,22 +659,12 @@ xdg_toplevel_send_wm_capabilities(struct wl_resource *resource_, struct wl_array #ifndef XDG_POPUP_ERROR_ENUM #define XDG_POPUP_ERROR_ENUM enum xdg_popup_error { - /** - * tried to grab after being mapped - */ XDG_POPUP_ERROR_INVALID_GRAB = 0, }; -#endif /* XDG_POPUP_ERROR_ENUM */ +#endif #ifndef XDG_POPUP_ERROR_ENUM_IS_VALID #define XDG_POPUP_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_popup - * Validate a xdg_popup error value. - * - * @return true on success, false on error. - * @ref xdg_popup_error - */ static inline bool xdg_popup_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2076,106 +674,15 @@ xdg_popup_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_POPUP_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_xdg_popup - * @struct xdg_popup_interface - */ struct xdg_popup_interface { - /** - * remove xdg_popup interface - * - * This destroys the popup. Explicitly destroying the xdg_popup - * object will also dismiss the popup, and unmap the surface. - * - * If this xdg_popup is not the "topmost" popup, a protocol error - * will be sent. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * make the popup take an explicit grab - * - * This request makes the created popup take an explicit grab. An - * explicit grab will be dismissed when the user dismisses the - * popup, or when the client destroys the xdg_popup. This can be - * done by the user clicking outside the surface, using the - * keyboard, or even locking the screen through closing the lid or - * a timeout. - * - * If the compositor denies the grab, the popup will be immediately - * dismissed. - * - * This request must be used in response to some sort of user - * action like a button press, key press, or touch down event. The - * serial number of the event should be passed as 'serial'. - * - * The parent of a grabbing popup must either be an xdg_toplevel - * surface or another xdg_popup with an explicit grab. If the - * parent is another xdg_popup it means that the popups are nested, - * with this popup now being the topmost popup. - * - * Nested popups must be destroyed in the reverse order they were - * created in, e.g. the only popup you are allowed to destroy at - * all times is the topmost one. - * - * When compositors choose to dismiss a popup, they may dismiss - * every nested grabbing popup as well. When a compositor dismisses - * popups, it will follow the same dismissing order as required - * from the client. - * - * If the topmost grabbing popup is destroyed, the grab will be - * returned to the parent of the popup, if that parent previously - * had an explicit grab. - * - * If the parent is a grabbing popup which has already been - * dismissed, this popup will be immediately dismissed. If the - * parent is a popup that did not take an explicit grab, an error - * will be raised. - * - * During a popup grab, the client owning the grab will receive - * pointer and touch events for all their surfaces as normal - * (similar to an "owner-events" grab in X11 parlance), while the - * top most grabbing popup will always have keyboard focus. - * @param seat the wl_seat of the user event - * @param serial the serial of the user event - */ void (*grab)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, uint32_t serial); - /** - * recalculate the popup's location - * - * Reposition an already-mapped popup. The popup will be placed - * given the details in the passed xdg_positioner object, and a - * xdg_popup.repositioned followed by xdg_popup.configure and - * xdg_surface.configure will be emitted in response. Any - * parameters set by the previous positioner will be discarded. - * - * The passed token will be sent in the corresponding - * xdg_popup.repositioned event. The new popup position will not - * take effect until the corresponding configure event is - * acknowledged by the client. See xdg_popup.repositioned for - * details. The token itself is opaque, and has no other special - * meaning. - * - * If multiple reposition requests are sent, the compositor may - * skip all but the last one. - * - * If the popup is repositioned in response to a configure event - * for its parent, the client should send an - * xdg_positioner.set_parent_configure and possibly an - * xdg_positioner.set_parent_size request to allow the compositor - * to properly constrain the popup. - * - * If the popup is repositioned together with a parent that is - * being resized, but not in response to a configure event, the - * client should send an xdg_positioner.set_parent_size request. - * @param token reposition request token - * @since 3 - */ void (*reposition)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *positioner, @@ -2186,64 +693,26 @@ struct xdg_popup_interface { #define XDG_POPUP_POPUP_DONE 1 #define XDG_POPUP_REPOSITIONED 2 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_POPUP_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_REPOSITIONED_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_GRAB_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ #define XDG_POPUP_REPOSITION_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_popup - * Sends an configure event to the client owning the resource. - * @param resource_ The client's resource - * @param x x position relative to parent surface window geometry - * @param y y position relative to parent surface window geometry - * @param width window geometry width - * @param height window geometry height - */ static inline void xdg_popup_send_configure(struct wl_resource *resource_, int32_t x, int32_t y, int32_t width, int32_t height) { wl_resource_post_event(resource_, XDG_POPUP_CONFIGURE, x, y, width, height); } -/** - * @ingroup iface_xdg_popup - * Sends an popup_done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void xdg_popup_send_popup_done(struct wl_resource *resource_) { wl_resource_post_event(resource_, XDG_POPUP_POPUP_DONE); } -/** - * @ingroup iface_xdg_popup - * Sends an repositioned event to the client owning the resource. - * @param resource_ The client's resource - * @param token reposition request token - */ static inline void xdg_popup_send_repositioned(struct wl_resource *resource_, uint32_t token) { diff --git a/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-protocol.c b/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-protocol.c index 73d39d207..264b8f29e 100644 --- a/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-protocol.c +++ b/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-protocol.c @@ -1,5 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ - /* * Copyright © 2023-2024 Matthias Klumpp * Copyright © 2024 David Edmundson @@ -30,7 +28,7 @@ #include "wayland-util.h" #ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +# define __has_attribute(x) 0 #endif #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) diff --git a/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-server-protocol.h b/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-server-protocol.h index b186d8897..33e15fd5a 100644 --- a/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/generated/xdg-toplevel-icon-v1-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.24.0 */ #ifndef XDG_TOPLEVEL_ICON_V1_SERVER_PROTOCOL_H #define XDG_TOPLEVEL_ICON_V1_SERVER_PROTOCOL_H @@ -14,33 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_xdg_toplevel_icon_v1 The xdg_toplevel_icon_v1 protocol - * protocol to assign icons to toplevels - * - * @section page_desc_xdg_toplevel_icon_v1 Description - * - * This protocol allows clients to set icons for their toplevel surfaces - * either via the XDG icon stock (using an icon name), or from pixel data. - * - * A toplevel icon represents the individual toplevel (unlike the application - * or launcher icon, which represents the application as a whole), and may be - * shown in window switchers, window overviews and taskbars that list - * individual windows. - * - * This document adheres to RFC 2119 when using words like "must", - * "should", "may", etc. - * - * Warning! The protocol described in this file is currently in the testing - * phase. Backward compatible changes may be added together with the - * corresponding interface version bump. Backward incompatible changes can - * only be done by creating a new major version of the extension. - * - * @section page_ifaces_xdg_toplevel_icon_v1 Interfaces - * - @subpage page_iface_xdg_toplevel_icon_manager_v1 - interface to manage toplevel icons - * - @subpage page_iface_xdg_toplevel_icon_v1 - a toplevel window icon +/* * @section page_copyright_xdg_toplevel_icon_v1 Copyright - *
  *
  * Copyright © 2023-2024 Matthias Klumpp
  * Copyright ©      2024 David Edmundson
@@ -63,8 +37,33 @@ struct wl_resource;
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  * DEALINGS IN THE SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_buffer; struct xdg_toplevel; struct xdg_toplevel_icon_manager_v1; @@ -72,104 +71,19 @@ struct xdg_toplevel_icon_v1; #ifndef XDG_TOPLEVEL_ICON_MANAGER_V1_INTERFACE #define XDG_TOPLEVEL_ICON_MANAGER_V1_INTERFACE -/** - * @page page_iface_xdg_toplevel_icon_manager_v1 xdg_toplevel_icon_manager_v1 - * @section page_iface_xdg_toplevel_icon_manager_v1_desc Description - * - * This interface allows clients to create toplevel window icons and set - * them on toplevel windows to be displayed to the user. - * @section page_iface_xdg_toplevel_icon_manager_v1_api API - * See @ref iface_xdg_toplevel_icon_manager_v1. - */ -/** - * @defgroup iface_xdg_toplevel_icon_manager_v1 The xdg_toplevel_icon_manager_v1 interface - * - * This interface allows clients to create toplevel window icons and set - * them on toplevel windows to be displayed to the user. - */ extern const struct wl_interface xdg_toplevel_icon_manager_v1_interface; #endif #ifndef XDG_TOPLEVEL_ICON_V1_INTERFACE #define XDG_TOPLEVEL_ICON_V1_INTERFACE -/** - * @page page_iface_xdg_toplevel_icon_v1 xdg_toplevel_icon_v1 - * @section page_iface_xdg_toplevel_icon_v1_desc Description - * - * This interface defines a toplevel icon. - * An icon can have a name, and multiple buffers. - * In order to be applied, the icon must have either a name, or at least - * one buffer assigned. Applying an empty icon (with no buffer or name) to - * a toplevel should reset its icon to the default icon. - * - * It is up to compositor policy whether to prefer using a buffer or loading - * an icon via its name. See 'set_name' and 'add_buffer' for details. - * @section page_iface_xdg_toplevel_icon_v1_api API - * See @ref iface_xdg_toplevel_icon_v1. - */ -/** - * @defgroup iface_xdg_toplevel_icon_v1 The xdg_toplevel_icon_v1 interface - * - * This interface defines a toplevel icon. - * An icon can have a name, and multiple buffers. - * In order to be applied, the icon must have either a name, or at least - * one buffer assigned. Applying an empty icon (with no buffer or name) to - * a toplevel should reset its icon to the default icon. - * - * It is up to compositor policy whether to prefer using a buffer or loading - * an icon via its name. See 'set_name' and 'add_buffer' for details. - */ extern const struct wl_interface xdg_toplevel_icon_v1_interface; #endif -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - * @struct xdg_toplevel_icon_manager_v1_interface - */ struct xdg_toplevel_icon_manager_v1_interface { - /** - * destroy the toplevel icon manager - * - * Destroy the toplevel icon manager. This does not destroy - * objects created with the manager. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * create a new icon instance - * - * Creates a new icon object. This icon can then be attached to a - * xdg_toplevel via the 'set_icon' request. - */ void (*create_icon)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * set an icon on a toplevel window - * - * This request assigns the icon 'icon' to 'toplevel', or clears - * the toplevel icon if 'icon' was null. This state is - * double-buffered and is applied on the next wl_surface.commit of - * the toplevel. - * - * After making this call, the xdg_toplevel_icon_v1 provided as - * 'icon' can be destroyed by the client without 'toplevel' losing - * its icon. The xdg_toplevel_icon_v1 is immutable from this point, - * and any future attempts to change it must raise the - * 'xdg_toplevel_icon_v1.immutable' protocol error. - * - * The compositor must set the toplevel icon from either the pixel - * data the icon provides, or by loading a stock icon using the - * icon name. See the description of 'xdg_toplevel_icon_v1' for - * details. - * - * If 'icon' is set to null, the icon of the respective toplevel is - * reset to its default icon (usually the icon of the application, - * derived from its desktop-entry file, or a placeholder icon). If - * this request is passed an icon with no pixel buffers or icon - * name assigned, the icon must be reset just like if 'icon' was - * null. - * @param toplevel the toplevel to act on - */ void (*set_icon)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *toplevel, @@ -179,45 +93,19 @@ struct xdg_toplevel_icon_manager_v1_interface { #define XDG_TOPLEVEL_ICON_MANAGER_V1_ICON_SIZE 0 #define XDG_TOPLEVEL_ICON_MANAGER_V1_DONE 1 -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - */ #define XDG_TOPLEVEL_ICON_MANAGER_V1_ICON_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - */ #define XDG_TOPLEVEL_ICON_MANAGER_V1_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - */ #define XDG_TOPLEVEL_ICON_MANAGER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - */ #define XDG_TOPLEVEL_ICON_MANAGER_V1_CREATE_ICON_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - */ #define XDG_TOPLEVEL_ICON_MANAGER_V1_SET_ICON_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - * Sends an icon_size event to the client owning the resource. - * @param resource_ The client's resource - * @param size the edge size of the square icon in surface-local coordinates, e.g. 64 - */ static inline void xdg_toplevel_icon_manager_v1_send_icon_size(struct wl_resource *resource_, int32_t size) { wl_resource_post_event(resource_, XDG_TOPLEVEL_ICON_MANAGER_V1_ICON_SIZE, size); } -/** - * @ingroup iface_xdg_toplevel_icon_manager_v1 - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void xdg_toplevel_icon_manager_v1_send_done(struct wl_resource *resource_) { @@ -227,30 +115,14 @@ xdg_toplevel_icon_manager_v1_send_done(struct wl_resource *resource_) #ifndef XDG_TOPLEVEL_ICON_V1_ERROR_ENUM #define XDG_TOPLEVEL_ICON_V1_ERROR_ENUM enum xdg_toplevel_icon_v1_error { - /** - * the provided buffer does not satisfy requirements - */ XDG_TOPLEVEL_ICON_V1_ERROR_INVALID_BUFFER = 1, - /** - * the icon has already been assigned to a toplevel and must not be changed - */ XDG_TOPLEVEL_ICON_V1_ERROR_IMMUTABLE = 2, - /** - * the provided buffer has been destroyed before the toplevel icon - */ XDG_TOPLEVEL_ICON_V1_ERROR_NO_BUFFER = 3, }; -#endif /* XDG_TOPLEVEL_ICON_V1_ERROR_ENUM */ +#endif #ifndef XDG_TOPLEVEL_ICON_V1_ERROR_ENUM_IS_VALID #define XDG_TOPLEVEL_ICON_V1_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_xdg_toplevel_icon_v1 - * Validate a xdg_toplevel_icon_v1 error value. - * - * @return true on success, false on error. - * @ref xdg_toplevel_icon_v1_error - */ static inline bool xdg_toplevel_icon_v1_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -264,77 +136,14 @@ xdg_toplevel_icon_v1_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* XDG_TOPLEVEL_ICON_V1_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_xdg_toplevel_icon_v1 - * @struct xdg_toplevel_icon_v1_interface - */ struct xdg_toplevel_icon_v1_interface { - /** - * destroy the icon object - * - * Destroys the 'xdg_toplevel_icon_v1' object. The icon must - * still remain set on every toplevel it was assigned to, until the - * toplevel icon is reset explicitly. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set an icon name - * - * This request assigns an icon name to this icon. Any previously - * set name is overridden. - * - * The compositor must resolve 'icon_name' according to the lookup - * rules described in the XDG icon theme specification[1] using the - * environment's current icon theme. - * - * If the compositor does not support icon names or cannot resolve - * 'icon_name' according to the XDG icon theme specification it - * must fall back to using pixel buffer data instead. - * - * If this request is made after the icon has been assigned to a - * toplevel via 'set_icon', an 'immutable' error must be raised. - * - * [1]: - * https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html - */ void (*set_name)(struct wl_client *client, struct wl_resource *resource, const char *icon_name); - /** - * add icon data from a pixel buffer - * - * This request adds pixel data supplied as wl_buffer to the - * icon. - * - * The client should add pixel data for all icon sizes and scales - * that it can provide, or which are explicitly requested by the - * compositor via 'icon_size' events on - * xdg_toplevel_icon_manager_v1. - * - * The wl_buffer supplying pixel data as 'buffer' must be backed by - * wl_shm and must be a square (width and height being equal). If - * any of these buffer requirements are not fulfilled, a - * 'invalid_buffer' error must be raised. - * - * If this icon instance already has a buffer of the same size and - * scale from a previous 'add_buffer' request, data from the last - * request overrides the preexisting pixel data. - * - * The wl_buffer must be kept alive for as long as the - * xdg_toplevel_icon it is associated with is not destroyed, - * otherwise a 'no_buffer' error is raised. The buffer contents - * must not be modified after it was assigned to the icon. As a - * result, the region of the wl_shm_pool's backing storage used for - * the wl_buffer must not be modified after this request is sent. - * The wl_buffer.release event is unused. - * - * If this request is made after the icon has been assigned to a - * toplevel via 'set_icon', an 'immutable' error must be raised. - * @param scale the scaling factor of the icon, e.g. 1 - */ void (*add_buffer)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *buffer, @@ -342,17 +151,8 @@ struct xdg_toplevel_icon_v1_interface { }; -/** - * @ingroup iface_xdg_toplevel_icon_v1 - */ #define XDG_TOPLEVEL_ICON_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_v1 - */ #define XDG_TOPLEVEL_ICON_V1_SET_NAME_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel_icon_v1 - */ #define XDG_TOPLEVEL_ICON_V1_ADD_BUFFER_SINCE_VERSION 1 #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-core.h b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-core.h index 757f6a1f1..7c53fde66 100644 --- a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-core.h +++ b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-core.h @@ -43,87 +43,15 @@ enum { WL_EVENT_ERROR = 0x08 }; -/** File descriptor dispatch function type - * - * Functions of this type are used as callbacks for file descriptor events. - * - * \param fd The file descriptor delivering the event. - * \param mask Describes the kind of the event as a bitwise-or of: - * \c WL_EVENT_READABLE, \c WL_EVENT_WRITABLE, \c WL_EVENT_HANGUP, - * \c WL_EVENT_ERROR. - * \param data The user data argument of the related wl_event_loop_add_fd() - * call. - * \return If the event source is registered for re-check with - * wl_event_source_check(): 0 for all done, 1 for needing a re-check. - * If not registered, the return value is ignored and should be zero. - * - * \sa wl_event_loop_add_fd() - * \memberof wl_event_source - */ typedef int (*wl_event_loop_fd_func_t)(int fd, uint32_t mask, void *data); -/** Timer dispatch function type - * - * Functions of this type are used as callbacks for timer expiry. - * - * \param data The user data argument of the related wl_event_loop_add_timer() - * call. - * \return If the event source is registered for re-check with - * wl_event_source_check(): 0 for all done, 1 for needing a re-check. - * If not registered, the return value is ignored and should be zero. - * - * \sa wl_event_loop_add_timer() - * \memberof wl_event_source - */ typedef int (*wl_event_loop_timer_func_t)(void *data); -/** Signal dispatch function type - * - * Functions of this type are used as callbacks for (POSIX) signals. - * - * \param signal_number - * \param data The user data argument of the related wl_event_loop_add_signal() - * call. - * \return If the event source is registered for re-check with - * wl_event_source_check(): 0 for all done, 1 for needing a re-check. - * If not registered, the return value is ignored and should be zero. - * - * \sa wl_event_loop_add_signal() - * \memberof wl_event_source - */ typedef int (*wl_event_loop_signal_func_t)(int signal_number, void *data); -/** Idle task function type - * - * Functions of this type are used as callbacks before blocking in - * wl_event_loop_dispatch(). - * - * \param data The user data argument of the related wl_event_loop_add_idle() - * call. - * - * \sa wl_event_loop_add_idle() wl_event_loop_dispatch() - * \memberof wl_event_source - */ typedef void (*wl_event_loop_idle_func_t)(void *data); -/** \struct wl_event_loop - * - * \brief An event loop context - * - * Usually you create an event loop context, add sources to it, and call - * wl_event_loop_dispatch() in a loop to process events. - * - * \sa wl_event_source - */ -/** \struct wl_event_source - * - * \brief An abstract event source - * - * This is the generic type for fd, timer, signal, and idle sources. - * Functions that operate on specific source types must not be used with - * a different type, even if the function signature allows it. - */ struct wl_event_loop * wl_event_loop_create(void); @@ -266,21 +194,6 @@ wl_global_set_withdrawn_listener(struct wl_global *global, void wl_global_destroy(struct wl_global *global); -/** A filter function for wl_global objects - * - * \param client The client object - * \param global The global object to show or hide - * \param data The user data pointer - * - * A filter function enables the server to decide which globals to - * advertise to each client. - * - * When a wl_global filter is set, the given callback function will be - * called during wl_global advertisement and binding. - * - * This function should return true if the global object should be made - * visible to the client or false otherwise. - */ typedef bool (*wl_display_global_filter_func_t)(const struct wl_client *client, const struct wl_global *global, void *data); @@ -321,7 +234,6 @@ wl_client_get_link(struct wl_client *client); struct wl_client * wl_client_from_link(struct wl_list *link); -/** Iterate over a list of clients. */ #define wl_client_for_each(client, list) \ for (client = wl_client_from_link((list)->next); \ wl_client_get_link(client) != (list); \ @@ -370,7 +282,6 @@ void wl_client_add_resource_created_listener(struct wl_client *client, struct wl_listener *listener); -/** Callback function type for wl_client_for_each_resource() */ typedef enum wl_iterator_result (*wl_client_for_each_resource_iterator_func_t)( struct wl_resource *resource, void *user_data); @@ -393,112 +304,28 @@ wl_client_get_user_data(struct wl_client *client); void wl_client_set_max_buffer_size(struct wl_client *client, size_t max_buffer_size); -/** \class wl_listener - * - * \brief A single listener for Wayland signals - * - * wl_listener provides the means to listen for wl_signal notifications. Many - * Wayland objects use wl_listener for notification of significant events like - * object destruction. - * - * Clients should create wl_listener objects manually and can register them as - * listeners to signals using wl_signal_add(), assuming the signal is - * directly accessible. For opaque structs like wl_event_loop, adding a - * listener should be done through provided accessor methods. A listener can - * only listen to one signal at a time. - * - * \code - * struct wl_listener your_listener; - * - * your_listener.notify = your_callback_method; - * - * // Direct access - * wl_signal_add(&some_object->destroy_signal, &your_listener); - * - * // Accessor access - * wl_event_loop *loop = ...; - * wl_event_loop_add_destroy_listener(loop, &your_listener); - * \endcode - * - * If the listener is part of a larger struct, #wl_container_of can be used - * to retrieve a pointer to it: - * - * \code - * void your_listener(struct wl_listener *listener, void *data) - * { - * struct your_data *data; - * - * your_data = wl_container_of(listener, data, your_member_name); - * } - * \endcode - * - * If you need to remove a listener from a signal, use wl_list_remove(). - * - * \code - * wl_list_remove(&your_listener.link); - * \endcode - * - * \sa wl_signal - */ struct wl_listener { - /** Part of wl_signal::listener_list */ struct wl_list link; - /** Callback function pointer */ wl_notify_func_t notify; }; -/** \class wl_signal - * - * \brief A source of a type of observable event - * - * Signals are recognized points where significant events can be observed. - * Compositors as well as the server can provide signals. Observers are - * wl_listener's that are added through #wl_signal_add. Signals are emitted - * using #wl_signal_emit, which will invoke all listeners until that - * listener is removed by wl_list_remove() (or whenever the signal is - * destroyed). - * - * \sa wl_listener for more information on using wl_signal - */ struct wl_signal { struct wl_list listener_list; }; -/** Initialize a new \ref wl_signal for use. - * - * \param signal The signal that will be initialized - * - * \memberof wl_signal - */ static inline void wl_signal_init(struct wl_signal *signal) { wl_list_init(&signal->listener_list); } -/** Add the specified listener to this signal. - * - * \param signal The signal that will emit events to the listener - * \param listener The listener to add - * - * \memberof wl_signal - */ static inline void wl_signal_add(struct wl_signal *signal, struct wl_listener *listener) { wl_list_insert(signal->listener_list.prev, &listener->link); } -/** Gets the listener struct for the specified callback. - * - * \param signal The signal that contains the specified listener - * \param notify The listener that is the target of this search - * \return the list item that corresponds to the specified listener, or NULL - * if none was found - * - * \memberof wl_signal - */ static inline struct wl_listener * wl_signal_get(struct wl_signal *signal, wl_notify_func_t notify) { @@ -511,13 +338,6 @@ wl_signal_get(struct wl_signal *signal, wl_notify_func_t notify) return NULL; } -/** Emits this signal, notifying all registered listeners. - * - * \param signal The signal object that will emit the signal - * \param data The data that will be emitted with the signal - * - * \memberof wl_signal - */ static inline void wl_signal_emit(struct wl_signal *signal, void *data) { @@ -532,22 +352,6 @@ wl_signal_emit_mutable(struct wl_signal *signal, void *data); typedef void (*wl_resource_destroy_func_t)(struct wl_resource *resource); -/* - * Post an event to the client's object referred to by 'resource'. - * 'opcode' is the event number generated from the protocol XML - * description (the event name). The variable arguments are the event - * parameters, in the order they appear in the protocol XML specification. - * - * The variable arguments' types are: - * - type=uint: uint32_t - * - type=int: int32_t - * - type=fixed: wl_fixed_t - * - type=string: (const char *) to a nil-terminated string - * - type=array: (struct wl_array *) - * - type=fd: int, that is an open file descriptor - * - type=new_id: (struct wl_object *) or (struct wl_resource *) - * - type=object: (struct wl_object *) or (struct wl_resource *) - */ void wl_resource_post_event(struct wl_resource *resource, uint32_t opcode, ...); @@ -723,7 +527,6 @@ struct wl_protocol_logger_message { const union wl_argument *arguments; }; -/** Callback function type for wl_display_add_protocol_logger() */ typedef void (*wl_protocol_logger_func_t)(void *user_data, enum wl_protocol_logger_type direction, const struct wl_protocol_logger_message *message); diff --git a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-protocol.h b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-protocol.h index 484faff1b..86ac93f08 100644 --- a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-protocol.h +++ b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.26.0 */ #ifndef WAYLAND_SERVER_PROTOCOL_H #define WAYLAND_SERVER_PROTOCOL_H @@ -14,34 +13,8 @@ extern "C" { struct wl_client; struct wl_resource; -/** - * @page page_wayland The wayland protocol - * @section page_ifaces_wayland Interfaces - * - @subpage page_iface_wl_display - core global object - * - @subpage page_iface_wl_registry - global registry object - * - @subpage page_iface_wl_callback - callback object - * - @subpage page_iface_wl_compositor - the compositor singleton - * - @subpage page_iface_wl_shm_pool - a shared memory pool - * - @subpage page_iface_wl_shm - shared memory support - * - @subpage page_iface_wl_buffer - content for a wl_surface - * - @subpage page_iface_wl_data_offer - offer to transfer data - * - @subpage page_iface_wl_data_source - offer to transfer data - * - @subpage page_iface_wl_data_device - data transfer device - * - @subpage page_iface_wl_data_device_manager - data transfer interface - * - @subpage page_iface_wl_shell - create desktop-style surfaces - * - @subpage page_iface_wl_shell_surface - desktop-style metadata interface - * - @subpage page_iface_wl_surface - an onscreen surface - * - @subpage page_iface_wl_seat - group of input devices - * - @subpage page_iface_wl_pointer - pointer input device - * - @subpage page_iface_wl_keyboard - keyboard input device - * - @subpage page_iface_wl_touch - touchscreen input device - * - @subpage page_iface_wl_output - compositor output region - * - @subpage page_iface_wl_region - region interface - * - @subpage page_iface_wl_subcompositor - sub-surface compositing - * - @subpage page_iface_wl_subsurface - sub-surface interface to a wl_surface - * - @subpage page_iface_wl_fixes - wayland protocol fixes +/* * @section page_copyright_wayland Copyright - *
  *
  * Copyright © 2008-2011 Kristian Høgsberg
  * Copyright © 2010-2011 Intel Corporation
@@ -67,8 +40,34 @@ struct wl_resource;
  * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  * SOFTWARE.
- * 
*/ + + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_buffer; struct wl_callback; struct wl_compositor; @@ -95,904 +94,109 @@ struct wl_touch; #ifndef WL_DISPLAY_INTERFACE #define WL_DISPLAY_INTERFACE -/** - * @page page_iface_wl_display wl_display - * @section page_iface_wl_display_desc Description - * - * The core global object. This is a special singleton object. It - * is used for internal Wayland protocol features. - * @section page_iface_wl_display_api API - * See @ref iface_wl_display. - */ -/** - * @defgroup iface_wl_display The wl_display interface - * - * The core global object. This is a special singleton object. It - * is used for internal Wayland protocol features. - */ extern const struct wl_interface wl_display_interface; #endif #ifndef WL_REGISTRY_INTERFACE #define WL_REGISTRY_INTERFACE -/** - * @page page_iface_wl_registry wl_registry - * @section page_iface_wl_registry_desc Description - * - * The singleton global registry object. The server has a number of - * global objects that are available to all clients. These objects - * typically represent an actual object in the server (for example, - * an input device) or they are singleton objects that provide - * extension functionality. - * - * When a client creates a registry object, the registry object - * will emit a global event for each global currently in the - * registry. Globals come and go as a result of device or - * monitor hotplugs, reconfiguration or other events, and the - * registry will send out global and global_remove events to - * keep the client up to date with the changes. To mark the end - * of the initial burst of events, the client can use the - * wl_display.sync request immediately after calling - * wl_display.get_registry. - * - * A client can bind to a global object by using the bind - * request. This creates a client-side handle that lets the object - * emit events to the client and lets the client invoke requests on - * the object. - * @section page_iface_wl_registry_api API - * See @ref iface_wl_registry. - */ -/** - * @defgroup iface_wl_registry The wl_registry interface - * - * The singleton global registry object. The server has a number of - * global objects that are available to all clients. These objects - * typically represent an actual object in the server (for example, - * an input device) or they are singleton objects that provide - * extension functionality. - * - * When a client creates a registry object, the registry object - * will emit a global event for each global currently in the - * registry. Globals come and go as a result of device or - * monitor hotplugs, reconfiguration or other events, and the - * registry will send out global and global_remove events to - * keep the client up to date with the changes. To mark the end - * of the initial burst of events, the client can use the - * wl_display.sync request immediately after calling - * wl_display.get_registry. - * - * A client can bind to a global object by using the bind - * request. This creates a client-side handle that lets the object - * emit events to the client and lets the client invoke requests on - * the object. - */ extern const struct wl_interface wl_registry_interface; #endif #ifndef WL_CALLBACK_INTERFACE #define WL_CALLBACK_INTERFACE -/** - * @page page_iface_wl_callback wl_callback - * @section page_iface_wl_callback_desc Description - * - * Clients can handle the 'done' event to get notified when - * the related request is done. - * - * Note, because wl_callback objects are created from multiple independent - * factory interfaces, the wl_callback interface is frozen at version 1. - * @section page_iface_wl_callback_api API - * See @ref iface_wl_callback. - */ -/** - * @defgroup iface_wl_callback The wl_callback interface - * - * Clients can handle the 'done' event to get notified when - * the related request is done. - * - * Note, because wl_callback objects are created from multiple independent - * factory interfaces, the wl_callback interface is frozen at version 1. - */ extern const struct wl_interface wl_callback_interface; #endif #ifndef WL_COMPOSITOR_INTERFACE #define WL_COMPOSITOR_INTERFACE -/** - * @page page_iface_wl_compositor wl_compositor - * @section page_iface_wl_compositor_desc Description - * - * A compositor. This object is a singleton global. The - * compositor is in charge of combining the contents of multiple - * surfaces into one displayable output. - * @section page_iface_wl_compositor_api API - * See @ref iface_wl_compositor. - */ -/** - * @defgroup iface_wl_compositor The wl_compositor interface - * - * A compositor. This object is a singleton global. The - * compositor is in charge of combining the contents of multiple - * surfaces into one displayable output. - */ extern const struct wl_interface wl_compositor_interface; #endif #ifndef WL_SHM_POOL_INTERFACE #define WL_SHM_POOL_INTERFACE -/** - * @page page_iface_wl_shm_pool wl_shm_pool - * @section page_iface_wl_shm_pool_desc Description - * - * The wl_shm_pool object encapsulates a piece of memory shared - * between the compositor and client. Through the wl_shm_pool - * object, the client can allocate shared memory wl_buffer objects. - * All objects created through the same pool share the same - * underlying mapped memory. Reusing the mapped memory avoids the - * setup/teardown overhead and is useful when interactively resizing - * a surface or for many small buffers. - * @section page_iface_wl_shm_pool_api API - * See @ref iface_wl_shm_pool. - */ -/** - * @defgroup iface_wl_shm_pool The wl_shm_pool interface - * - * The wl_shm_pool object encapsulates a piece of memory shared - * between the compositor and client. Through the wl_shm_pool - * object, the client can allocate shared memory wl_buffer objects. - * All objects created through the same pool share the same - * underlying mapped memory. Reusing the mapped memory avoids the - * setup/teardown overhead and is useful when interactively resizing - * a surface or for many small buffers. - */ extern const struct wl_interface wl_shm_pool_interface; #endif #ifndef WL_SHM_INTERFACE #define WL_SHM_INTERFACE -/** - * @page page_iface_wl_shm wl_shm - * @section page_iface_wl_shm_desc Description - * - * A singleton global object that provides support for shared - * memory. - * - * Clients can create wl_shm_pool objects using the create_pool - * request. - * - * On binding the wl_shm object one or more format events - * are emitted to inform clients about the valid pixel formats - * that can be used for buffers. - * @section page_iface_wl_shm_api API - * See @ref iface_wl_shm. - */ -/** - * @defgroup iface_wl_shm The wl_shm interface - * - * A singleton global object that provides support for shared - * memory. - * - * Clients can create wl_shm_pool objects using the create_pool - * request. - * - * On binding the wl_shm object one or more format events - * are emitted to inform clients about the valid pixel formats - * that can be used for buffers. - */ extern const struct wl_interface wl_shm_interface; #endif #ifndef WL_BUFFER_INTERFACE #define WL_BUFFER_INTERFACE -/** - * @page page_iface_wl_buffer wl_buffer - * @section page_iface_wl_buffer_desc Description - * - * A buffer provides the content for a wl_surface. Buffers are - * created through factory interfaces such as wl_shm, wp_linux_buffer_params - * (from the linux-dmabuf protocol extension) or similar. It has a width and - * a height and can be attached to a wl_surface, but the mechanism by which a - * client provides and updates the contents is defined by the buffer factory - * interface. - * - * Color channels are assumed to be electrical rather than optical (in other - * words, encoded with a transfer function) unless otherwise specified. If - * the buffer uses a format that has an alpha channel, the alpha channel is - * assumed to be premultiplied into the electrical color channel values - * (after transfer function encoding) unless otherwise specified. - * - * Note, because wl_buffer objects are created from multiple independent - * factory interfaces, the wl_buffer interface is frozen at version 1. - * @section page_iface_wl_buffer_api API - * See @ref iface_wl_buffer. - */ -/** - * @defgroup iface_wl_buffer The wl_buffer interface - * - * A buffer provides the content for a wl_surface. Buffers are - * created through factory interfaces such as wl_shm, wp_linux_buffer_params - * (from the linux-dmabuf protocol extension) or similar. It has a width and - * a height and can be attached to a wl_surface, but the mechanism by which a - * client provides and updates the contents is defined by the buffer factory - * interface. - * - * Color channels are assumed to be electrical rather than optical (in other - * words, encoded with a transfer function) unless otherwise specified. If - * the buffer uses a format that has an alpha channel, the alpha channel is - * assumed to be premultiplied into the electrical color channel values - * (after transfer function encoding) unless otherwise specified. - * - * Note, because wl_buffer objects are created from multiple independent - * factory interfaces, the wl_buffer interface is frozen at version 1. - */ extern const struct wl_interface wl_buffer_interface; #endif #ifndef WL_DATA_OFFER_INTERFACE #define WL_DATA_OFFER_INTERFACE -/** - * @page page_iface_wl_data_offer wl_data_offer - * @section page_iface_wl_data_offer_desc Description - * - * A wl_data_offer represents a piece of data offered for transfer - * by another client (the source client). It is used by the - * copy-and-paste and drag-and-drop mechanisms. The offer - * describes the different mime types that the data can be - * converted to and provides the mechanism for transferring the - * data directly from the source client. - * @section page_iface_wl_data_offer_api API - * See @ref iface_wl_data_offer. - */ -/** - * @defgroup iface_wl_data_offer The wl_data_offer interface - * - * A wl_data_offer represents a piece of data offered for transfer - * by another client (the source client). It is used by the - * copy-and-paste and drag-and-drop mechanisms. The offer - * describes the different mime types that the data can be - * converted to and provides the mechanism for transferring the - * data directly from the source client. - */ extern const struct wl_interface wl_data_offer_interface; #endif #ifndef WL_DATA_SOURCE_INTERFACE #define WL_DATA_SOURCE_INTERFACE -/** - * @page page_iface_wl_data_source wl_data_source - * @section page_iface_wl_data_source_desc Description - * - * The wl_data_source object is the source side of a wl_data_offer. - * It is created by the source client in a data transfer and - * provides a way to describe the offered data and a way to respond - * to requests to transfer the data. - * @section page_iface_wl_data_source_api API - * See @ref iface_wl_data_source. - */ -/** - * @defgroup iface_wl_data_source The wl_data_source interface - * - * The wl_data_source object is the source side of a wl_data_offer. - * It is created by the source client in a data transfer and - * provides a way to describe the offered data and a way to respond - * to requests to transfer the data. - */ extern const struct wl_interface wl_data_source_interface; #endif #ifndef WL_DATA_DEVICE_INTERFACE #define WL_DATA_DEVICE_INTERFACE -/** - * @page page_iface_wl_data_device wl_data_device - * @section page_iface_wl_data_device_desc Description - * - * There is one wl_data_device per seat which can be obtained - * from the global wl_data_device_manager singleton. - * - * A wl_data_device provides access to inter-client data transfer - * mechanisms such as copy-and-paste and drag-and-drop. - * @section page_iface_wl_data_device_api API - * See @ref iface_wl_data_device. - */ -/** - * @defgroup iface_wl_data_device The wl_data_device interface - * - * There is one wl_data_device per seat which can be obtained - * from the global wl_data_device_manager singleton. - * - * A wl_data_device provides access to inter-client data transfer - * mechanisms such as copy-and-paste and drag-and-drop. - */ extern const struct wl_interface wl_data_device_interface; #endif #ifndef WL_DATA_DEVICE_MANAGER_INTERFACE #define WL_DATA_DEVICE_MANAGER_INTERFACE -/** - * @page page_iface_wl_data_device_manager wl_data_device_manager - * @section page_iface_wl_data_device_manager_desc Description - * - * The wl_data_device_manager is a singleton global object that - * provides access to inter-client data transfer mechanisms such as - * copy-and-paste and drag-and-drop. These mechanisms are tied to - * a wl_seat and this interface lets a client get a wl_data_device - * corresponding to a wl_seat. - * - * Depending on the version bound, the objects created from the bound - * wl_data_device_manager object will have different requirements for - * functioning properly. See wl_data_source.set_actions, - * wl_data_offer.accept and wl_data_offer.finish for details. - * @section page_iface_wl_data_device_manager_api API - * See @ref iface_wl_data_device_manager. - */ -/** - * @defgroup iface_wl_data_device_manager The wl_data_device_manager interface - * - * The wl_data_device_manager is a singleton global object that - * provides access to inter-client data transfer mechanisms such as - * copy-and-paste and drag-and-drop. These mechanisms are tied to - * a wl_seat and this interface lets a client get a wl_data_device - * corresponding to a wl_seat. - * - * Depending on the version bound, the objects created from the bound - * wl_data_device_manager object will have different requirements for - * functioning properly. See wl_data_source.set_actions, - * wl_data_offer.accept and wl_data_offer.finish for details. - */ extern const struct wl_interface wl_data_device_manager_interface; #endif #ifndef WL_SHELL_INTERFACE #define WL_SHELL_INTERFACE -/** - * @page page_iface_wl_shell wl_shell - * @section page_iface_wl_shell_desc Description - * - * This interface is implemented by servers that provide - * desktop-style user interfaces. - * - * It allows clients to associate a wl_shell_surface with - * a basic surface. - * - * Note! This protocol is deprecated and not intended for production use. - * For desktop-style user interfaces, use xdg_shell. Compositors and clients - * should not implement this interface. - * @section page_iface_wl_shell_api API - * See @ref iface_wl_shell. - */ -/** - * @defgroup iface_wl_shell The wl_shell interface - * - * This interface is implemented by servers that provide - * desktop-style user interfaces. - * - * It allows clients to associate a wl_shell_surface with - * a basic surface. - * - * Note! This protocol is deprecated and not intended for production use. - * For desktop-style user interfaces, use xdg_shell. Compositors and clients - * should not implement this interface. - */ extern const struct wl_interface wl_shell_interface; #endif #ifndef WL_SHELL_SURFACE_INTERFACE #define WL_SHELL_SURFACE_INTERFACE -/** - * @page page_iface_wl_shell_surface wl_shell_surface - * @section page_iface_wl_shell_surface_desc Description - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides requests to treat surfaces like toplevel, fullscreen - * or popup windows, move, resize or maximize them, associate - * metadata like title and class, etc. - * - * On the server side the object is automatically destroyed when - * the related wl_surface is destroyed. On the client side, - * wl_shell_surface_destroy() must be called before destroying - * the wl_surface object. - * @section page_iface_wl_shell_surface_api API - * See @ref iface_wl_shell_surface. - */ -/** - * @defgroup iface_wl_shell_surface The wl_shell_surface interface - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides requests to treat surfaces like toplevel, fullscreen - * or popup windows, move, resize or maximize them, associate - * metadata like title and class, etc. - * - * On the server side the object is automatically destroyed when - * the related wl_surface is destroyed. On the client side, - * wl_shell_surface_destroy() must be called before destroying - * the wl_surface object. - */ extern const struct wl_interface wl_shell_surface_interface; #endif #ifndef WL_SURFACE_INTERFACE #define WL_SURFACE_INTERFACE -/** - * @page page_iface_wl_surface wl_surface - * @section page_iface_wl_surface_desc Description - * - * A surface is a rectangular area that may be displayed on zero - * or more outputs, and shown any number of times at the compositor's - * discretion. They can present wl_buffers, receive user input, and - * define a local coordinate system. - * - * The size of a surface (and relative positions on it) is described - * in surface-local coordinates, which may differ from the buffer - * coordinates of the pixel content, in case a buffer_transform - * or a buffer_scale is used. - * - * A surface without a "role" is fairly useless: a compositor does - * not know where, when or how to present it. The role is the - * purpose of a wl_surface. Examples of roles are a cursor for a - * pointer (as set by wl_pointer.set_cursor), a drag icon - * (wl_data_device.start_drag), a sub-surface - * (wl_subcompositor.get_subsurface), and a window as defined by a - * shell protocol (e.g. wl_shell.get_shell_surface). - * - * A surface can have only one role at a time. Initially a - * wl_surface does not have a role. Once a wl_surface is given a - * role, it is set permanently for the whole lifetime of the - * wl_surface object. Giving the current role again is allowed, - * unless explicitly forbidden by the relevant interface - * specification. - * - * Surface roles are given by requests in other interfaces such as - * wl_pointer.set_cursor. The request should explicitly mention - * that this request gives a role to a wl_surface. Often, this - * request also creates a new protocol object that represents the - * role and adds additional functionality to wl_surface. When a - * client wants to destroy a wl_surface, they must destroy this role - * object before the wl_surface, otherwise a defunct_role_object error is - * sent. - * - * Destroying the role object does not remove the role from the - * wl_surface, but it may stop the wl_surface from "playing the role". - * For instance, if a wl_subsurface object is destroyed, the wl_surface - * it was created for will be unmapped and forget its position and - * z-order. It is allowed to create a wl_subsurface for the same - * wl_surface again, but it is not allowed to use the wl_surface as - * a cursor (cursor is a different role than sub-surface, and role - * switching is not allowed). - * @section page_iface_wl_surface_api API - * See @ref iface_wl_surface. - */ -/** - * @defgroup iface_wl_surface The wl_surface interface - * - * A surface is a rectangular area that may be displayed on zero - * or more outputs, and shown any number of times at the compositor's - * discretion. They can present wl_buffers, receive user input, and - * define a local coordinate system. - * - * The size of a surface (and relative positions on it) is described - * in surface-local coordinates, which may differ from the buffer - * coordinates of the pixel content, in case a buffer_transform - * or a buffer_scale is used. - * - * A surface without a "role" is fairly useless: a compositor does - * not know where, when or how to present it. The role is the - * purpose of a wl_surface. Examples of roles are a cursor for a - * pointer (as set by wl_pointer.set_cursor), a drag icon - * (wl_data_device.start_drag), a sub-surface - * (wl_subcompositor.get_subsurface), and a window as defined by a - * shell protocol (e.g. wl_shell.get_shell_surface). - * - * A surface can have only one role at a time. Initially a - * wl_surface does not have a role. Once a wl_surface is given a - * role, it is set permanently for the whole lifetime of the - * wl_surface object. Giving the current role again is allowed, - * unless explicitly forbidden by the relevant interface - * specification. - * - * Surface roles are given by requests in other interfaces such as - * wl_pointer.set_cursor. The request should explicitly mention - * that this request gives a role to a wl_surface. Often, this - * request also creates a new protocol object that represents the - * role and adds additional functionality to wl_surface. When a - * client wants to destroy a wl_surface, they must destroy this role - * object before the wl_surface, otherwise a defunct_role_object error is - * sent. - * - * Destroying the role object does not remove the role from the - * wl_surface, but it may stop the wl_surface from "playing the role". - * For instance, if a wl_subsurface object is destroyed, the wl_surface - * it was created for will be unmapped and forget its position and - * z-order. It is allowed to create a wl_subsurface for the same - * wl_surface again, but it is not allowed to use the wl_surface as - * a cursor (cursor is a different role than sub-surface, and role - * switching is not allowed). - */ extern const struct wl_interface wl_surface_interface; #endif #ifndef WL_SEAT_INTERFACE #define WL_SEAT_INTERFACE -/** - * @page page_iface_wl_seat wl_seat - * @section page_iface_wl_seat_desc Description - * - * A seat is a group of keyboards, pointer and touch devices. This - * object is published as a global during start up, or when such a - * device is hot plugged. A seat typically has a pointer and - * maintains a keyboard focus and a pointer focus. - * @section page_iface_wl_seat_api API - * See @ref iface_wl_seat. - */ -/** - * @defgroup iface_wl_seat The wl_seat interface - * - * A seat is a group of keyboards, pointer and touch devices. This - * object is published as a global during start up, or when such a - * device is hot plugged. A seat typically has a pointer and - * maintains a keyboard focus and a pointer focus. - */ extern const struct wl_interface wl_seat_interface; #endif #ifndef WL_POINTER_INTERFACE #define WL_POINTER_INTERFACE -/** - * @page page_iface_wl_pointer wl_pointer - * @section page_iface_wl_pointer_desc Description - * - * The wl_pointer interface represents one or more input devices, - * such as mice, which control the pointer location and pointer_focus - * of a seat. - * - * The wl_pointer interface generates motion, enter and leave - * events for the surfaces that the pointer is located over, - * and button and axis events for button presses, button releases - * and scrolling. - * @section page_iface_wl_pointer_api API - * See @ref iface_wl_pointer. - */ -/** - * @defgroup iface_wl_pointer The wl_pointer interface - * - * The wl_pointer interface represents one or more input devices, - * such as mice, which control the pointer location and pointer_focus - * of a seat. - * - * The wl_pointer interface generates motion, enter and leave - * events for the surfaces that the pointer is located over, - * and button and axis events for button presses, button releases - * and scrolling. - */ extern const struct wl_interface wl_pointer_interface; #endif #ifndef WL_KEYBOARD_INTERFACE #define WL_KEYBOARD_INTERFACE -/** - * @page page_iface_wl_keyboard wl_keyboard - * @section page_iface_wl_keyboard_desc Description - * - * The wl_keyboard interface represents one or more keyboards - * associated with a seat. - * - * Each wl_keyboard has the following logical state: - * - * - an active surface (possibly null), - * - the keys currently logically down, - * - the active modifiers, - * - the active group. - * - * By default, the active surface is null, the keys currently logically down - * are empty, the active modifiers and the active group are 0. - * @section page_iface_wl_keyboard_api API - * See @ref iface_wl_keyboard. - */ -/** - * @defgroup iface_wl_keyboard The wl_keyboard interface - * - * The wl_keyboard interface represents one or more keyboards - * associated with a seat. - * - * Each wl_keyboard has the following logical state: - * - * - an active surface (possibly null), - * - the keys currently logically down, - * - the active modifiers, - * - the active group. - * - * By default, the active surface is null, the keys currently logically down - * are empty, the active modifiers and the active group are 0. - */ extern const struct wl_interface wl_keyboard_interface; #endif #ifndef WL_TOUCH_INTERFACE #define WL_TOUCH_INTERFACE -/** - * @page page_iface_wl_touch wl_touch - * @section page_iface_wl_touch_desc Description - * - * The wl_touch interface represents a touchscreen - * associated with a seat. - * - * Touch interactions can consist of one or more contacts. - * For each contact, a series of events is generated, starting - * with a down event, followed by zero or more motion events, - * and ending with an up event. Events relating to the same - * contact point can be identified by the ID of the sequence. - * @section page_iface_wl_touch_api API - * See @ref iface_wl_touch. - */ -/** - * @defgroup iface_wl_touch The wl_touch interface - * - * The wl_touch interface represents a touchscreen - * associated with a seat. - * - * Touch interactions can consist of one or more contacts. - * For each contact, a series of events is generated, starting - * with a down event, followed by zero or more motion events, - * and ending with an up event. Events relating to the same - * contact point can be identified by the ID of the sequence. - */ extern const struct wl_interface wl_touch_interface; #endif #ifndef WL_OUTPUT_INTERFACE #define WL_OUTPUT_INTERFACE -/** - * @page page_iface_wl_output wl_output - * @section page_iface_wl_output_desc Description - * - * An output describes part of the compositor geometry. The - * compositor works in the 'compositor coordinate system' and an - * output corresponds to a rectangular area in that space that is - * actually visible. This typically corresponds to a monitor that - * displays part of the compositor space. This object is published - * as global during start up, or when a monitor is hotplugged. - * @section page_iface_wl_output_api API - * See @ref iface_wl_output. - */ -/** - * @defgroup iface_wl_output The wl_output interface - * - * An output describes part of the compositor geometry. The - * compositor works in the 'compositor coordinate system' and an - * output corresponds to a rectangular area in that space that is - * actually visible. This typically corresponds to a monitor that - * displays part of the compositor space. This object is published - * as global during start up, or when a monitor is hotplugged. - */ extern const struct wl_interface wl_output_interface; #endif #ifndef WL_REGION_INTERFACE #define WL_REGION_INTERFACE -/** - * @page page_iface_wl_region wl_region - * @section page_iface_wl_region_desc Description - * - * A region object describes an area. - * - * Region objects are used to describe the opaque and input - * regions of a surface. - * @section page_iface_wl_region_api API - * See @ref iface_wl_region. - */ -/** - * @defgroup iface_wl_region The wl_region interface - * - * A region object describes an area. - * - * Region objects are used to describe the opaque and input - * regions of a surface. - */ extern const struct wl_interface wl_region_interface; #endif #ifndef WL_SUBCOMPOSITOR_INTERFACE #define WL_SUBCOMPOSITOR_INTERFACE -/** - * @page page_iface_wl_subcompositor wl_subcompositor - * @section page_iface_wl_subcompositor_desc Description - * - * The global interface exposing sub-surface compositing capabilities. - * A wl_surface, that has sub-surfaces associated, is called the - * parent surface. Sub-surfaces can be arbitrarily nested and create - * a tree of sub-surfaces. - * - * The root surface in a tree of sub-surfaces is the main - * surface. The main surface cannot be a sub-surface, because - * sub-surfaces must always have a parent. - * - * A main surface with its sub-surfaces forms a (compound) window. - * For window management purposes, this set of wl_surface objects is - * to be considered as a single window, and it should also behave as - * such. - * - * The aim of sub-surfaces is to offload some of the compositing work - * within a window from clients to the compositor. A prime example is - * a video player with decorations and video in separate wl_surface - * objects. This should allow the compositor to pass YUV video buffer - * processing to dedicated overlay hardware when possible. - * @section page_iface_wl_subcompositor_api API - * See @ref iface_wl_subcompositor. - */ -/** - * @defgroup iface_wl_subcompositor The wl_subcompositor interface - * - * The global interface exposing sub-surface compositing capabilities. - * A wl_surface, that has sub-surfaces associated, is called the - * parent surface. Sub-surfaces can be arbitrarily nested and create - * a tree of sub-surfaces. - * - * The root surface in a tree of sub-surfaces is the main - * surface. The main surface cannot be a sub-surface, because - * sub-surfaces must always have a parent. - * - * A main surface with its sub-surfaces forms a (compound) window. - * For window management purposes, this set of wl_surface objects is - * to be considered as a single window, and it should also behave as - * such. - * - * The aim of sub-surfaces is to offload some of the compositing work - * within a window from clients to the compositor. A prime example is - * a video player with decorations and video in separate wl_surface - * objects. This should allow the compositor to pass YUV video buffer - * processing to dedicated overlay hardware when possible. - */ extern const struct wl_interface wl_subcompositor_interface; #endif #ifndef WL_SUBSURFACE_INTERFACE #define WL_SUBSURFACE_INTERFACE -/** - * @page page_iface_wl_subsurface wl_subsurface - * @section page_iface_wl_subsurface_desc Description - * - * An additional interface to a wl_surface object, which has been - * made a sub-surface. A sub-surface has one parent surface. A - * sub-surface's size and position are not limited to that of the parent. - * Particularly, a sub-surface is not automatically clipped to its - * parent's area. - * - * A sub-surface becomes mapped, when a non-NULL wl_buffer is applied - * and the parent surface is mapped. The order of which one happens - * first is irrelevant. A sub-surface is hidden if the parent becomes - * hidden, or if a NULL wl_buffer is applied. These rules apply - * recursively through the tree of surfaces. - * - * A sub-surface can be in one of two modes. The possible modes are - * synchronized and desynchronized, see methods wl_subsurface.set_sync and - * wl_subsurface.set_desync. - * - * The main surface can be thought to be always in desynchronized mode, - * since it does not have a parent in the sub-surfaces sense. - * - * Even if a sub-surface is in desynchronized mode, it will behave as - * in synchronized mode, if its parent surface behaves as in - * synchronized mode. This rule is applied recursively throughout the - * tree of surfaces. This means, that one can set a sub-surface into - * synchronized mode, and then assume that all its child and grand-child - * sub-surfaces are synchronized, too, without explicitly setting them. - * - * If a surface behaves as in synchronized mode, it is effectively - * synchronized, otherwise it is effectively desynchronized. - * - * A sub-surface is initially in the synchronized mode. - * - * The wl_subsurface interface has requests which modify double-buffered - * state of the parent surface (wl_subsurface.set_position, .place_above and - * .place_below). - * - * Destroying a sub-surface takes effect immediately. If you need to - * synchronize the removal of a sub-surface to the parent surface update, - * unmap the sub-surface first by attaching a NULL wl_buffer, update parent, - * and then destroy the sub-surface. - * - * If the parent wl_surface object is destroyed, the sub-surface is - * unmapped. - * - * A sub-surface never has the keyboard focus of any seat. - * - * The wl_surface.offset request is ignored: clients must use set_position - * instead to move the sub-surface. - * @section page_iface_wl_subsurface_api API - * See @ref iface_wl_subsurface. - */ -/** - * @defgroup iface_wl_subsurface The wl_subsurface interface - * - * An additional interface to a wl_surface object, which has been - * made a sub-surface. A sub-surface has one parent surface. A - * sub-surface's size and position are not limited to that of the parent. - * Particularly, a sub-surface is not automatically clipped to its - * parent's area. - * - * A sub-surface becomes mapped, when a non-NULL wl_buffer is applied - * and the parent surface is mapped. The order of which one happens - * first is irrelevant. A sub-surface is hidden if the parent becomes - * hidden, or if a NULL wl_buffer is applied. These rules apply - * recursively through the tree of surfaces. - * - * A sub-surface can be in one of two modes. The possible modes are - * synchronized and desynchronized, see methods wl_subsurface.set_sync and - * wl_subsurface.set_desync. - * - * The main surface can be thought to be always in desynchronized mode, - * since it does not have a parent in the sub-surfaces sense. - * - * Even if a sub-surface is in desynchronized mode, it will behave as - * in synchronized mode, if its parent surface behaves as in - * synchronized mode. This rule is applied recursively throughout the - * tree of surfaces. This means, that one can set a sub-surface into - * synchronized mode, and then assume that all its child and grand-child - * sub-surfaces are synchronized, too, without explicitly setting them. - * - * If a surface behaves as in synchronized mode, it is effectively - * synchronized, otherwise it is effectively desynchronized. - * - * A sub-surface is initially in the synchronized mode. - * - * The wl_subsurface interface has requests which modify double-buffered - * state of the parent surface (wl_subsurface.set_position, .place_above and - * .place_below). - * - * Destroying a sub-surface takes effect immediately. If you need to - * synchronize the removal of a sub-surface to the parent surface update, - * unmap the sub-surface first by attaching a NULL wl_buffer, update parent, - * and then destroy the sub-surface. - * - * If the parent wl_surface object is destroyed, the sub-surface is - * unmapped. - * - * A sub-surface never has the keyboard focus of any seat. - * - * The wl_surface.offset request is ignored: clients must use set_position - * instead to move the sub-surface. - */ extern const struct wl_interface wl_subsurface_interface; #endif #ifndef WL_FIXES_INTERFACE #define WL_FIXES_INTERFACE -/** - * @page page_iface_wl_fixes wl_fixes - * @section page_iface_wl_fixes_desc Description - * - * This global fixes problems with other core-protocol interfaces that - * cannot be fixed in these interfaces themselves. - * @section page_iface_wl_fixes_api API - * See @ref iface_wl_fixes. - */ -/** - * @defgroup iface_wl_fixes The wl_fixes interface - * - * This global fixes problems with other core-protocol interfaces that - * cannot be fixed in these interfaces themselves. - */ extern const struct wl_interface wl_fixes_interface; #endif #ifndef WL_DISPLAY_ERROR_ENUM #define WL_DISPLAY_ERROR_ENUM -/** - * @ingroup iface_wl_display - * global error values - * - * These errors are global and can be emitted in response to any - * server request. - */ enum wl_display_error { - /** - * server couldn't find object - */ WL_DISPLAY_ERROR_INVALID_OBJECT = 0, - /** - * method doesn't exist on the specified interface or malformed request - */ WL_DISPLAY_ERROR_INVALID_METHOD = 1, - /** - * server is out of memory - */ WL_DISPLAY_ERROR_NO_MEMORY = 2, - /** - * implementation error in compositor - */ WL_DISPLAY_ERROR_IMPLEMENTATION = 3, }; -#endif /* WL_DISPLAY_ERROR_ENUM */ +#endif #ifndef WL_DISPLAY_ERROR_ENUM_IS_VALID #define WL_DISPLAY_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_display - * Validate a wl_display error value. - * - * @return true on success, false on error. - * @ref wl_display_error - */ static inline bool wl_display_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1008,47 +212,12 @@ wl_display_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_DISPLAY_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_display - * @struct wl_display_interface - */ struct wl_display_interface { - /** - * asynchronous roundtrip - * - * The sync request asks the server to emit the 'done' event on - * the returned wl_callback object. Since requests are handled - * in-order and events are delivered in-order, this can be used as - * a barrier to ensure all previous requests and the resulting - * events have been handled. - * - * The object returned by this request will be destroyed by the - * compositor after the callback is fired and as such the client - * must not attempt to use it after that point. - * - * The callback_data passed in the callback is undefined and should - * be ignored. - * @param callback callback object for the sync request - */ void (*sync)(struct wl_client *client, struct wl_resource *resource, uint32_t callback); - /** - * get global registry object - * - * This request creates a registry object that allows the client - * to list and bind the global objects available from the - * compositor. - * - * It should be noted that the server side resources consumed in - * response to a get_registry request can only be released when the - * client disconnects, not when the client side proxy is destroyed. - * Therefore, clients should invoke get_registry as infrequently as - * possible to avoid wasting memory. - * @param registry global registry object - */ void (*get_registry)(struct wl_client *client, struct wl_resource *resource, uint32_t registry); @@ -1057,39 +226,13 @@ struct wl_display_interface { #define WL_DISPLAY_ERROR 0 #define WL_DISPLAY_DELETE_ID 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_ERROR_SINCE_VERSION 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_DELETE_ID_SINCE_VERSION 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_SYNC_SINCE_VERSION 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_GET_REGISTRY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_registry - * @struct wl_registry_interface - */ struct wl_registry_interface { - /** - * bind an object to the display - * - * Binds a new, client-created object to the server using the - * specified name as the identifier. - * @param name unique numeric name of the object - * @param interface name of the objects interface - * @param version version of the objects interface - * @param id bound object - */ void (*bind)(struct wl_client *client, struct wl_resource *resource, uint32_t name, @@ -1099,40 +242,17 @@ struct wl_registry_interface { #define WL_REGISTRY_GLOBAL 0 #define WL_REGISTRY_GLOBAL_REMOVE 1 -/** - * @ingroup iface_wl_registry - */ #define WL_REGISTRY_GLOBAL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_registry - */ #define WL_REGISTRY_GLOBAL_REMOVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_registry - */ #define WL_REGISTRY_BIND_SINCE_VERSION 1 -/** - * @ingroup iface_wl_registry - * Sends an global event to the client owning the resource. - * @param resource_ The client's resource - * @param name numeric name of the global object - * @param interface interface implemented by the object - * @param version interface version - */ static inline void wl_registry_send_global(struct wl_resource *resource_, uint32_t name, const char *interface, uint32_t version) { wl_resource_post_event(resource_, WL_REGISTRY_GLOBAL, name, interface, version); } -/** - * @ingroup iface_wl_registry - * Sends an global_remove event to the client owning the resource. - * @param resource_ The client's resource - * @param name numeric name of the global object - */ static inline void wl_registry_send_global_remove(struct wl_resource *resource_, uint32_t name) { @@ -1141,101 +261,41 @@ wl_registry_send_global_remove(struct wl_resource *resource_, uint32_t name) #define WL_CALLBACK_DONE 0 -/** - * @ingroup iface_wl_callback - */ #define WL_CALLBACK_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_callback - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - * @param callback_data request-specific data for the callback - */ static inline void wl_callback_send_done(struct wl_resource *resource_, uint32_t callback_data) { wl_resource_post_event(resource_, WL_CALLBACK_DONE, callback_data); } -/** - * @ingroup iface_wl_compositor - * @struct wl_compositor_interface - */ struct wl_compositor_interface { - /** - * create new surface - * - * Ask the compositor to create a new surface. - * @param id the new surface - */ void (*create_surface)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * create new region - * - * Ask the compositor to create a new region. - * @param id the new region - */ void (*create_region)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * destroy wl_compositor - * - * This request destroys the wl_compositor. This has no effect on - * any other objects. - * @since 7 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; -/** - * @ingroup iface_wl_compositor - */ #define WL_COMPOSITOR_CREATE_SURFACE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_compositor - */ #define WL_COMPOSITOR_CREATE_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_compositor - */ #define WL_COMPOSITOR_RELEASE_SINCE_VERSION 7 #ifndef WL_SHM_POOL_ERROR_ENUM #define WL_SHM_POOL_ERROR_ENUM -/** - * @ingroup iface_wl_shm_pool - * wl_shm_pool error values - * - * These errors can be emitted in response to wl_shm_pool requests. - */ enum wl_shm_pool_error { - /** - * buffer format is not known - */ WL_SHM_POOL_ERROR_INVALID_FORMAT = 0, - /** - * invalid size or stride during buffer creation - */ WL_SHM_POOL_ERROR_INVALID_STRIDE = 1, }; -#endif /* WL_SHM_POOL_ERROR_ENUM */ +#endif #ifndef WL_SHM_POOL_ERROR_ENUM_IS_VALID #define WL_SHM_POOL_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_shm_pool - * Validate a wl_shm_pool error value. - * - * @return true on success, false on error. - * @ref wl_shm_pool_error - */ static inline bool wl_shm_pool_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1247,34 +307,9 @@ wl_shm_pool_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SHM_POOL_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_shm_pool - * @struct wl_shm_pool_interface - */ struct wl_shm_pool_interface { - /** - * create a buffer from the pool - * - * Create a wl_buffer object from the pool. - * - * The buffer is created offset bytes into the pool and has width - * and height as specified. The stride argument specifies the - * number of bytes from the beginning of one row to the beginning - * of the next. The format is the pixel format of the buffer and - * must be one of those advertised through the wl_shm.format event. - * - * A buffer will keep a reference to the pool it was created from - * so it is valid to destroy the pool immediately after creating a - * buffer from it. - * @param id buffer to create - * @param offset buffer byte offset within the pool - * @param width buffer width, in pixels - * @param height buffer height, in pixels - * @param stride number of bytes from the beginning of one row to the beginning of the next row - * @param format buffer pixel format - */ void (*create_buffer)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -1283,84 +318,29 @@ struct wl_shm_pool_interface { int32_t height, int32_t stride, uint32_t format); - /** - * destroy the pool - * - * Destroy the shared memory pool. - * - * The mmapped memory will be released when all buffers that have - * been created from this pool are gone. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * change the size of the pool mapping - * - * This request will cause the server to remap the backing memory - * for the pool from the file descriptor passed when the pool was - * created, but using the new size. This request can only be used - * to make the pool bigger. - * - * This request only changes the amount of bytes that are mmapped - * by the server and does not touch the file corresponding to the - * file descriptor passed at creation time. It is the client's - * responsibility to ensure that the file is at least as big as the - * new pool size. - * @param size new size of the pool, in bytes - */ void (*resize)(struct wl_client *client, struct wl_resource *resource, int32_t size); }; -/** - * @ingroup iface_wl_shm_pool - */ #define WL_SHM_POOL_CREATE_BUFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm_pool - */ #define WL_SHM_POOL_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm_pool - */ #define WL_SHM_POOL_RESIZE_SINCE_VERSION 1 #ifndef WL_SHM_ERROR_ENUM #define WL_SHM_ERROR_ENUM -/** - * @ingroup iface_wl_shm - * wl_shm error values - * - * These errors can be emitted in response to wl_shm requests. - */ enum wl_shm_error { - /** - * buffer format is not known - * @deprecated Deprecated since version 3 - */ WL_SHM_ERROR_INVALID_FORMAT = 0, - /** - * invalid size or stride during pool creation - */ WL_SHM_ERROR_INVALID_STRIDE = 1, - /** - * mmapping the file descriptor failed - */ WL_SHM_ERROR_INVALID_FD = 2, }; -#endif /* WL_SHM_ERROR_ENUM */ +#endif #ifndef WL_SHM_ERROR_ENUM_IS_VALID #define WL_SHM_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_shm - * Validate a wl_shm error value. - * - * @return true on success, false on error. - * @ref wl_shm_error - */ static inline bool wl_shm_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -1374,364 +354,94 @@ wl_shm_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SHM_ERROR_ENUM_IS_VALID */ +#endif #ifndef WL_SHM_FORMAT_ENUM #define WL_SHM_FORMAT_ENUM -/** - * @ingroup iface_wl_shm - * pixel formats - * - * This describes the memory layout of an individual pixel. - * - * All renderers should support argb8888 and xrgb8888 but any other - * formats are optional and may not be supported by the particular - * renderer in use. - * - * The drm format codes match the macros defined in drm_fourcc.h, except - * argb8888 and xrgb8888. The formats actually supported by the compositor - * will be reported by the format event. See drm_fourcc.h for more detailed - * format descriptions. - * - * For all wl_shm formats and unless specified in another protocol - * extension, pre-multiplied alpha is used for pixel values. - */ enum wl_shm_format { - /** - * 32-bit ARGB format, [31:0] A:R:G:B 8:8:8:8 little endian - */ WL_SHM_FORMAT_ARGB8888 = 0, - /** - * 32-bit RGB format, [31:0] x:R:G:B 8:8:8:8 little endian - */ WL_SHM_FORMAT_XRGB8888 = 1, - /** - * 8-bit color index format, [7:0] C - */ WL_SHM_FORMAT_C8 = 0x20203843, - /** - * 8-bit RGB format, [7:0] R:G:B 3:3:2 - */ WL_SHM_FORMAT_RGB332 = 0x38424752, - /** - * 8-bit BGR format, [7:0] B:G:R 2:3:3 - */ WL_SHM_FORMAT_BGR233 = 0x38524742, - /** - * 16-bit xRGB format, [15:0] x:R:G:B 4:4:4:4 little endian - */ WL_SHM_FORMAT_XRGB4444 = 0x32315258, - /** - * 16-bit xBGR format, [15:0] x:B:G:R 4:4:4:4 little endian - */ WL_SHM_FORMAT_XBGR4444 = 0x32314258, - /** - * 16-bit RGBx format, [15:0] R:G:B:x 4:4:4:4 little endian - */ WL_SHM_FORMAT_RGBX4444 = 0x32315852, - /** - * 16-bit BGRx format, [15:0] B:G:R:x 4:4:4:4 little endian - */ WL_SHM_FORMAT_BGRX4444 = 0x32315842, - /** - * 16-bit ARGB format, [15:0] A:R:G:B 4:4:4:4 little endian - */ WL_SHM_FORMAT_ARGB4444 = 0x32315241, - /** - * 16-bit ABGR format, [15:0] A:B:G:R 4:4:4:4 little endian - */ WL_SHM_FORMAT_ABGR4444 = 0x32314241, - /** - * 16-bit RGBA format, [15:0] R:G:B:A 4:4:4:4 little endian - */ WL_SHM_FORMAT_RGBA4444 = 0x32314152, - /** - * 16-bit BGRA format, [15:0] B:G:R:A 4:4:4:4 little endian - */ WL_SHM_FORMAT_BGRA4444 = 0x32314142, - /** - * 16-bit xRGB format, [15:0] x:R:G:B 1:5:5:5 little endian - */ WL_SHM_FORMAT_XRGB1555 = 0x35315258, - /** - * 16-bit xBGR 1555 format, [15:0] x:B:G:R 1:5:5:5 little endian - */ WL_SHM_FORMAT_XBGR1555 = 0x35314258, - /** - * 16-bit RGBx 5551 format, [15:0] R:G:B:x 5:5:5:1 little endian - */ WL_SHM_FORMAT_RGBX5551 = 0x35315852, - /** - * 16-bit BGRx 5551 format, [15:0] B:G:R:x 5:5:5:1 little endian - */ WL_SHM_FORMAT_BGRX5551 = 0x35315842, - /** - * 16-bit ARGB 1555 format, [15:0] A:R:G:B 1:5:5:5 little endian - */ WL_SHM_FORMAT_ARGB1555 = 0x35315241, - /** - * 16-bit ABGR 1555 format, [15:0] A:B:G:R 1:5:5:5 little endian - */ WL_SHM_FORMAT_ABGR1555 = 0x35314241, - /** - * 16-bit RGBA 5551 format, [15:0] R:G:B:A 5:5:5:1 little endian - */ WL_SHM_FORMAT_RGBA5551 = 0x35314152, - /** - * 16-bit BGRA 5551 format, [15:0] B:G:R:A 5:5:5:1 little endian - */ WL_SHM_FORMAT_BGRA5551 = 0x35314142, - /** - * 16-bit RGB 565 format, [15:0] R:G:B 5:6:5 little endian - */ WL_SHM_FORMAT_RGB565 = 0x36314752, - /** - * 16-bit BGR 565 format, [15:0] B:G:R 5:6:5 little endian - */ WL_SHM_FORMAT_BGR565 = 0x36314742, - /** - * 24-bit RGB format, [23:0] R:G:B little endian - */ WL_SHM_FORMAT_RGB888 = 0x34324752, - /** - * 24-bit BGR format, [23:0] B:G:R little endian - */ WL_SHM_FORMAT_BGR888 = 0x34324742, - /** - * 32-bit xBGR format, [31:0] x:B:G:R 8:8:8:8 little endian - */ WL_SHM_FORMAT_XBGR8888 = 0x34324258, - /** - * 32-bit RGBx format, [31:0] R:G:B:x 8:8:8:8 little endian - */ WL_SHM_FORMAT_RGBX8888 = 0x34325852, - /** - * 32-bit BGRx format, [31:0] B:G:R:x 8:8:8:8 little endian - */ WL_SHM_FORMAT_BGRX8888 = 0x34325842, - /** - * 32-bit ABGR format, [31:0] A:B:G:R 8:8:8:8 little endian - */ WL_SHM_FORMAT_ABGR8888 = 0x34324241, - /** - * 32-bit RGBA format, [31:0] R:G:B:A 8:8:8:8 little endian - */ WL_SHM_FORMAT_RGBA8888 = 0x34324152, - /** - * 32-bit BGRA format, [31:0] B:G:R:A 8:8:8:8 little endian - */ WL_SHM_FORMAT_BGRA8888 = 0x34324142, - /** - * 32-bit xRGB format, [31:0] x:R:G:B 2:10:10:10 little endian - */ WL_SHM_FORMAT_XRGB2101010 = 0x30335258, - /** - * 32-bit xBGR format, [31:0] x:B:G:R 2:10:10:10 little endian - */ WL_SHM_FORMAT_XBGR2101010 = 0x30334258, - /** - * 32-bit RGBx format, [31:0] R:G:B:x 10:10:10:2 little endian - */ WL_SHM_FORMAT_RGBX1010102 = 0x30335852, - /** - * 32-bit BGRx format, [31:0] B:G:R:x 10:10:10:2 little endian - */ WL_SHM_FORMAT_BGRX1010102 = 0x30335842, - /** - * 32-bit ARGB format, [31:0] A:R:G:B 2:10:10:10 little endian - */ WL_SHM_FORMAT_ARGB2101010 = 0x30335241, - /** - * 32-bit ABGR format, [31:0] A:B:G:R 2:10:10:10 little endian - */ WL_SHM_FORMAT_ABGR2101010 = 0x30334241, - /** - * 32-bit RGBA format, [31:0] R:G:B:A 10:10:10:2 little endian - */ WL_SHM_FORMAT_RGBA1010102 = 0x30334152, - /** - * 32-bit BGRA format, [31:0] B:G:R:A 10:10:10:2 little endian - */ WL_SHM_FORMAT_BGRA1010102 = 0x30334142, - /** - * packed YCbCr format, [31:0] Cr0:Y1:Cb0:Y0 8:8:8:8 little endian - */ WL_SHM_FORMAT_YUYV = 0x56595559, - /** - * packed YCbCr format, [31:0] Cb0:Y1:Cr0:Y0 8:8:8:8 little endian - */ WL_SHM_FORMAT_YVYU = 0x55595659, - /** - * packed YCbCr format, [31:0] Y1:Cr0:Y0:Cb0 8:8:8:8 little endian - */ WL_SHM_FORMAT_UYVY = 0x59565955, - /** - * packed YCbCr format, [31:0] Y1:Cb0:Y0:Cr0 8:8:8:8 little endian - */ WL_SHM_FORMAT_VYUY = 0x59555956, - /** - * packed AYCbCr format, [31:0] A:Y:Cb:Cr 8:8:8:8 little endian - */ WL_SHM_FORMAT_AYUV = 0x56555941, - /** - * 2 plane YCbCr Cr:Cb format, 2x2 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV12 = 0x3231564e, - /** - * 2 plane YCbCr Cb:Cr format, 2x2 subsampled Cb:Cr plane - */ WL_SHM_FORMAT_NV21 = 0x3132564e, - /** - * 2 plane YCbCr Cr:Cb format, 2x1 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV16 = 0x3631564e, - /** - * 2 plane YCbCr Cb:Cr format, 2x1 subsampled Cb:Cr plane - */ WL_SHM_FORMAT_NV61 = 0x3136564e, - /** - * 3 plane YCbCr format, 4x4 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV410 = 0x39565559, - /** - * 3 plane YCbCr format, 4x4 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU410 = 0x39555659, - /** - * 3 plane YCbCr format, 4x1 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV411 = 0x31315559, - /** - * 3 plane YCbCr format, 4x1 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU411 = 0x31315659, - /** - * 3 plane YCbCr format, 2x2 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV420 = 0x32315559, - /** - * 3 plane YCbCr format, 2x2 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU420 = 0x32315659, - /** - * 3 plane YCbCr format, 2x1 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV422 = 0x36315559, - /** - * 3 plane YCbCr format, 2x1 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU422 = 0x36315659, - /** - * 3 plane YCbCr format, non-subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV444 = 0x34325559, - /** - * 3 plane YCbCr format, non-subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU444 = 0x34325659, - /** - * [7:0] R - */ WL_SHM_FORMAT_R8 = 0x20203852, - /** - * [15:0] R little endian - */ WL_SHM_FORMAT_R16 = 0x20363152, - /** - * [15:0] R:G 8:8 little endian - */ WL_SHM_FORMAT_RG88 = 0x38384752, - /** - * [15:0] G:R 8:8 little endian - */ WL_SHM_FORMAT_GR88 = 0x38385247, - /** - * [31:0] R:G 16:16 little endian - */ WL_SHM_FORMAT_RG1616 = 0x32334752, - /** - * [31:0] G:R 16:16 little endian - */ WL_SHM_FORMAT_GR1616 = 0x32335247, - /** - * [63:0] x:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_XRGB16161616F = 0x48345258, - /** - * [63:0] x:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_XBGR16161616F = 0x48344258, - /** - * [63:0] A:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_ARGB16161616F = 0x48345241, - /** - * [63:0] A:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_ABGR16161616F = 0x48344241, - /** - * [31:0] X:Y:Cb:Cr 8:8:8:8 little endian - */ WL_SHM_FORMAT_XYUV8888 = 0x56555958, - /** - * [23:0] Cr:Cb:Y 8:8:8 little endian - */ WL_SHM_FORMAT_VUY888 = 0x34325556, - /** - * Y followed by U then V, 10:10:10. Non-linear modifier only - */ WL_SHM_FORMAT_VUY101010 = 0x30335556, - /** - * [63:0] Cr0:0:Y1:0:Cb0:0:Y0:0 10:6:10:6:10:6:10:6 little endian per 2 Y pixels - */ WL_SHM_FORMAT_Y210 = 0x30313259, - /** - * [63:0] Cr0:0:Y1:0:Cb0:0:Y0:0 12:4:12:4:12:4:12:4 little endian per 2 Y pixels - */ WL_SHM_FORMAT_Y212 = 0x32313259, - /** - * [63:0] Cr0:Y1:Cb0:Y0 16:16:16:16 little endian per 2 Y pixels - */ WL_SHM_FORMAT_Y216 = 0x36313259, - /** - * [31:0] A:Cr:Y:Cb 2:10:10:10 little endian - */ WL_SHM_FORMAT_Y410 = 0x30313459, - /** - * [63:0] A:0:Cr:0:Y:0:Cb:0 12:4:12:4:12:4:12:4 little endian - */ WL_SHM_FORMAT_Y412 = 0x32313459, - /** - * [63:0] A:Cr:Y:Cb 16:16:16:16 little endian - */ WL_SHM_FORMAT_Y416 = 0x36313459, - /** - * [31:0] X:Cr:Y:Cb 2:10:10:10 little endian - */ WL_SHM_FORMAT_XVYU2101010 = 0x30335658, - /** - * [63:0] X:0:Cr:0:Y:0:Cb:0 12:4:12:4:12:4:12:4 little endian - */ WL_SHM_FORMAT_XVYU12_16161616 = 0x36335658, - /** - * [63:0] X:Cr:Y:Cb 16:16:16:16 little endian - */ WL_SHM_FORMAT_XVYU16161616 = 0x38345658, - /** - * [63:0] A3:A2:Y3:0:Cr0:0:Y2:0:A1:A0:Y1:0:Cb0:0:Y0:0 1:1:8:2:8:2:8:2:1:1:8:2:8:2:8:2 little endian - */ WL_SHM_FORMAT_Y0L0 = 0x304c3059, - /** - * [63:0] X3:X2:Y3:0:Cr0:0:Y2:0:X1:X0:Y1:0:Cb0:0:Y0:0 1:1:8:2:8:2:8:2:1:1:8:2:8:2:8:2 little endian - */ WL_SHM_FORMAT_X0L0 = 0x304c3058, - /** - * [63:0] A3:A2:Y3:Cr0:Y2:A1:A0:Y1:Cb0:Y0 1:1:10:10:10:1:1:10:10:10 little endian - */ WL_SHM_FORMAT_Y0L2 = 0x324c3059, - /** - * [63:0] X3:X2:Y3:Cr0:Y2:X1:X0:Y1:Cb0:Y0 1:1:10:10:10:1:1:10:10:10 little endian - */ WL_SHM_FORMAT_X0L2 = 0x324c3058, WL_SHM_FORMAT_YUV420_8BIT = 0x38305559, WL_SHM_FORMAT_YUV420_10BIT = 0x30315559, @@ -1743,225 +453,65 @@ enum wl_shm_format { WL_SHM_FORMAT_BGR888_A8 = 0x38413842, WL_SHM_FORMAT_RGB565_A8 = 0x38413552, WL_SHM_FORMAT_BGR565_A8 = 0x38413542, - /** - * non-subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV24 = 0x3432564e, - /** - * non-subsampled Cb:Cr plane - */ WL_SHM_FORMAT_NV42 = 0x3234564e, - /** - * 2x1 subsampled Cr:Cb plane, 10 bit per channel - */ WL_SHM_FORMAT_P210 = 0x30313250, - /** - * 2x2 subsampled Cr:Cb plane 10 bits per channel - */ WL_SHM_FORMAT_P010 = 0x30313050, - /** - * 2x2 subsampled Cr:Cb plane 12 bits per channel - */ WL_SHM_FORMAT_P012 = 0x32313050, - /** - * 2x2 subsampled Cr:Cb plane 16 bits per channel - */ WL_SHM_FORMAT_P016 = 0x36313050, - /** - * [63:0] A:x:B:x:G:x:R:x 10:6:10:6:10:6:10:6 little endian - */ WL_SHM_FORMAT_AXBXGXRX106106106106 = 0x30314241, - /** - * 2x2 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV15 = 0x3531564e, WL_SHM_FORMAT_Q410 = 0x30313451, WL_SHM_FORMAT_Q401 = 0x31303451, - /** - * [63:0] x:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_XRGB16161616 = 0x38345258, - /** - * [63:0] x:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_XBGR16161616 = 0x38344258, - /** - * [63:0] A:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_ARGB16161616 = 0x38345241, - /** - * [63:0] A:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_ABGR16161616 = 0x38344241, - /** - * [7:0] C0:C1:C2:C3:C4:C5:C6:C7 1:1:1:1:1:1:1:1 eight pixels/byte - */ WL_SHM_FORMAT_C1 = 0x20203143, - /** - * [7:0] C0:C1:C2:C3 2:2:2:2 four pixels/byte - */ WL_SHM_FORMAT_C2 = 0x20203243, - /** - * [7:0] C0:C1 4:4 two pixels/byte - */ WL_SHM_FORMAT_C4 = 0x20203443, - /** - * [7:0] D0:D1:D2:D3:D4:D5:D6:D7 1:1:1:1:1:1:1:1 eight pixels/byte - */ WL_SHM_FORMAT_D1 = 0x20203144, - /** - * [7:0] D0:D1:D2:D3 2:2:2:2 four pixels/byte - */ WL_SHM_FORMAT_D2 = 0x20203244, - /** - * [7:0] D0:D1 4:4 two pixels/byte - */ WL_SHM_FORMAT_D4 = 0x20203444, - /** - * [7:0] D - */ WL_SHM_FORMAT_D8 = 0x20203844, - /** - * [7:0] R0:R1:R2:R3:R4:R5:R6:R7 1:1:1:1:1:1:1:1 eight pixels/byte - */ WL_SHM_FORMAT_R1 = 0x20203152, - /** - * [7:0] R0:R1:R2:R3 2:2:2:2 four pixels/byte - */ WL_SHM_FORMAT_R2 = 0x20203252, - /** - * [7:0] R0:R1 4:4 two pixels/byte - */ WL_SHM_FORMAT_R4 = 0x20203452, - /** - * [15:0] x:R 6:10 little endian - */ WL_SHM_FORMAT_R10 = 0x20303152, - /** - * [15:0] x:R 4:12 little endian - */ WL_SHM_FORMAT_R12 = 0x20323152, - /** - * [31:0] A:Cr:Cb:Y 8:8:8:8 little endian - */ WL_SHM_FORMAT_AVUY8888 = 0x59555641, - /** - * [31:0] X:Cr:Cb:Y 8:8:8:8 little endian - */ WL_SHM_FORMAT_XVUY8888 = 0x59555658, - /** - * 2x2 subsampled Cr:Cb plane 10 bits per channel packed - */ WL_SHM_FORMAT_P030 = 0x30333050, - /** - * [47:0] R:G:B 16:16:16 little endian - */ WL_SHM_FORMAT_RGB161616 = 0x38344752, - /** - * [47:0] B:G:R 16:16:16 little endian - */ WL_SHM_FORMAT_BGR161616 = 0x38344742, - /** - * [15:0] R 16 little endian - */ WL_SHM_FORMAT_R16F = 0x48202052, - /** - * [31:0] G:R 16:16 little endian - */ WL_SHM_FORMAT_GR1616F = 0x48205247, - /** - * [47:0] B:G:R 16:16:16 little endian - */ WL_SHM_FORMAT_BGR161616F = 0x48524742, - /** - * [31:0] R 32 little endian - */ WL_SHM_FORMAT_R32F = 0x46202052, - /** - * [63:0] G:R 32:32 little endian - */ WL_SHM_FORMAT_GR3232F = 0x46205247, - /** - * [95:0] B:G:R 32:32:32 little endian - */ WL_SHM_FORMAT_BGR323232F = 0x46524742, - /** - * [127:0] A:B:G:R 32:32:32:32 little endian - */ WL_SHM_FORMAT_ABGR32323232F = 0x46384241, - /** - * 2x1 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV20 = 0x3032564e, - /** - * non-subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV30 = 0x3033564e, - /** - * 2x2 subsampled Cb (1) and Cr (2) planes 10 bits per channel - */ WL_SHM_FORMAT_S010 = 0x30313053, - /** - * 2x1 subsampled Cb (1) and Cr (2) planes 10 bits per channel - */ WL_SHM_FORMAT_S210 = 0x30313253, - /** - * non-subsampled Cb (1) and Cr (2) planes 10 bits per channel - */ WL_SHM_FORMAT_S410 = 0x30313453, - /** - * 2x2 subsampled Cb (1) and Cr (2) planes 12 bits per channel - */ WL_SHM_FORMAT_S012 = 0x32313053, - /** - * 2x1 subsampled Cb (1) and Cr (2) planes 12 bits per channel - */ WL_SHM_FORMAT_S212 = 0x32313253, - /** - * non-subsampled Cb (1) and Cr (2) planes 12 bits per channel - */ WL_SHM_FORMAT_S412 = 0x32313453, - /** - * 2x2 subsampled Cb (1) and Cr (2) planes 16 bits per channel - */ WL_SHM_FORMAT_S016 = 0x36313053, - /** - * 2x1 subsampled Cb (1) and Cr (2) planes 16 bits per channel - */ WL_SHM_FORMAT_S216 = 0x36313253, - /** - * non-subsampled Cb (1) and Cr (2) planes 16 bits per channel - */ WL_SHM_FORMAT_S416 = 0x36313453, - /** - * [31:0] x:Cr:Cb:Y 2:10:10:10 little endian - */ WL_SHM_FORMAT_XVUY2101010 = 0x30335958, - /** - * 2x1 subsampled Cr:Cb plane 10 bits per channel packed - */ WL_SHM_FORMAT_P230 = 0x30333250, WL_SHM_FORMAT_T430 = 0x30333454, - /** - * 8-bit Y-only - */ WL_SHM_FORMAT_Y8 = 0x59455247, - /** - * [31:0] x:Y2:Y1:Y0 2:10:10:10 little endian - */ WL_SHM_FORMAT_XYYY2101010 = 0x34415059, }; -#endif /* WL_SHM_FORMAT_ENUM */ +#endif #ifndef WL_SHM_FORMAT_ENUM_IS_VALID #define WL_SHM_FORMAT_ENUM_IS_VALID -/** - * @ingroup iface_wl_shm - * Validate a wl_shm format value. - * - * @return true on success, false on error. - * @ref wl_shm_format - */ static inline bool wl_shm_format_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2265,105 +815,42 @@ wl_shm_format_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SHM_FORMAT_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_shm - * @struct wl_shm_interface - */ struct wl_shm_interface { - /** - * create a shm pool - * - * Create a new wl_shm_pool object. - * - * The pool can be used to create shared memory based buffer - * objects. The server will mmap size bytes of the passed file - * descriptor, to use as backing memory for the pool. - * @param id pool to create - * @param fd file descriptor for the pool - * @param size pool size, in bytes - */ void (*create_pool)(struct wl_client *client, struct wl_resource *resource, uint32_t id, int32_t fd, int32_t size); - /** - * release the shm object - * - * Using this request a client can tell the server that it is not - * going to use the shm object anymore. - * - * Objects created via this interface remain unaffected. - * @since 2 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; #define WL_SHM_FORMAT 0 -/** - * @ingroup iface_wl_shm - */ #define WL_SHM_FORMAT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm - */ #define WL_SHM_CREATE_POOL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm - */ #define WL_SHM_RELEASE_SINCE_VERSION 2 -/** - * @ingroup iface_wl_shm - * Sends an format event to the client owning the resource. - * @param resource_ The client's resource - * @param format buffer pixel format - */ static inline void wl_shm_send_format(struct wl_resource *resource_, uint32_t format) { wl_resource_post_event(resource_, WL_SHM_FORMAT, format); } -/** - * @ingroup iface_wl_buffer - * @struct wl_buffer_interface - */ struct wl_buffer_interface { - /** - * destroy a buffer - * - * Destroy a buffer. If and how you need to release the backing - * storage is defined by the buffer factory interface. - * - * For possible side-effects to a surface, see wl_surface.attach. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); }; #define WL_BUFFER_RELEASE 0 -/** - * @ingroup iface_wl_buffer - */ #define WL_BUFFER_RELEASE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_buffer - */ #define WL_BUFFER_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_buffer - * Sends an release event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_buffer_send_release(struct wl_resource *resource_) { @@ -2373,34 +860,15 @@ wl_buffer_send_release(struct wl_resource *resource_) #ifndef WL_DATA_OFFER_ERROR_ENUM #define WL_DATA_OFFER_ERROR_ENUM enum wl_data_offer_error { - /** - * finish request was called untimely - */ WL_DATA_OFFER_ERROR_INVALID_FINISH = 0, - /** - * action mask contains invalid values - */ WL_DATA_OFFER_ERROR_INVALID_ACTION_MASK = 1, - /** - * action argument has an invalid value - */ WL_DATA_OFFER_ERROR_INVALID_ACTION = 2, - /** - * offer doesn't accept this request - */ WL_DATA_OFFER_ERROR_INVALID_OFFER = 3, }; -#endif /* WL_DATA_OFFER_ERROR_ENUM */ +#endif #ifndef WL_DATA_OFFER_ERROR_ENUM_IS_VALID #define WL_DATA_OFFER_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_data_offer - * Validate a wl_data_offer error value. - * - * @return true on success, false on error. - * @ref wl_data_offer_error - */ static inline bool wl_data_offer_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2416,129 +884,21 @@ wl_data_offer_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_DATA_OFFER_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_data_offer - * @struct wl_data_offer_interface - */ struct wl_data_offer_interface { - /** - * accept one of the offered mime types - * - * Indicate that the client can accept the given mime type, or - * NULL for not accepted. - * - * For objects of version 2 or older, this request is used by the - * client to give feedback whether the client can receive the given - * mime type, or NULL if none is accepted; the feedback does not - * determine whether the drag-and-drop operation succeeds or not. - * - * For objects of version 3 or newer, this request determines the - * final result of the drag-and-drop operation. If the end result - * is that no mime types were accepted, the drag-and-drop operation - * will be cancelled and the corresponding drag source will receive - * wl_data_source.cancelled. Clients may still use this event in - * conjunction with wl_data_source.action for feedback. - * @param serial serial number of the accept request - * @param mime_type mime type accepted by the client - */ void (*accept)(struct wl_client *client, struct wl_resource *resource, uint32_t serial, const char *mime_type); - /** - * request that the data is transferred - * - * To transfer the offered data, the client issues this request - * and indicates the mime type it wants to receive. The transfer - * happens through the passed file descriptor (typically created - * with the pipe system call). The source client writes the data in - * the mime type representation requested and then closes the file - * descriptor. - * - * The receiving client reads from the read end of the pipe until - * EOF and then closes its end, at which point the transfer is - * complete. - * - * This request may happen multiple times for different mime types, - * both before and after wl_data_device.drop. Drag-and-drop - * destination clients may preemptively fetch data or examine it - * more closely to determine acceptance. - * @param mime_type mime type desired by receiver - * @param fd file descriptor for data transfer - */ void (*receive)(struct wl_client *client, struct wl_resource *resource, const char *mime_type, int32_t fd); - /** - * destroy data offer - * - * Destroy the data offer. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * the offer will no longer be used - * - * Notifies the compositor that the drag destination successfully - * finished the drag-and-drop operation. - * - * Upon receiving this request, the compositor will emit - * wl_data_source.dnd_finished on the drag source client. - * - * It is a client error to perform other requests than - * wl_data_offer.destroy after this one. It is also an error to - * perform this request after a NULL mime type has been set in - * wl_data_offer.accept or no action was received through - * wl_data_offer.action. - * - * If wl_data_offer.finish request is received for a non drag and - * drop operation, the invalid_finish protocol error is raised. - * @since 3 - */ void (*finish)(struct wl_client *client, struct wl_resource *resource); - /** - * set the available/preferred drag-and-drop actions - * - * Sets the actions that the destination side client supports for - * this operation. This request may trigger the emission of - * wl_data_source.action and wl_data_offer.action events if the - * compositor needs to change the selected action. - * - * This request can be called multiple times throughout the - * drag-and-drop operation, typically in response to - * wl_data_device.enter or wl_data_device.motion events. - * - * This request determines the final result of the drag-and-drop - * operation. If the end result is that no action is accepted, the - * drag source will receive wl_data_source.cancelled. - * - * The dnd_actions argument must contain only values expressed in - * the wl_data_device_manager.dnd_actions enum, and the - * preferred_action argument must only contain one of those values - * set, otherwise it will result in a protocol error. - * - * While managing an "ask" action, the destination drag-and-drop - * client may perform further wl_data_offer.receive requests, and - * is expected to perform one last wl_data_offer.set_actions - * request with a preferred action other than "ask" (and optionally - * wl_data_offer.accept) before requesting wl_data_offer.finish, in - * order to convey the action selected by the user. If the - * preferred action is not in the wl_data_offer.source_actions - * mask, an error will be raised. - * - * If the "ask" action is dismissed (e.g. user cancellation), the - * client is expected to perform wl_data_offer.destroy right away. - * - * This request can only be made on drag-and-drop offers, a - * protocol error will be raised otherwise. - * @param dnd_actions actions supported by the destination client - * @param preferred_action action preferred by the destination client - * @since 3 - */ void (*set_actions)(struct wl_client *client, struct wl_resource *resource, uint32_t dnd_actions, @@ -2549,70 +909,28 @@ struct wl_data_offer_interface { #define WL_DATA_OFFER_SOURCE_ACTIONS 1 #define WL_DATA_OFFER_ACTION 2 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_SOURCE_ACTIONS_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_ACTION_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_ACCEPT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_RECEIVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_FINISH_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_SET_ACTIONS_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - * Sends an offer event to the client owning the resource. - * @param resource_ The client's resource - * @param mime_type offered mime type - */ static inline void wl_data_offer_send_offer(struct wl_resource *resource_, const char *mime_type) { wl_resource_post_event(resource_, WL_DATA_OFFER_OFFER, mime_type); } -/** - * @ingroup iface_wl_data_offer - * Sends an source_actions event to the client owning the resource. - * @param resource_ The client's resource - * @param source_actions actions offered by the data source - */ static inline void wl_data_offer_send_source_actions(struct wl_resource *resource_, uint32_t source_actions) { wl_resource_post_event(resource_, WL_DATA_OFFER_SOURCE_ACTIONS, source_actions); } -/** - * @ingroup iface_wl_data_offer - * Sends an action event to the client owning the resource. - * @param resource_ The client's resource - * @param dnd_action action selected by the compositor - */ static inline void wl_data_offer_send_action(struct wl_resource *resource_, uint32_t dnd_action) { @@ -2622,26 +940,13 @@ wl_data_offer_send_action(struct wl_resource *resource_, uint32_t dnd_action) #ifndef WL_DATA_SOURCE_ERROR_ENUM #define WL_DATA_SOURCE_ERROR_ENUM enum wl_data_source_error { - /** - * action mask contains invalid values - */ WL_DATA_SOURCE_ERROR_INVALID_ACTION_MASK = 0, - /** - * source doesn't accept this request - */ WL_DATA_SOURCE_ERROR_INVALID_SOURCE = 1, }; -#endif /* WL_DATA_SOURCE_ERROR_ENUM */ +#endif #ifndef WL_DATA_SOURCE_ERROR_ENUM_IS_VALID #define WL_DATA_SOURCE_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_data_source - * Validate a wl_data_source error value. - * - * @return true on success, false on error. - * @ref wl_data_source_error - */ static inline bool wl_data_source_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2653,50 +958,14 @@ wl_data_source_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_DATA_SOURCE_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_data_source - * @struct wl_data_source_interface - */ struct wl_data_source_interface { - /** - * add an offered mime type - * - * This request adds a mime type to the set of mime types - * advertised to targets. Can be called several times to offer - * multiple types. - * @param mime_type mime type offered by the data source - */ void (*offer)(struct wl_client *client, struct wl_resource *resource, const char *mime_type); - /** - * destroy the data source - * - * Destroy the data source. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the available drag-and-drop actions - * - * Sets the actions that the source side client supports for this - * operation. This request may trigger wl_data_source.action and - * wl_data_offer.action events if the compositor needs to change - * the selected action. - * - * The dnd_actions argument must contain only values expressed in - * the wl_data_device_manager.dnd_actions enum, otherwise it will - * result in a protocol error. - * - * This request must be made once only, and can only be made on - * sources used in drag-and-drop, so it must be performed before - * wl_data_device.start_drag. Attempting to use the source other - * than for drag-and-drop will raise a protocol error. - * @param dnd_actions actions supported by the data source - * @since 3 - */ void (*set_actions)(struct wl_client *client, struct wl_resource *resource, uint32_t dnd_actions); @@ -2709,108 +978,47 @@ struct wl_data_source_interface { #define WL_DATA_SOURCE_DND_FINISHED 4 #define WL_DATA_SOURCE_ACTION 5 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_TARGET_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_SEND_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_CANCELLED_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_DND_DROP_PERFORMED_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_DND_FINISHED_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_ACTION_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_SET_ACTIONS_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - * Sends an target event to the client owning the resource. - * @param resource_ The client's resource - * @param mime_type mime type accepted by the target - */ static inline void wl_data_source_send_target(struct wl_resource *resource_, const char *mime_type) { wl_resource_post_event(resource_, WL_DATA_SOURCE_TARGET, mime_type); } -/** - * @ingroup iface_wl_data_source - * Sends an send event to the client owning the resource. - * @param resource_ The client's resource - * @param mime_type mime type for the data - * @param fd file descriptor for the data - */ static inline void wl_data_source_send_send(struct wl_resource *resource_, const char *mime_type, int32_t fd) { wl_resource_post_event(resource_, WL_DATA_SOURCE_SEND, mime_type, fd); } -/** - * @ingroup iface_wl_data_source - * Sends an cancelled event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_data_source_send_cancelled(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_DATA_SOURCE_CANCELLED); } -/** - * @ingroup iface_wl_data_source - * Sends an dnd_drop_performed event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_data_source_send_dnd_drop_performed(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_DATA_SOURCE_DND_DROP_PERFORMED); } -/** - * @ingroup iface_wl_data_source - * Sends an dnd_finished event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_data_source_send_dnd_finished(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_DATA_SOURCE_DND_FINISHED); } -/** - * @ingroup iface_wl_data_source - * Sends an action event to the client owning the resource. - * @param resource_ The client's resource - * @param dnd_action action selected by the compositor - */ static inline void wl_data_source_send_action(struct wl_resource *resource_, uint32_t dnd_action) { @@ -2820,26 +1028,13 @@ wl_data_source_send_action(struct wl_resource *resource_, uint32_t dnd_action) #ifndef WL_DATA_DEVICE_ERROR_ENUM #define WL_DATA_DEVICE_ERROR_ENUM enum wl_data_device_error { - /** - * given wl_surface has another role - */ WL_DATA_DEVICE_ERROR_ROLE = 0, - /** - * source has already been used - */ WL_DATA_DEVICE_ERROR_USED_SOURCE = 1, }; -#endif /* WL_DATA_DEVICE_ERROR_ENUM */ +#endif #ifndef WL_DATA_DEVICE_ERROR_ENUM_IS_VALID #define WL_DATA_DEVICE_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_data_device - * Validate a wl_data_device error value. - * - * @return true on success, false on error. - * @ref wl_data_device_error - */ static inline bool wl_data_device_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -2851,80 +1046,19 @@ wl_data_device_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_DATA_DEVICE_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_data_device - * @struct wl_data_device_interface - */ struct wl_data_device_interface { - /** - * start drag-and-drop operation - * - * This request asks the compositor to start a drag-and-drop - * operation on behalf of the client. - * - * The source argument is the data source that provides the data - * for the eventual data transfer. If source is NULL, enter, leave - * and motion events are sent only to the client that initiated the - * drag and the client is expected to handle the data passing - * internally. If source is destroyed, the drag-and-drop session - * will be cancelled. - * - * The origin surface is the surface where the drag originates and - * the client must have an active implicit grab that matches the - * serial. - * - * The icon surface is an optional (can be NULL) surface that - * provides an icon to be moved around with the cursor. Initially, - * the top-left corner of the icon surface is placed at the cursor - * hotspot, but subsequent wl_surface.offset requests can move the - * relative position. Attach requests must be confirmed with - * wl_surface.commit as usual. The icon surface is given the role - * of a drag-and-drop icon. If the icon surface already has another - * role, it raises a protocol error. - * - * The input region is ignored for wl_surfaces with the role of a - * drag-and-drop icon. - * - * The given source may not be used in any further set_selection or - * start_drag requests. Attempting to reuse a previously-used - * source may send a used_source error. - * @param source data source for the eventual transfer - * @param origin surface where the drag originates - * @param icon drag-and-drop icon surface - * @param serial serial number of the implicit grab on the origin - */ void (*start_drag)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *source, struct wl_resource *origin, struct wl_resource *icon, uint32_t serial); - /** - * copy data to the selection - * - * This request asks the compositor to set the selection to the - * data from the source on behalf of the client. - * - * To unset the selection, set the source to NULL. - * - * The given source may not be used in any further set_selection or - * start_drag requests. Attempting to reuse a previously-used - * source may send a used_source error. - * @param source data source for the selection - * @param serial serial number of the event that triggered this request - */ void (*set_selection)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *source, uint32_t serial); - /** - * destroy data device - * - * This request destroys the data device. - * @since 2 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; @@ -2936,114 +1070,47 @@ struct wl_data_device_interface { #define WL_DATA_DEVICE_DROP 4 #define WL_DATA_DEVICE_SELECTION 5 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_DATA_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_DROP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_SELECTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_START_DRAG_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_SET_SELECTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_RELEASE_SINCE_VERSION 2 -/** - * @ingroup iface_wl_data_device - * Sends an data_offer event to the client owning the resource. - * @param resource_ The client's resource - * @param id the new data_offer object - */ static inline void wl_data_device_send_data_offer(struct wl_resource *resource_, struct wl_resource *id) { wl_resource_post_event(resource_, WL_DATA_DEVICE_DATA_OFFER, id); } -/** - * @ingroup iface_wl_data_device - * Sends an enter event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the enter event - * @param surface client surface entered - * @param x surface-local x coordinate - * @param y surface-local y coordinate - * @param id source data_offer object - */ static inline void wl_data_device_send_enter(struct wl_resource *resource_, uint32_t serial, struct wl_resource *surface, wl_fixed_t x, wl_fixed_t y, struct wl_resource *id) { wl_resource_post_event(resource_, WL_DATA_DEVICE_ENTER, serial, surface, x, y, id); } -/** - * @ingroup iface_wl_data_device - * Sends an leave event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_data_device_send_leave(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_DATA_DEVICE_LEAVE); } -/** - * @ingroup iface_wl_data_device - * Sends an motion event to the client owning the resource. - * @param resource_ The client's resource - * @param time timestamp with millisecond granularity - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ static inline void wl_data_device_send_motion(struct wl_resource *resource_, uint32_t time, wl_fixed_t x, wl_fixed_t y) { wl_resource_post_event(resource_, WL_DATA_DEVICE_MOTION, time, x, y); } -/** - * @ingroup iface_wl_data_device - * Sends an drop event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_data_device_send_drop(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_DATA_DEVICE_DROP); } -/** - * @ingroup iface_wl_data_device - * Sends an selection event to the client owning the resource. - * @param resource_ The client's resource - * @param id selection data_offer object - */ static inline void wl_data_device_send_selection(struct wl_resource *resource_, struct wl_resource *id) { @@ -3052,63 +1119,16 @@ wl_data_device_send_selection(struct wl_resource *resource_, struct wl_resource #ifndef WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM #define WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM -/** - * @ingroup iface_wl_data_device_manager - * drag and drop actions - * - * This is a bitmask of the available/preferred actions in a - * drag-and-drop operation. - * - * In the compositor, the selected action is a result of matching the - * actions offered by the source and destination sides. "action" events - * with a "none" action will be sent to both source and destination if - * there is no match. All further checks will effectively happen on - * (source actions ∩ destination actions). - * - * In addition, compositors may also pick different actions in - * reaction to key modifiers being pressed. One common design that - * is used in major toolkits (and the behavior recommended for - * compositors) is: - * - * - If no modifiers are pressed, the first match (in bit order) - * will be used. - * - Pressing Shift selects "move", if enabled in the mask. - * - Pressing Control selects "copy", if enabled in the mask. - * - * Behavior beyond that is considered implementation-dependent. - * Compositors may for example bind other modifiers (like Alt/Meta) - * or drags initiated with other buttons than BTN_LEFT to specific - * actions (e.g. "ask"). - */ enum wl_data_device_manager_dnd_action { - /** - * no action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE = 0, - /** - * copy action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY = 1, - /** - * move action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE = 2, - /** - * ask action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK = 4, }; -#endif /* WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM */ +#endif #ifndef WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM_IS_VALID #define WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM_IS_VALID -/** - * @ingroup iface_wl_data_device_manager - * Validate a wl_data_device_manager dnd_action value. - * - * @return true on success, false on error. - * @ref wl_data_device_manager_dnd_action - */ static inline bool wl_data_device_manager_dnd_action_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -3122,77 +1142,34 @@ wl_data_device_manager_dnd_action_is_valid(uint32_t value, uint32_t version) { valid |= WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK; return (value & ~valid) == 0; } -#endif /* WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_data_device_manager - * @struct wl_data_device_manager_interface - */ struct wl_data_device_manager_interface { - /** - * create a new data source - * - * Create a new data source. - * @param id data source to create - */ void (*create_data_source)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * create a new data device - * - * Create a new data device for a given seat. - * @param id data device to create - * @param seat seat associated with the data device - */ void (*get_data_device)(struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *seat); - /** - * destroy wl_data_device_manager - * - * This request destroys the wl_data_device_manager. This has no - * effect on any other objects. - * @since 4 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; -/** - * @ingroup iface_wl_data_device_manager - */ #define WL_DATA_DEVICE_MANAGER_CREATE_DATA_SOURCE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device_manager - */ #define WL_DATA_DEVICE_MANAGER_GET_DATA_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device_manager - */ #define WL_DATA_DEVICE_MANAGER_RELEASE_SINCE_VERSION 4 #ifndef WL_SHELL_ERROR_ENUM #define WL_SHELL_ERROR_ENUM enum wl_shell_error { - /** - * given wl_surface has another role - */ WL_SHELL_ERROR_ROLE = 0, }; -#endif /* WL_SHELL_ERROR_ENUM */ +#endif #ifndef WL_SHELL_ERROR_ENUM_IS_VALID #define WL_SHELL_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_shell - * Validate a wl_shell error value. - * - * @return true on success, false on error. - * @ref wl_shell_error - */ static inline bool wl_shell_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -3202,24 +1179,9 @@ wl_shell_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SHELL_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_shell - * @struct wl_shell_interface - */ struct wl_shell_interface { - /** - * create a shell surface from a surface - * - * Create a shell surface for an existing surface. This gives the - * wl_surface the role of a shell surface. If the wl_surface - * already has another role, it raises a protocol error. - * - * Only one shell surface can be associated with a given surface. - * @param id shell surface to create - * @param surface surface to be given the shell surface role - */ void (*get_shell_surface)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -3227,71 +1189,25 @@ struct wl_shell_interface { }; -/** - * @ingroup iface_wl_shell - */ #define WL_SHELL_GET_SHELL_SURFACE_SINCE_VERSION 1 #ifndef WL_SHELL_SURFACE_RESIZE_ENUM #define WL_SHELL_SURFACE_RESIZE_ENUM -/** - * @ingroup iface_wl_shell_surface - * edge values for resizing - * - * These values are used to indicate which edge of a surface - * is being dragged in a resize operation. The server may - * use this information to adapt its behavior, e.g. choose - * an appropriate cursor image. - */ enum wl_shell_surface_resize { - /** - * no edge - */ WL_SHELL_SURFACE_RESIZE_NONE = 0, - /** - * top edge - */ WL_SHELL_SURFACE_RESIZE_TOP = 1, - /** - * bottom edge - */ WL_SHELL_SURFACE_RESIZE_BOTTOM = 2, - /** - * left edge - */ WL_SHELL_SURFACE_RESIZE_LEFT = 4, - /** - * top and left edges - */ WL_SHELL_SURFACE_RESIZE_TOP_LEFT = 5, - /** - * bottom and left edges - */ WL_SHELL_SURFACE_RESIZE_BOTTOM_LEFT = 6, - /** - * right edge - */ WL_SHELL_SURFACE_RESIZE_RIGHT = 8, - /** - * top and right edges - */ WL_SHELL_SURFACE_RESIZE_TOP_RIGHT = 9, - /** - * bottom and right edges - */ WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT = 10, }; -#endif /* WL_SHELL_SURFACE_RESIZE_ENUM */ +#endif #ifndef WL_SHELL_SURFACE_RESIZE_ENUM_IS_VALID #define WL_SHELL_SURFACE_RESIZE_ENUM_IS_VALID -/** - * @ingroup iface_wl_shell_surface - * Validate a wl_shell_surface resize value. - * - * @return true on success, false on error. - * @ref wl_shell_surface_resize - */ static inline bool wl_shell_surface_resize_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -3315,34 +1231,17 @@ wl_shell_surface_resize_is_valid(uint32_t value, uint32_t version) { valid |= WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT; return (value & ~valid) == 0; } -#endif /* WL_SHELL_SURFACE_RESIZE_ENUM_IS_VALID */ +#endif #ifndef WL_SHELL_SURFACE_TRANSIENT_ENUM #define WL_SHELL_SURFACE_TRANSIENT_ENUM -/** - * @ingroup iface_wl_shell_surface - * details of transient behaviour - * - * These flags specify details of the expected behaviour - * of transient surfaces. Used in the set_transient request. - */ enum wl_shell_surface_transient { - /** - * do not set keyboard focus - */ WL_SHELL_SURFACE_TRANSIENT_INACTIVE = 0x1, }; -#endif /* WL_SHELL_SURFACE_TRANSIENT_ENUM */ +#endif #ifndef WL_SHELL_SURFACE_TRANSIENT_ENUM_IS_VALID #define WL_SHELL_SURFACE_TRANSIENT_ENUM_IS_VALID -/** - * @ingroup iface_wl_shell_surface - * Validate a wl_shell_surface transient value. - * - * @return true on success, false on error. - * @ref wl_shell_surface_transient - */ static inline bool wl_shell_surface_transient_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -3350,47 +1249,20 @@ wl_shell_surface_transient_is_valid(uint32_t value, uint32_t version) { valid |= WL_SHELL_SURFACE_TRANSIENT_INACTIVE; return (value & ~valid) == 0; } -#endif /* WL_SHELL_SURFACE_TRANSIENT_ENUM_IS_VALID */ +#endif #ifndef WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM #define WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM -/** - * @ingroup iface_wl_shell_surface - * different method to set the surface fullscreen - * - * Hints to indicate to the compositor how to deal with a conflict - * between the dimensions of the surface and the dimensions of the - * output. The compositor is free to ignore this parameter. - */ enum wl_shell_surface_fullscreen_method { - /** - * no preference, apply default policy - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_DEFAULT = 0, - /** - * scale, preserve the surface's aspect ratio and center on output - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_SCALE = 1, - /** - * switch output mode to the smallest mode that can fit the surface, add black borders to compensate size mismatch - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_DRIVER = 2, - /** - * no upscaling, center on output and add black borders to compensate size mismatch - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_FILL = 3, }; -#endif /* WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM */ +#endif #ifndef WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM_IS_VALID #define WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM_IS_VALID -/** - * @ingroup iface_wl_shell_surface - * Validate a wl_shell_surface fullscreen_method value. - * - * @return true on success, false on error. - * @ref wl_shell_surface_fullscreen_method - */ static inline bool wl_shell_surface_fullscreen_method_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -3406,160 +1278,34 @@ wl_shell_surface_fullscreen_method_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_shell_surface - * @struct wl_shell_surface_interface - */ struct wl_shell_surface_interface { - /** - * respond to a ping event - * - * A client must respond to a ping event with a pong request or - * the client may be deemed unresponsive. - * @param serial serial number of the ping event - */ void (*pong)(struct wl_client *client, struct wl_resource *resource, uint32_t serial); - /** - * start an interactive move - * - * Start a pointer-driven move of the surface. - * - * This request must be used in response to a button press event. - * The server may ignore move requests depending on the state of - * the surface (e.g. fullscreen or maximized). - * @param seat seat whose pointer is used - * @param serial serial number of the implicit grab on the pointer - */ void (*move)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, uint32_t serial); - /** - * start an interactive resize - * - * Start a pointer-driven resizing of the surface. - * - * This request must be used in response to a button press event. - * The server may ignore resize requests depending on the state of - * the surface (e.g. fullscreen or maximized). - * @param seat seat whose pointer is used - * @param serial serial number of the implicit grab on the pointer - * @param edges which edge or corner is being dragged - */ void (*resize)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, uint32_t serial, uint32_t edges); - /** - * make the surface a toplevel surface - * - * Map the surface as a toplevel surface. - * - * A toplevel surface is not fullscreen, maximized or transient. - */ void (*set_toplevel)(struct wl_client *client, struct wl_resource *resource); - /** - * make the surface a transient surface - * - * Map the surface relative to an existing surface. - * - * The x and y arguments specify the location of the upper left - * corner of the surface relative to the upper left corner of the - * parent surface, in surface-local coordinates. - * - * The flags argument controls details of the transient behaviour. - * @param parent parent surface - * @param x surface-local x coordinate - * @param y surface-local y coordinate - * @param flags transient surface behavior - */ void (*set_transient)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *parent, int32_t x, int32_t y, uint32_t flags); - /** - * make the surface a fullscreen surface - * - * Map the surface as a fullscreen surface. - * - * If an output parameter is given then the surface will be made - * fullscreen on that output. If the client does not specify the - * output then the compositor will apply its policy - usually - * choosing the output on which the surface has the biggest surface - * area. - * - * The client may specify a method to resolve a size conflict - * between the output size and the surface size - this is provided - * through the method parameter. - * - * The framerate parameter is used only when the method is set to - * "driver", to indicate the preferred framerate. A value of 0 - * indicates that the client does not care about framerate. The - * framerate is specified in mHz, that is framerate of 60000 is - * 60Hz. - * - * A method of "scale" or "driver" implies a scaling operation of - * the surface, either via a direct scaling operation or a change - * of the output mode. This will override any kind of output - * scaling, so that mapping a surface with a buffer size equal to - * the mode can fill the screen independent of buffer_scale. - * - * A method of "fill" means we don't scale up the buffer, however - * any output scale is applied. This means that you may run into an - * edge case where the application maps a buffer with the same size - * of the output mode but buffer_scale 1 (thus making a surface - * larger than the output). In this case it is allowed to downscale - * the results to fit the screen. - * - * The compositor must reply to this request with a configure event - * with the dimensions for the output on which the surface will be - * made fullscreen. - * @param method method for resolving size conflict - * @param framerate framerate in mHz - * @param output output on which the surface is to be fullscreen - */ void (*set_fullscreen)(struct wl_client *client, struct wl_resource *resource, uint32_t method, uint32_t framerate, struct wl_resource *output); - /** - * make the surface a popup surface - * - * Map the surface as a popup. - * - * A popup surface is a transient surface with an added pointer - * grab. - * - * An existing implicit grab will be changed to owner-events mode, - * and the popup grab will continue after the implicit grab ends - * (i.e. releasing the mouse button does not cause the popup to be - * unmapped). - * - * The popup grab continues until the window is destroyed or a - * mouse button is pressed in any other client's window. A click in - * any of the client's surfaces is reported as normal, however, - * clicks in other clients' surfaces will be discarded and trigger - * the callback. - * - * The x and y arguments specify the location of the upper left - * corner of the surface relative to the upper left corner of the - * parent surface, in surface-local coordinates. - * @param seat seat whose pointer is used - * @param serial serial number of the implicit grab on the pointer - * @param parent parent surface - * @param x surface-local x coordinate - * @param y surface-local y coordinate - * @param flags transient surface behavior - */ void (*set_popup)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, @@ -3568,58 +1314,12 @@ struct wl_shell_surface_interface { int32_t x, int32_t y, uint32_t flags); - /** - * make the surface a maximized surface - * - * Map the surface as a maximized surface. - * - * If an output parameter is given then the surface will be - * maximized on that output. If the client does not specify the - * output then the compositor will apply its policy - usually - * choosing the output on which the surface has the biggest surface - * area. - * - * The compositor will reply with a configure event telling the - * expected new surface size. The operation is completed on the - * next buffer attach to this surface. - * - * A maximized surface typically fills the entire output it is - * bound to, except for desktop elements such as panels. This is - * the main difference between a maximized shell surface and a - * fullscreen shell surface. - * - * The details depend on the compositor implementation. - * @param output output on which the surface is to be maximized - */ void (*set_maximized)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *output); - /** - * set surface title - * - * Set a short title for the surface. - * - * This string may be used to identify the surface in a task bar, - * window list, or other user interface elements provided by the - * compositor. - * - * The string must be encoded in UTF-8. - * @param title surface title - */ void (*set_title)(struct wl_client *client, struct wl_resource *resource, const char *title); - /** - * set surface class - * - * Set a class for the surface. - * - * The surface class identifies the general class of applications - * to which the surface belongs. A common convention is to use the - * file name (or the full path if it is a non-standard location) of - * the application's .desktop file as the class. - * @param class_ surface class - */ void (*set_class)(struct wl_client *client, struct wl_resource *resource, const char *class_); @@ -3629,91 +1329,33 @@ struct wl_shell_surface_interface { #define WL_SHELL_SURFACE_CONFIGURE 1 #define WL_SHELL_SURFACE_POPUP_DONE 2 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_PING_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_POPUP_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_PONG_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_MOVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_RESIZE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_TOPLEVEL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_TRANSIENT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_POPUP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_TITLE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_CLASS_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - * Sends an ping event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the ping - */ static inline void wl_shell_surface_send_ping(struct wl_resource *resource_, uint32_t serial) { wl_resource_post_event(resource_, WL_SHELL_SURFACE_PING, serial); } -/** - * @ingroup iface_wl_shell_surface - * Sends an configure event to the client owning the resource. - * @param resource_ The client's resource - * @param edges how the surface was resized - * @param width new width of the surface - * @param height new height of the surface - */ static inline void wl_shell_surface_send_configure(struct wl_resource *resource_, uint32_t edges, int32_t width, int32_t height) { wl_resource_post_event(resource_, WL_SHELL_SURFACE_CONFIGURE, edges, width, height); } -/** - * @ingroup iface_wl_shell_surface - * Sends an popup_done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_shell_surface_send_popup_done(struct wl_resource *resource_) { @@ -3722,49 +1364,18 @@ wl_shell_surface_send_popup_done(struct wl_resource *resource_) #ifndef WL_SURFACE_ERROR_ENUM #define WL_SURFACE_ERROR_ENUM -/** - * @ingroup iface_wl_surface - * wl_surface error values - * - * These errors can be emitted in response to wl_surface requests. - */ enum wl_surface_error { - /** - * buffer scale value is invalid - */ WL_SURFACE_ERROR_INVALID_SCALE = 0, - /** - * buffer transform value is invalid - */ WL_SURFACE_ERROR_INVALID_TRANSFORM = 1, - /** - * buffer size is invalid - */ WL_SURFACE_ERROR_INVALID_SIZE = 2, - /** - * buffer offset is invalid - */ WL_SURFACE_ERROR_INVALID_OFFSET = 3, - /** - * surface was destroyed before its role object - */ WL_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 4, - /** - * no buffer was attached - */ WL_SURFACE_ERROR_NO_BUFFER = 5, }; -#endif /* WL_SURFACE_ERROR_ENUM */ +#endif #ifndef WL_SURFACE_ERROR_ENUM_IS_VALID #define WL_SURFACE_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_surface - * Validate a wl_surface error value. - * - * @return true on success, false on error. - * @ref wl_surface_error - */ static inline bool wl_surface_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -3784,479 +1395,49 @@ wl_surface_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SURFACE_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_surface - * @struct wl_surface_interface - */ struct wl_surface_interface { - /** - * delete surface - * - * Deletes the surface and invalidates its object ID. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * set the surface contents - * - * Set a buffer as the content of this surface. - * - * The new size of the surface is calculated based on the buffer - * size transformed by the inverse buffer_transform and the inverse - * buffer_scale. This means that at commit time the supplied buffer - * size must be an integer multiple of the buffer_scale. If that's - * not the case, an invalid_size error is sent. - * - * The x and y arguments specify the location of the new pending - * buffer's upper left corner, relative to the current buffer's - * upper left corner, in surface-local coordinates. In other words, - * the x and y, combined with the new surface size define in which - * directions the surface's size changes. Setting anything other - * than 0 as x and y arguments is discouraged, and should instead - * be replaced with using the separate wl_surface.offset request. - * - * When the bound wl_surface version is 5 or higher, passing any - * non-zero x or y is a protocol violation, and will result in an - * 'invalid_offset' error being raised. The x and y arguments are - * ignored and do not change the pending state. To achieve - * equivalent semantics, use wl_surface.offset. - * - * Surface contents are double-buffered state, see - * wl_surface.commit. - * - * The initial surface contents are void; there is no content. - * wl_surface.attach assigns the given wl_buffer as the pending - * wl_buffer. wl_surface.commit makes the pending wl_buffer the new - * surface contents, and the size of the surface becomes the size - * calculated from the wl_buffer, as described above. After commit, - * there is no pending buffer until the next attach. - * - * Committing a pending wl_buffer allows the compositor to read the - * pixels in the wl_buffer. The compositor may access the pixels at - * any time after the wl_surface.commit request. When the - * compositor will not access the pixels anymore, it will send the - * wl_buffer.release event. Only after receiving wl_buffer.release, - * the client may reuse the wl_buffer. A wl_buffer that has been - * attached and then replaced by another attach instead of - * committed will not receive a release event, and is not used by - * the compositor. - * - * If a pending wl_buffer has been committed to more than one - * wl_surface, the delivery of wl_buffer.release events becomes - * undefined. A well behaved client should not rely on - * wl_buffer.release events in this case. Instead, clients hitting - * this case should use wl_surface.get_release or use a protocol - * extension providing per-commit release notifications (if none of - * these options are available, a fallback can be implemented by - * creating multiple wl_buffer objects from the same backing - * storage). - * - * Destroying the wl_buffer after wl_buffer.release does not change - * the surface contents. Destroying the wl_buffer before - * wl_buffer.release is allowed as long as the underlying buffer - * storage isn't re-used (this can happen e.g. on client process - * termination). However, if the client destroys the wl_buffer - * before receiving the wl_buffer.release event and mutates the - * underlying buffer storage, the surface contents become undefined - * immediately. - * - * If wl_surface.attach is sent with a NULL wl_buffer, the - * following wl_surface.commit will remove the surface content. - * - * If a pending wl_buffer has been destroyed, the result is not - * specified. Many compositors are known to remove the surface - * content on the following wl_surface.commit, but this behaviour - * is not universal. Clients seeking to maximise compatibility - * should not destroy pending buffers and should ensure that they - * explicitly remove content from surfaces, even after destroying - * buffers. - * @param buffer buffer of surface contents - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ void (*attach)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *buffer, int32_t x, int32_t y); - /** - * mark part of the surface damaged - * - * This request is used to describe the regions where the pending - * buffer is different from the current surface contents, and where - * the surface therefore needs to be repainted. The compositor - * ignores the parts of the damage that fall outside of the - * surface. - * - * Damage is double-buffered state, see wl_surface.commit. - * - * The damage rectangle is specified in surface-local coordinates, - * where x and y specify the upper left corner of the damage - * rectangle. - * - * The initial value for pending damage is empty: no damage. - * wl_surface.damage adds pending damage: the new pending damage is - * the union of old pending damage and the given rectangle. - * - * wl_surface.commit assigns pending damage as the current damage, - * and clears pending damage. The server will clear the current - * damage as it repaints the surface. - * - * Note! New clients should not use this request. Instead damage - * can be posted with wl_surface.damage_buffer which uses buffer - * coordinates instead of surface coordinates. - * @param x surface-local x coordinate - * @param y surface-local y coordinate - * @param width width of damage rectangle - * @param height height of damage rectangle - */ void (*damage)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * request a frame throttling hint - * - * Request a notification when it is a good time to start drawing - * a new frame, by creating a frame callback. This is useful for - * throttling redrawing operations, and driving animations. - * - * When a client is animating on a wl_surface, it can use the - * 'frame' request to get notified when it is a good time to draw - * and commit the next frame of animation. If the client commits an - * update earlier than that, it is likely that some updates will - * not make it to the display, and the client is wasting resources - * by drawing too often. - * - * The frame request will take effect on the next - * wl_surface.commit. The notification will only be posted for one - * frame unless requested again. For a wl_surface, the - * notifications are posted in the order the frame requests were - * committed. - * - * The server must send the notifications so that a client will not - * send excessive updates, while still allowing the highest - * possible update rate for clients that wait for the reply before - * drawing again. The server should give some time for the client - * to draw and commit after sending the frame callback events to - * let it hit the next output refresh. - * - * A server should avoid signaling the frame callbacks if the - * surface is not visible in any way, e.g. the surface is - * off-screen, or completely obscured by other opaque surfaces. - * - * The object returned by this request will be destroyed by the - * compositor after the callback is fired and as such the client - * must not attempt to use it after that point. - * - * The callback_data passed in the callback is the current time, in - * milliseconds, with an undefined base. - * @param callback callback object for the frame request - */ void (*frame)(struct wl_client *client, struct wl_resource *resource, uint32_t callback); - /** - * set opaque region - * - * This request sets the region of the surface that contains - * opaque content. - * - * The opaque region is an optimization hint for the compositor - * that lets it optimize the redrawing of content behind opaque - * regions. Setting an opaque region is not required for correct - * behaviour, but marking transparent content as opaque will result - * in repaint artifacts. - * - * The opaque region is specified in surface-local coordinates. - * - * The compositor ignores the parts of the opaque region that fall - * outside of the surface. - * - * Opaque region is double-buffered state, see wl_surface.commit. - * - * wl_surface.set_opaque_region changes the pending opaque region. - * wl_surface.commit copies the pending region to the current - * region. Otherwise, the pending and current regions are never - * changed. - * - * The initial value for an opaque region is empty. Setting the - * pending opaque region has copy semantics, and the wl_region - * object can be destroyed immediately. A NULL wl_region causes the - * pending opaque region to be set to empty. - * @param region opaque region of the surface - */ void (*set_opaque_region)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *region); - /** - * set input region - * - * This request sets the region of the surface that can receive - * pointer and touch events. - * - * Input events happening outside of this region will try the next - * surface in the server surface stack. The compositor ignores the - * parts of the input region that fall outside of the surface. - * - * The input region is specified in surface-local coordinates. - * - * Input region is double-buffered state, see wl_surface.commit. - * - * wl_surface.set_input_region changes the pending input region. - * wl_surface.commit copies the pending region to the current - * region. Otherwise the pending and current regions are never - * changed, except cursor and icon surfaces are special cases, see - * wl_pointer.set_cursor and wl_data_device.start_drag. - * - * The initial value for an input region is infinite. That means - * the whole surface will accept input. Setting the pending input - * region has copy semantics, and the wl_region object can be - * destroyed immediately. A NULL wl_region causes the input region - * to be set to infinite. - * @param region input region of the surface - */ void (*set_input_region)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *region); - /** - * commit pending surface state - * - * Surface state (input, opaque, and damage regions, attached - * buffers, etc.) is double-buffered. Protocol requests modify the - * pending state, as opposed to the active state in use by the - * compositor. - * - * All requests that need a commit to become effective are - * documented to affect double-buffered state. - * - * Other interfaces may add further double-buffered surface state. - * - * A commit request atomically creates a Content Update (CU) from - * the pending state, even if the pending state has not been - * touched. The content update is placed at the end of a - * per-surface queue until it becomes active. After commit, the new - * pending state is as documented for each related request. - * - * A CU is either a Desync Content Update (DCU) or a Sync Content - * Update (SCU). If the surface is effectively synchronized at the - * commit request, it is a SCU, otherwise a DCU. - * - * When a surface transitions from effectively synchronized to - * effectively desynchronized, all SCUs in its queue which are not - * reachable by any DCU become DCUs and dependency edges from - * outside the queue to these CUs are removed. - * - * See wl_subsurface for the definition of 'effectively - * synchronized' and 'effectively desynchronized'. - * - * When a CU is placed in the queue, the CU has a dependency on the - * CU in front of it and to the SCU at end of the queue of every - * direct child surface if that SCU exists and does not have - * another dependent. This can form a directed acyclic graph of CUs - * with dependencies as edges. - * - * In addition to surface state, the CU can have constraints that - * must be satisfied before it can be applied. Other interfaces may - * add CU constraints. - * - * All DCUs which do not have a SCU in front of themselves in their - * queue, are candidates. If the graph that's reachable by a - * candidate does not have any unsatisfied constraints, the entire - * graph must be applied atomically. - * - * When a CU is applied, the wl_buffer is applied before all other - * state. This means that all coordinates in double-buffered state - * are relative to the newly attached wl_buffers, except for - * wl_surface.attach itself. If there is no newly attached - * wl_buffer, the coordinates are relative to the previous content - * update. - */ void (*commit)(struct wl_client *client, struct wl_resource *resource); - /** - * sets the buffer transformation - * - * This request sets the transformation that the client has - * already applied to the content of the buffer. The accepted - * values for the transform parameter are the values for - * wl_output.transform. - * - * The compositor applies the inverse of this transformation - * whenever it uses the buffer contents. - * - * Buffer transform is double-buffered state, see - * wl_surface.commit. - * - * A newly created surface has its buffer transformation set to - * normal. - * - * wl_surface.set_buffer_transform changes the pending buffer - * transformation. wl_surface.commit copies the pending buffer - * transformation to the current one. Otherwise, the pending and - * current values are never changed. - * - * The purpose of this request is to allow clients to render - * content according to the output transform, thus permitting the - * compositor to use certain optimizations even if the display is - * rotated. Using hardware overlays and scanning out a client - * buffer for fullscreen surfaces are examples of such - * optimizations. Those optimizations are highly dependent on the - * compositor implementation, so the use of this request should be - * considered on a case-by-case basis. - * - * Note that if the transform value includes 90 or 270 degree - * rotation, the width of the buffer will become the surface height - * and the height of the buffer will become the surface width. - * - * If transform is not one of the values from the - * wl_output.transform enum the invalid_transform protocol error is - * raised. - * @param transform transform for interpreting buffer contents - * @since 2 - */ void (*set_buffer_transform)(struct wl_client *client, struct wl_resource *resource, int32_t transform); - /** - * sets the buffer scaling factor - * - * This request sets an optional scaling factor on how the - * compositor interprets the contents of the buffer attached to the - * window. - * - * Buffer scale is double-buffered state, see wl_surface.commit. - * - * A newly created surface has its buffer scale set to 1. - * - * wl_surface.set_buffer_scale changes the pending buffer scale. - * wl_surface.commit copies the pending buffer scale to the current - * one. Otherwise, the pending and current values are never - * changed. - * - * The purpose of this request is to allow clients to supply higher - * resolution buffer data for use on high resolution outputs. It is - * intended that you pick the same buffer scale as the scale of the - * output that the surface is displayed on. This means the - * compositor can avoid scaling when rendering the surface on that - * output. - * - * Note that if the scale is larger than 1, then you have to attach - * a buffer that is larger (by a factor of scale in each dimension) - * than the desired surface size. - * - * If scale is not greater than 0 the invalid_scale protocol error - * is raised. - * @param scale scale for interpreting buffer contents - * @since 3 - */ void (*set_buffer_scale)(struct wl_client *client, struct wl_resource *resource, int32_t scale); - /** - * mark part of the surface damaged using buffer coordinates - * - * This request is used to describe the regions where the pending - * buffer is different from the current surface contents, and where - * the surface therefore needs to be repainted. The compositor - * ignores the parts of the damage that fall outside of the - * surface. - * - * Damage is double-buffered state, see wl_surface.commit. - * - * The damage rectangle is specified in buffer coordinates, where x - * and y specify the upper left corner of the damage rectangle. - * - * The initial value for pending damage is empty: no damage. - * wl_surface.damage_buffer adds pending damage: the new pending - * damage is the union of old pending damage and the given - * rectangle. - * - * wl_surface.commit assigns pending damage as the current damage, - * and clears pending damage. The server will clear the current - * damage as it repaints the surface. - * - * This request differs from wl_surface.damage in only one way - it - * takes damage in buffer coordinates instead of surface-local - * coordinates. While this generally is more intuitive than surface - * coordinates, it is especially desirable when using wp_viewport - * or when a drawing library (like EGL) is unaware of buffer scale - * and buffer transform. - * - * Note: Because buffer transformation changes and damage requests - * may be interleaved in the protocol stream, it is impossible to - * determine the actual mapping between surface and buffer damage - * until wl_surface.commit time. Therefore, compositors wishing to - * take both kinds of damage into account will have to accumulate - * damage from the two requests separately and only transform from - * one to the other after receiving the wl_surface.commit. - * @param x buffer-local x coordinate - * @param y buffer-local y coordinate - * @param width width of damage rectangle - * @param height height of damage rectangle - * @since 4 - */ void (*damage_buffer)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * set the surface contents offset - * - * The x and y arguments specify the location of the new pending - * buffer's upper left corner, relative to the current buffer's - * upper left corner, in surface-local coordinates. In other words, - * the x and y, combined with the new surface size define in which - * directions the surface's size changes. - * - * The exact semantics of wl_surface.offset are role-specific. - * Refer to the documentation of specific roles for more - * information. - * - * Surface location offset is double-buffered state, see - * wl_surface.commit. - * - * This request is semantically equivalent to and the replaces the - * x and y arguments in the wl_surface.attach request in wl_surface - * versions prior to 5. See wl_surface.attach for details. - * @param x surface-local x coordinate - * @param y surface-local y coordinate - * @since 5 - */ void (*offset)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y); - /** - * get a release callback - * - * Create a callback for the release of the buffer attached by - * the client with wl_surface.attach. - * - * The compositor will release the buffer when it has finished its - * usage of the underlying storage for the relevant commit. Once - * the client receives this event, and assuming the associated - * buffer is not pending release from other wl_surface.commit - * requests, the client can safely re-use the buffer. - * - * Release callbacks are double-buffered state, and will be - * associated with the pending buffer at wl_surface.commit time. - * - * The callback_data passed in the wl_callback.done event is unused - * and is always zero. - * - * Sending this request without attaching a non-null buffer in the - * same content update is a protocol error. The compositor will - * send the no_buffer error in this case. - * @param callback callback object for the release - * @since 7 - */ void (*get_release)(struct wl_client *client, struct wl_resource *resource, uint32_t callback); @@ -4267,114 +1448,42 @@ struct wl_surface_interface { #define WL_SURFACE_PREFERRED_BUFFER_SCALE 2 #define WL_SURFACE_PREFERRED_BUFFER_TRANSFORM 3 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION 6 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_PREFERRED_BUFFER_TRANSFORM_SINCE_VERSION 6 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_ATTACH_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_DAMAGE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_FRAME_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_OPAQUE_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_INPUT_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_COMMIT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_BUFFER_TRANSFORM_SINCE_VERSION 2 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_BUFFER_SCALE_SINCE_VERSION 3 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION 4 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_OFFSET_SINCE_VERSION 5 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_GET_RELEASE_SINCE_VERSION 7 -/** - * @ingroup iface_wl_surface - * Sends an enter event to the client owning the resource. - * @param resource_ The client's resource - * @param output output entered by the surface - */ static inline void wl_surface_send_enter(struct wl_resource *resource_, struct wl_resource *output) { wl_resource_post_event(resource_, WL_SURFACE_ENTER, output); } -/** - * @ingroup iface_wl_surface - * Sends an leave event to the client owning the resource. - * @param resource_ The client's resource - * @param output output left by the surface - */ static inline void wl_surface_send_leave(struct wl_resource *resource_, struct wl_resource *output) { wl_resource_post_event(resource_, WL_SURFACE_LEAVE, output); } -/** - * @ingroup iface_wl_surface - * Sends an preferred_buffer_scale event to the client owning the resource. - * @param resource_ The client's resource - * @param factor preferred scaling factor - */ static inline void wl_surface_send_preferred_buffer_scale(struct wl_resource *resource_, int32_t factor) { wl_resource_post_event(resource_, WL_SURFACE_PREFERRED_BUFFER_SCALE, factor); } -/** - * @ingroup iface_wl_surface - * Sends an preferred_buffer_transform event to the client owning the resource. - * @param resource_ The client's resource - * @param transform preferred transform - */ static inline void wl_surface_send_preferred_buffer_transform(struct wl_resource *resource_, uint32_t transform) { @@ -4383,38 +1492,15 @@ wl_surface_send_preferred_buffer_transform(struct wl_resource *resource_, uint32 #ifndef WL_SEAT_CAPABILITY_ENUM #define WL_SEAT_CAPABILITY_ENUM -/** - * @ingroup iface_wl_seat - * seat capability bitmask - * - * This is a bitmask of capabilities this seat has; if a member is - * set, then it is present on the seat. - */ enum wl_seat_capability { - /** - * the seat has pointer devices - */ WL_SEAT_CAPABILITY_POINTER = 1, - /** - * the seat has one or more keyboards - */ WL_SEAT_CAPABILITY_KEYBOARD = 2, - /** - * the seat has touch devices - */ WL_SEAT_CAPABILITY_TOUCH = 4, }; -#endif /* WL_SEAT_CAPABILITY_ENUM */ +#endif #ifndef WL_SEAT_CAPABILITY_ENUM_IS_VALID #define WL_SEAT_CAPABILITY_ENUM_IS_VALID -/** - * @ingroup iface_wl_seat - * Validate a wl_seat capability value. - * - * @return true on success, false on error. - * @ref wl_seat_capability - */ static inline bool wl_seat_capability_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -4426,33 +1512,17 @@ wl_seat_capability_is_valid(uint32_t value, uint32_t version) { valid |= WL_SEAT_CAPABILITY_TOUCH; return (value & ~valid) == 0; } -#endif /* WL_SEAT_CAPABILITY_ENUM_IS_VALID */ +#endif #ifndef WL_SEAT_ERROR_ENUM #define WL_SEAT_ERROR_ENUM -/** - * @ingroup iface_wl_seat - * wl_seat error values - * - * These errors can be emitted in response to wl_seat requests. - */ enum wl_seat_error { - /** - * get_pointer, get_keyboard or get_touch called on seat without the matching capability - */ WL_SEAT_ERROR_MISSING_CAPABILITY = 0, }; -#endif /* WL_SEAT_ERROR_ENUM */ +#endif #ifndef WL_SEAT_ERROR_ENUM_IS_VALID #define WL_SEAT_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_seat - * Validate a wl_seat error value. - * - * @return true on success, false on error. - * @ref wl_seat_error - */ static inline bool wl_seat_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -4462,68 +1532,18 @@ wl_seat_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SEAT_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_seat - * @struct wl_seat_interface - */ struct wl_seat_interface { - /** - * return pointer object - * - * The ID provided will be initialized to the wl_pointer - * interface for this seat. - * - * This request only takes effect if the seat has the pointer - * capability, or has had the pointer capability in the past. It is - * a protocol violation to issue this request on a seat that has - * never had the pointer capability. The missing_capability error - * will be sent in this case. - * @param id seat pointer - */ void (*get_pointer)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * return keyboard object - * - * The ID provided will be initialized to the wl_keyboard - * interface for this seat. - * - * This request only takes effect if the seat has the keyboard - * capability, or has had the keyboard capability in the past. It - * is a protocol violation to issue this request on a seat that has - * never had the keyboard capability. The missing_capability error - * will be sent in this case. - * @param id seat keyboard - */ void (*get_keyboard)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * return touch object - * - * The ID provided will be initialized to the wl_touch interface - * for this seat. - * - * This request only takes effect if the seat has the touch - * capability, or has had the touch capability in the past. It is a - * protocol violation to issue this request on a seat that has - * never had the touch capability. The missing_capability error - * will be sent in this case. - * @param id seat touch interface - */ void (*get_touch)(struct wl_client *client, struct wl_resource *resource, uint32_t id); - /** - * release the seat object - * - * Using this request a client can tell the server that it is not - * going to use the seat object anymore. - * @since 5 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; @@ -4531,50 +1551,20 @@ struct wl_seat_interface { #define WL_SEAT_CAPABILITIES 0 #define WL_SEAT_NAME 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_CAPABILITIES_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_NAME_SINCE_VERSION 2 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_GET_POINTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_GET_KEYBOARD_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_GET_TOUCH_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_RELEASE_SINCE_VERSION 5 -/** - * @ingroup iface_wl_seat - * Sends an capabilities event to the client owning the resource. - * @param resource_ The client's resource - * @param capabilities capabilities of the seat - */ static inline void wl_seat_send_capabilities(struct wl_resource *resource_, uint32_t capabilities) { wl_resource_post_event(resource_, WL_SEAT_CAPABILITIES, capabilities); } -/** - * @ingroup iface_wl_seat - * Sends an name event to the client owning the resource. - * @param resource_ The client's resource - * @param name seat identifier - */ static inline void wl_seat_send_name(struct wl_resource *resource_, const char *name) { @@ -4584,22 +1574,12 @@ wl_seat_send_name(struct wl_resource *resource_, const char *name) #ifndef WL_POINTER_ERROR_ENUM #define WL_POINTER_ERROR_ENUM enum wl_pointer_error { - /** - * given wl_surface has another role - */ WL_POINTER_ERROR_ROLE = 0, }; -#endif /* WL_POINTER_ERROR_ENUM */ +#endif #ifndef WL_POINTER_ERROR_ENUM_IS_VALID #define WL_POINTER_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_pointer - * Validate a wl_pointer error value. - * - * @return true on success, false on error. - * @ref wl_pointer_error - */ static inline bool wl_pointer_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -4609,38 +1589,18 @@ wl_pointer_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_POINTER_ERROR_ENUM_IS_VALID */ +#endif #ifndef WL_POINTER_BUTTON_STATE_ENUM #define WL_POINTER_BUTTON_STATE_ENUM -/** - * @ingroup iface_wl_pointer - * physical button state - * - * Describes the physical state of a button that produced the button - * event. - */ enum wl_pointer_button_state { - /** - * the button is not pressed - */ WL_POINTER_BUTTON_STATE_RELEASED = 0, - /** - * the button is pressed - */ WL_POINTER_BUTTON_STATE_PRESSED = 1, }; -#endif /* WL_POINTER_BUTTON_STATE_ENUM */ +#endif #ifndef WL_POINTER_BUTTON_STATE_ENUM_IS_VALID #define WL_POINTER_BUTTON_STATE_ENUM_IS_VALID -/** - * @ingroup iface_wl_pointer - * Validate a wl_pointer button_state value. - * - * @return true on success, false on error. - * @ref wl_pointer_button_state - */ static inline bool wl_pointer_button_state_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -4652,37 +1612,18 @@ wl_pointer_button_state_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_POINTER_BUTTON_STATE_ENUM_IS_VALID */ +#endif #ifndef WL_POINTER_AXIS_ENUM #define WL_POINTER_AXIS_ENUM -/** - * @ingroup iface_wl_pointer - * axis types - * - * Describes the axis types of scroll events. - */ enum wl_pointer_axis { - /** - * vertical axis - */ WL_POINTER_AXIS_VERTICAL_SCROLL = 0, - /** - * horizontal axis - */ WL_POINTER_AXIS_HORIZONTAL_SCROLL = 1, }; -#endif /* WL_POINTER_AXIS_ENUM */ +#endif #ifndef WL_POINTER_AXIS_ENUM_IS_VALID #define WL_POINTER_AXIS_ENUM_IS_VALID -/** - * @ingroup iface_wl_pointer - * Validate a wl_pointer axis value. - * - * @return true on success, false on error. - * @ref wl_pointer_axis - */ static inline bool wl_pointer_axis_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -4694,65 +1635,21 @@ wl_pointer_axis_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_POINTER_AXIS_ENUM_IS_VALID */ +#endif #ifndef WL_POINTER_AXIS_SOURCE_ENUM #define WL_POINTER_AXIS_SOURCE_ENUM -/** - * @ingroup iface_wl_pointer - * axis source types - * - * Describes the source types for axis events. This indicates to the - * client how an axis event was physically generated; a client may - * adjust the user interface accordingly. For example, scroll events - * from a "finger" source may be in a smooth coordinate space with - * kinetic scrolling whereas a "wheel" source may be in discrete steps - * of a number of lines. - * - * The "continuous" axis source is a device generating events in a - * continuous coordinate space, but using something other than a - * finger. One example for this source is button-based scrolling where - * the vertical motion of a device is converted to scroll events while - * a button is held down. - * - * The "wheel tilt" axis source indicates that the actual device is a - * wheel but the scroll event is not caused by a rotation but a - * (usually sideways) tilt of the wheel. - */ enum wl_pointer_axis_source { - /** - * a physical wheel rotation - */ WL_POINTER_AXIS_SOURCE_WHEEL = 0, - /** - * finger on a touch surface - */ WL_POINTER_AXIS_SOURCE_FINGER = 1, - /** - * continuous coordinate space - */ WL_POINTER_AXIS_SOURCE_CONTINUOUS = 2, - /** - * a physical wheel tilt - * @since 6 - */ WL_POINTER_AXIS_SOURCE_WHEEL_TILT = 3, }; -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_SOURCE_WHEEL_TILT_SINCE_VERSION 6 -#endif /* WL_POINTER_AXIS_SOURCE_ENUM */ +#endif #ifndef WL_POINTER_AXIS_SOURCE_ENUM_IS_VALID #define WL_POINTER_AXIS_SOURCE_ENUM_IS_VALID -/** - * @ingroup iface_wl_pointer - * Validate a wl_pointer axis_source value. - * - * @return true on success, false on error. - * @ref wl_pointer_axis_source - */ static inline bool wl_pointer_axis_source_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -4768,38 +1665,18 @@ wl_pointer_axis_source_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_POINTER_AXIS_SOURCE_ENUM_IS_VALID */ +#endif #ifndef WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM #define WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM -/** - * @ingroup iface_wl_pointer - * axis relative direction - * - * This specifies the direction of the physical motion that caused a - * wl_pointer.axis event, relative to the wl_pointer.axis direction. - */ enum wl_pointer_axis_relative_direction { - /** - * physical motion matches axis direction - */ WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL = 0, - /** - * physical motion is the inverse of the axis direction - */ WL_POINTER_AXIS_RELATIVE_DIRECTION_INVERTED = 1, }; -#endif /* WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM */ +#endif #ifndef WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM_IS_VALID #define WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM_IS_VALID -/** - * @ingroup iface_wl_pointer - * Validate a wl_pointer axis_relative_direction value. - * - * @return true on success, false on error. - * @ref wl_pointer_axis_relative_direction - */ static inline bool wl_pointer_axis_relative_direction_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -4811,70 +1688,15 @@ wl_pointer_axis_relative_direction_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_pointer - * @struct wl_pointer_interface - */ struct wl_pointer_interface { - /** - * set the pointer surface - * - * Set the pointer surface, i.e., the surface that contains the - * pointer image (cursor). This request gives the surface the role - * of a cursor. If the surface already has another role, it raises - * a protocol error. - * - * The cursor actually changes only if the pointer focus for this - * device is one of the requesting client's surfaces or the surface - * parameter is the current pointer surface. If there was a - * previous surface set with this request it is replaced. If - * surface is NULL, the pointer image is hidden. - * - * The parameters hotspot_x and hotspot_y define the position of - * the pointer surface relative to the pointer location. Its - * top-left corner is always at (x, y) - (hotspot_x, hotspot_y), - * where (x, y) are the coordinates of the pointer location, in - * surface-local coordinates. - * - * On wl_surface.offset requests to the pointer surface, hotspot_x - * and hotspot_y are decremented by the x and y parameters passed - * to the request. The offset must be applied by wl_surface.commit - * as usual. - * - * The hotspot can also be updated by passing the currently set - * pointer surface to this request with new values for hotspot_x - * and hotspot_y. - * - * The input region is ignored for wl_surfaces with the role of a - * cursor. When the use as a cursor ends, the wl_surface is - * unmapped. - * - * The serial parameter must match the latest wl_pointer.enter - * serial number sent to the client. Otherwise the request will be - * ignored. - * @param serial serial number of the enter event - * @param surface pointer surface - * @param hotspot_x surface-local x coordinate - * @param hotspot_y surface-local y coordinate - */ void (*set_cursor)(struct wl_client *client, struct wl_resource *resource, uint32_t serial, struct wl_resource *surface, int32_t hotspot_x, int32_t hotspot_y); - /** - * release the pointer object - * - * Using this request a client can tell the server that it is not - * going to use the pointer object anymore. - * - * This request destroys the pointer proxy object, so clients must - * not call wl_pointer_destroy() after using this request. - * @since 3 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; @@ -4892,217 +1714,88 @@ struct wl_pointer_interface { #define WL_POINTER_AXIS_RELATIVE_DIRECTION 10 #define WL_POINTER_WARP 11 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_BUTTON_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_FRAME_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_SOURCE_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_STOP_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_DISCRETE_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_VALUE120_SINCE_VERSION 8 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_RELATIVE_DIRECTION_SINCE_VERSION 9 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_WARP_SINCE_VERSION 11 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_SET_CURSOR_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_RELEASE_SINCE_VERSION 3 -/** - * @ingroup iface_wl_pointer - * Sends an enter event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the enter event - * @param surface surface entered by the pointer - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - */ static inline void wl_pointer_send_enter(struct wl_resource *resource_, uint32_t serial, struct wl_resource *surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { wl_resource_post_event(resource_, WL_POINTER_ENTER, serial, surface, surface_x, surface_y); } -/** - * @ingroup iface_wl_pointer - * Sends an leave event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the leave event - * @param surface surface left by the pointer - */ static inline void wl_pointer_send_leave(struct wl_resource *resource_, uint32_t serial, struct wl_resource *surface) { wl_resource_post_event(resource_, WL_POINTER_LEAVE, serial, surface); } -/** - * @ingroup iface_wl_pointer - * Sends an motion event to the client owning the resource. - * @param resource_ The client's resource - * @param time timestamp with millisecond granularity - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - */ static inline void wl_pointer_send_motion(struct wl_resource *resource_, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { wl_resource_post_event(resource_, WL_POINTER_MOTION, time, surface_x, surface_y); } -/** - * @ingroup iface_wl_pointer - * Sends an button event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the button event - * @param time timestamp with millisecond granularity - * @param button button that produced the event - * @param state physical state of the button - */ static inline void wl_pointer_send_button(struct wl_resource *resource_, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) { wl_resource_post_event(resource_, WL_POINTER_BUTTON, serial, time, button, state); } -/** - * @ingroup iface_wl_pointer - * Sends an axis event to the client owning the resource. - * @param resource_ The client's resource - * @param time timestamp with millisecond granularity - * @param axis axis type - * @param value length of vector in surface-local coordinate space - */ static inline void wl_pointer_send_axis(struct wl_resource *resource_, uint32_t time, uint32_t axis, wl_fixed_t value) { wl_resource_post_event(resource_, WL_POINTER_AXIS, time, axis, value); } -/** - * @ingroup iface_wl_pointer - * Sends an frame event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_pointer_send_frame(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_POINTER_FRAME); } -/** - * @ingroup iface_wl_pointer - * Sends an axis_source event to the client owning the resource. - * @param resource_ The client's resource - * @param axis_source source of the axis event - */ static inline void wl_pointer_send_axis_source(struct wl_resource *resource_, uint32_t axis_source) { wl_resource_post_event(resource_, WL_POINTER_AXIS_SOURCE, axis_source); } -/** - * @ingroup iface_wl_pointer - * Sends an axis_stop event to the client owning the resource. - * @param resource_ The client's resource - * @param time timestamp with millisecond granularity - * @param axis the axis stopped with this event - */ static inline void wl_pointer_send_axis_stop(struct wl_resource *resource_, uint32_t time, uint32_t axis) { wl_resource_post_event(resource_, WL_POINTER_AXIS_STOP, time, axis); } -/** - * @ingroup iface_wl_pointer - * Sends an axis_discrete event to the client owning the resource. - * @param resource_ The client's resource - * @param axis axis type - * @param discrete number of steps - */ static inline void wl_pointer_send_axis_discrete(struct wl_resource *resource_, uint32_t axis, int32_t discrete) { wl_resource_post_event(resource_, WL_POINTER_AXIS_DISCRETE, axis, discrete); } -/** - * @ingroup iface_wl_pointer - * Sends an axis_value120 event to the client owning the resource. - * @param resource_ The client's resource - * @param axis axis type - * @param value120 scroll distance as fraction of 120 - */ static inline void wl_pointer_send_axis_value120(struct wl_resource *resource_, uint32_t axis, int32_t value120) { wl_resource_post_event(resource_, WL_POINTER_AXIS_VALUE120, axis, value120); } -/** - * @ingroup iface_wl_pointer - * Sends an axis_relative_direction event to the client owning the resource. - * @param resource_ The client's resource - * @param axis axis type - * @param direction physical direction relative to axis motion - */ static inline void wl_pointer_send_axis_relative_direction(struct wl_resource *resource_, uint32_t axis, uint32_t direction) { wl_resource_post_event(resource_, WL_POINTER_AXIS_RELATIVE_DIRECTION, axis, direction); } -/** - * @ingroup iface_wl_pointer - * Sends an warp event to the client owning the resource. - * @param resource_ The client's resource - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - */ static inline void wl_pointer_send_warp(struct wl_resource *resource_, wl_fixed_t surface_x, wl_fixed_t surface_y) { @@ -5111,34 +1804,14 @@ wl_pointer_send_warp(struct wl_resource *resource_, wl_fixed_t surface_x, wl_fix #ifndef WL_KEYBOARD_KEYMAP_FORMAT_ENUM #define WL_KEYBOARD_KEYMAP_FORMAT_ENUM -/** - * @ingroup iface_wl_keyboard - * keyboard mapping format - * - * This specifies the format of the keymap provided to the - * client with the wl_keyboard.keymap event. - */ enum wl_keyboard_keymap_format { - /** - * no keymap; client must understand how to interpret the raw keycode - */ WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP = 0, - /** - * libxkbcommon compatible, null-terminated string; to determine the xkb keycode, clients must add 8 to the key event keycode - */ WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 = 1, }; -#endif /* WL_KEYBOARD_KEYMAP_FORMAT_ENUM */ +#endif #ifndef WL_KEYBOARD_KEYMAP_FORMAT_ENUM_IS_VALID #define WL_KEYBOARD_KEYMAP_FORMAT_ENUM_IS_VALID -/** - * @ingroup iface_wl_keyboard - * Validate a wl_keyboard keymap_format value. - * - * @return true on success, false on error. - * @ref wl_keyboard_keymap_format - */ static inline bool wl_keyboard_keymap_format_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -5150,54 +1823,20 @@ wl_keyboard_keymap_format_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_KEYBOARD_KEYMAP_FORMAT_ENUM_IS_VALID */ +#endif #ifndef WL_KEYBOARD_KEY_STATE_ENUM #define WL_KEYBOARD_KEY_STATE_ENUM -/** - * @ingroup iface_wl_keyboard - * physical key state - * - * Describes the physical state of a key that produced the key event. - * - * Since version 10, the key can be in a "repeated" pseudo-state which - * means the same as "pressed", but is used to signal repetition in the - * key event. - * - * The key may only enter the repeated state after entering the pressed - * state and before entering the released state. This event may be - * generated multiple times while the key is down. - */ enum wl_keyboard_key_state { - /** - * key is not pressed - */ WL_KEYBOARD_KEY_STATE_RELEASED = 0, - /** - * key is pressed - */ WL_KEYBOARD_KEY_STATE_PRESSED = 1, - /** - * key was repeated - * @since 10 - */ WL_KEYBOARD_KEY_STATE_REPEATED = 2, }; -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_KEY_STATE_REPEATED_SINCE_VERSION 10 -#endif /* WL_KEYBOARD_KEY_STATE_ENUM */ +#endif #ifndef WL_KEYBOARD_KEY_STATE_ENUM_IS_VALID #define WL_KEYBOARD_KEY_STATE_ENUM_IS_VALID -/** - * @ingroup iface_wl_keyboard - * Validate a wl_keyboard key_state value. - * - * @return true on success, false on error. - * @ref wl_keyboard_key_state - */ static inline bool wl_keyboard_key_state_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -5211,19 +1850,9 @@ wl_keyboard_key_state_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_KEYBOARD_KEY_STATE_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_keyboard - * @struct wl_keyboard_interface - */ struct wl_keyboard_interface { - /** - * release the keyboard object - * - * - * @since 3 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; @@ -5235,132 +1864,52 @@ struct wl_keyboard_interface { #define WL_KEYBOARD_MODIFIERS 4 #define WL_KEYBOARD_REPEAT_INFO 5 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_KEYMAP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_KEY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_MODIFIERS_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_REPEAT_INFO_SINCE_VERSION 4 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_RELEASE_SINCE_VERSION 3 -/** - * @ingroup iface_wl_keyboard - * Sends an keymap event to the client owning the resource. - * @param resource_ The client's resource - * @param format keymap format - * @param fd keymap file descriptor - * @param size keymap size, in bytes - */ static inline void wl_keyboard_send_keymap(struct wl_resource *resource_, uint32_t format, int32_t fd, uint32_t size) { wl_resource_post_event(resource_, WL_KEYBOARD_KEYMAP, format, fd, size); } -/** - * @ingroup iface_wl_keyboard - * Sends an enter event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the enter event - * @param surface surface gaining keyboard focus - * @param keys the keys currently logically down - */ static inline void wl_keyboard_send_enter(struct wl_resource *resource_, uint32_t serial, struct wl_resource *surface, struct wl_array *keys) { wl_resource_post_event(resource_, WL_KEYBOARD_ENTER, serial, surface, keys); } -/** - * @ingroup iface_wl_keyboard - * Sends an leave event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the leave event - * @param surface surface that lost keyboard focus - */ static inline void wl_keyboard_send_leave(struct wl_resource *resource_, uint32_t serial, struct wl_resource *surface) { wl_resource_post_event(resource_, WL_KEYBOARD_LEAVE, serial, surface); } -/** - * @ingroup iface_wl_keyboard - * Sends an key event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the key event - * @param time timestamp with millisecond granularity - * @param key key that produced the event - * @param state physical state of the key - */ static inline void wl_keyboard_send_key(struct wl_resource *resource_, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { wl_resource_post_event(resource_, WL_KEYBOARD_KEY, serial, time, key, state); } -/** - * @ingroup iface_wl_keyboard - * Sends an modifiers event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the modifiers event - * @param mods_depressed depressed modifiers - * @param mods_latched latched modifiers - * @param mods_locked locked modifiers - * @param group keyboard layout - */ static inline void wl_keyboard_send_modifiers(struct wl_resource *resource_, uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { wl_resource_post_event(resource_, WL_KEYBOARD_MODIFIERS, serial, mods_depressed, mods_latched, mods_locked, group); } -/** - * @ingroup iface_wl_keyboard - * Sends an repeat_info event to the client owning the resource. - * @param resource_ The client's resource - * @param rate the rate of repeating keys in characters per second - * @param delay delay in milliseconds since key down until repeating starts - */ static inline void wl_keyboard_send_repeat_info(struct wl_resource *resource_, int32_t rate, int32_t delay) { wl_resource_post_event(resource_, WL_KEYBOARD_REPEAT_INFO, rate, delay); } -/** - * @ingroup iface_wl_touch - * @struct wl_touch_interface - */ struct wl_touch_interface { - /** - * release the touch object - * - * - * @since 3 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; @@ -5373,129 +1922,52 @@ struct wl_touch_interface { #define WL_TOUCH_SHAPE 5 #define WL_TOUCH_ORIENTATION 6 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_DOWN_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_UP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_FRAME_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_CANCEL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_SHAPE_SINCE_VERSION 6 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_ORIENTATION_SINCE_VERSION 6 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_RELEASE_SINCE_VERSION 3 -/** - * @ingroup iface_wl_touch - * Sends an down event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the touch down event - * @param time timestamp with millisecond granularity - * @param surface surface touched - * @param id the unique ID of this touch point - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ static inline void wl_touch_send_down(struct wl_resource *resource_, uint32_t serial, uint32_t time, struct wl_resource *surface, int32_t id, wl_fixed_t x, wl_fixed_t y) { wl_resource_post_event(resource_, WL_TOUCH_DOWN, serial, time, surface, id, x, y); } -/** - * @ingroup iface_wl_touch - * Sends an up event to the client owning the resource. - * @param resource_ The client's resource - * @param serial serial number of the touch up event - * @param time timestamp with millisecond granularity - * @param id the unique ID of this touch point - */ static inline void wl_touch_send_up(struct wl_resource *resource_, uint32_t serial, uint32_t time, int32_t id) { wl_resource_post_event(resource_, WL_TOUCH_UP, serial, time, id); } -/** - * @ingroup iface_wl_touch - * Sends an motion event to the client owning the resource. - * @param resource_ The client's resource - * @param time timestamp with millisecond granularity - * @param id the unique ID of this touch point - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ static inline void wl_touch_send_motion(struct wl_resource *resource_, uint32_t time, int32_t id, wl_fixed_t x, wl_fixed_t y) { wl_resource_post_event(resource_, WL_TOUCH_MOTION, time, id, x, y); } -/** - * @ingroup iface_wl_touch - * Sends an frame event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_touch_send_frame(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_TOUCH_FRAME); } -/** - * @ingroup iface_wl_touch - * Sends an cancel event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_touch_send_cancel(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_TOUCH_CANCEL); } -/** - * @ingroup iface_wl_touch - * Sends an shape event to the client owning the resource. - * @param resource_ The client's resource - * @param id the unique ID of this touch point - * @param major length of the major axis in surface-local coordinates - * @param minor length of the minor axis in surface-local coordinates - */ static inline void wl_touch_send_shape(struct wl_resource *resource_, int32_t id, wl_fixed_t major, wl_fixed_t minor) { wl_resource_post_event(resource_, WL_TOUCH_SHAPE, id, major, minor); } -/** - * @ingroup iface_wl_touch - * Sends an orientation event to the client owning the resource. - * @param resource_ The client's resource - * @param id the unique ID of this touch point - * @param orientation angle between major axis and positive surface y-axis in degrees - */ static inline void wl_touch_send_orientation(struct wl_resource *resource_, int32_t id, wl_fixed_t orientation) { @@ -5504,50 +1976,18 @@ wl_touch_send_orientation(struct wl_resource *resource_, int32_t id, wl_fixed_t #ifndef WL_OUTPUT_SUBPIXEL_ENUM #define WL_OUTPUT_SUBPIXEL_ENUM -/** - * @ingroup iface_wl_output - * subpixel geometry information - * - * This enumeration describes how the physical - * pixels on an output are laid out. - */ enum wl_output_subpixel { - /** - * unknown geometry - */ WL_OUTPUT_SUBPIXEL_UNKNOWN = 0, - /** - * no geometry - */ WL_OUTPUT_SUBPIXEL_NONE = 1, - /** - * horizontal RGB - */ WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB = 2, - /** - * horizontal BGR - */ WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR = 3, - /** - * vertical RGB - */ WL_OUTPUT_SUBPIXEL_VERTICAL_RGB = 4, - /** - * vertical BGR - */ WL_OUTPUT_SUBPIXEL_VERTICAL_BGR = 5, }; -#endif /* WL_OUTPUT_SUBPIXEL_ENUM */ +#endif #ifndef WL_OUTPUT_SUBPIXEL_ENUM_IS_VALID #define WL_OUTPUT_SUBPIXEL_ENUM_IS_VALID -/** - * @ingroup iface_wl_output - * Validate a wl_output subpixel value. - * - * @return true on success, false on error. - * @ref wl_output_subpixel - */ static inline bool wl_output_subpixel_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -5567,70 +2007,24 @@ wl_output_subpixel_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_OUTPUT_SUBPIXEL_ENUM_IS_VALID */ +#endif #ifndef WL_OUTPUT_TRANSFORM_ENUM #define WL_OUTPUT_TRANSFORM_ENUM -/** - * @ingroup iface_wl_output - * transformation applied to buffer contents - * - * This describes transformations that clients and compositors apply to - * buffer contents. - * - * The flipped values correspond to an initial flip around a - * vertical axis followed by rotation. - * - * The purpose is mainly to allow clients to render accordingly and - * tell the compositor, so that for fullscreen surfaces, the - * compositor will still be able to scan out directly from client - * surfaces. - */ enum wl_output_transform { - /** - * no transform - */ WL_OUTPUT_TRANSFORM_NORMAL = 0, - /** - * 90 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_90 = 1, - /** - * 180 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_180 = 2, - /** - * 270 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_270 = 3, - /** - * 180 degree flip around a vertical axis - */ WL_OUTPUT_TRANSFORM_FLIPPED = 4, - /** - * flip and rotate 90 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_FLIPPED_90 = 5, - /** - * flip and rotate 180 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_FLIPPED_180 = 6, - /** - * flip and rotate 270 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_FLIPPED_270 = 7, }; -#endif /* WL_OUTPUT_TRANSFORM_ENUM */ +#endif #ifndef WL_OUTPUT_TRANSFORM_ENUM_IS_VALID #define WL_OUTPUT_TRANSFORM_ENUM_IS_VALID -/** - * @ingroup iface_wl_output - * Validate a wl_output transform value. - * - * @return true on success, false on error. - * @ref wl_output_transform - */ static inline bool wl_output_transform_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -5654,38 +2048,18 @@ wl_output_transform_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_OUTPUT_TRANSFORM_ENUM_IS_VALID */ +#endif #ifndef WL_OUTPUT_MODE_ENUM #define WL_OUTPUT_MODE_ENUM -/** - * @ingroup iface_wl_output - * mode information - * - * These flags describe properties of an output mode. - * They are used in the flags bitfield of the mode event. - */ enum wl_output_mode { - /** - * indicates this is the current mode - */ WL_OUTPUT_MODE_CURRENT = 0x1, - /** - * indicates this is the preferred mode - */ WL_OUTPUT_MODE_PREFERRED = 0x2, }; -#endif /* WL_OUTPUT_MODE_ENUM */ +#endif #ifndef WL_OUTPUT_MODE_ENUM_IS_VALID #define WL_OUTPUT_MODE_ENUM_IS_VALID -/** - * @ingroup iface_wl_output - * Validate a wl_output mode value. - * - * @return true on success, false on error. - * @ref wl_output_mode - */ static inline bool wl_output_mode_is_valid(uint32_t value, uint32_t version) { uint32_t valid = 0; @@ -5695,20 +2069,9 @@ wl_output_mode_is_valid(uint32_t value, uint32_t version) { valid |= WL_OUTPUT_MODE_PREFERRED; return (value & ~valid) == 0; } -#endif /* WL_OUTPUT_MODE_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_output - * @struct wl_output_interface - */ struct wl_output_interface { - /** - * release the output object - * - * Using this request a client can tell the server that it is not - * going to use the output object anymore. - * @since 3 - */ void (*release)(struct wl_client *client, struct wl_resource *resource); }; @@ -5720,153 +2083,60 @@ struct wl_output_interface { #define WL_OUTPUT_NAME 4 #define WL_OUTPUT_DESCRIPTION 5 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_GEOMETRY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_MODE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_DONE_SINCE_VERSION 2 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_SCALE_SINCE_VERSION 2 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_NAME_SINCE_VERSION 4 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_DESCRIPTION_SINCE_VERSION 4 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_RELEASE_SINCE_VERSION 3 -/** - * @ingroup iface_wl_output - * Sends an geometry event to the client owning the resource. - * @param resource_ The client's resource - * @param x x position within the global compositor space - * @param y y position within the global compositor space - * @param physical_width width in millimeters of the output - * @param physical_height height in millimeters of the output - * @param subpixel subpixel orientation of the output - * @param make textual description of the manufacturer - * @param model textual description of the model - * @param transform additional transformation applied to buffer contents during presentation - */ static inline void wl_output_send_geometry(struct wl_resource *resource_, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char *make, const char *model, int32_t transform) { wl_resource_post_event(resource_, WL_OUTPUT_GEOMETRY, x, y, physical_width, physical_height, subpixel, make, model, transform); } -/** - * @ingroup iface_wl_output - * Sends an mode event to the client owning the resource. - * @param resource_ The client's resource - * @param flags bitfield of mode flags - * @param width width of the mode in hardware units - * @param height height of the mode in hardware units - * @param refresh vertical refresh rate in mHz - */ static inline void wl_output_send_mode(struct wl_resource *resource_, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { wl_resource_post_event(resource_, WL_OUTPUT_MODE, flags, width, height, refresh); } -/** - * @ingroup iface_wl_output - * Sends an done event to the client owning the resource. - * @param resource_ The client's resource - */ static inline void wl_output_send_done(struct wl_resource *resource_) { wl_resource_post_event(resource_, WL_OUTPUT_DONE); } -/** - * @ingroup iface_wl_output - * Sends an scale event to the client owning the resource. - * @param resource_ The client's resource - * @param factor scaling factor of output - */ static inline void wl_output_send_scale(struct wl_resource *resource_, int32_t factor) { wl_resource_post_event(resource_, WL_OUTPUT_SCALE, factor); } -/** - * @ingroup iface_wl_output - * Sends an name event to the client owning the resource. - * @param resource_ The client's resource - * @param name output name - */ static inline void wl_output_send_name(struct wl_resource *resource_, const char *name) { wl_resource_post_event(resource_, WL_OUTPUT_NAME, name); } -/** - * @ingroup iface_wl_output - * Sends an description event to the client owning the resource. - * @param resource_ The client's resource - * @param description output description - */ static inline void wl_output_send_description(struct wl_resource *resource_, const char *description) { wl_resource_post_event(resource_, WL_OUTPUT_DESCRIPTION, description); } -/** - * @ingroup iface_wl_region - * @struct wl_region_interface - */ struct wl_region_interface { - /** - * destroy region - * - * Destroy the region. This will invalidate the object ID. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * add rectangle to region - * - * Add the specified rectangle to the region. - * @param x region-local x coordinate - * @param y region-local y coordinate - * @param width rectangle width - * @param height rectangle height - */ void (*add)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y, int32_t width, int32_t height); - /** - * subtract rectangle from region - * - * Subtract the specified rectangle from the region. - * @param x region-local x coordinate - * @param y region-local y coordinate - * @param width rectangle width - * @param height rectangle height - */ void (*subtract)(struct wl_client *client, struct wl_resource *resource, int32_t x, @@ -5876,42 +2146,20 @@ struct wl_region_interface { }; -/** - * @ingroup iface_wl_region - */ #define WL_REGION_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_region - */ #define WL_REGION_ADD_SINCE_VERSION 1 -/** - * @ingroup iface_wl_region - */ #define WL_REGION_SUBTRACT_SINCE_VERSION 1 #ifndef WL_SUBCOMPOSITOR_ERROR_ENUM #define WL_SUBCOMPOSITOR_ERROR_ENUM enum wl_subcompositor_error { - /** - * the to-be sub-surface is invalid - */ WL_SUBCOMPOSITOR_ERROR_BAD_SURFACE = 0, - /** - * the to-be sub-surface parent is invalid - */ WL_SUBCOMPOSITOR_ERROR_BAD_PARENT = 1, }; -#endif /* WL_SUBCOMPOSITOR_ERROR_ENUM */ +#endif #ifndef WL_SUBCOMPOSITOR_ERROR_ENUM_IS_VALID #define WL_SUBCOMPOSITOR_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_subcompositor - * Validate a wl_subcompositor error value. - * - * @return true on success, false on error. - * @ref wl_subcompositor_error - */ static inline bool wl_subcompositor_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -5923,49 +2171,11 @@ wl_subcompositor_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SUBCOMPOSITOR_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_subcompositor - * @struct wl_subcompositor_interface - */ struct wl_subcompositor_interface { - /** - * unbind from the subcompositor interface - * - * Informs the server that the client will not be using this - * protocol object anymore. This does not affect any other objects, - * wl_subsurface objects included. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * give a surface the role sub-surface - * - * Create a sub-surface interface for the given surface, and - * associate it with the given parent surface. This turns a plain - * wl_surface into a sub-surface. - * - * The to-be sub-surface must not already have another role, and it - * must not have an existing wl_subsurface object. Otherwise the - * bad_surface protocol error is raised. - * - * Adding sub-surfaces to a parent is a double-buffered operation - * on the parent (see wl_surface.commit). The effect of adding a - * sub-surface becomes visible on the next time the state of the - * parent surface is applied. - * - * The parent surface must not be one of the child surface's - * descendants, and the parent must be different from the child - * surface, otherwise the bad_parent protocol error is raised. - * - * This request modifies the behaviour of wl_surface.commit request - * on the sub-surface, see the documentation on wl_subsurface - * interface. - * @param id the new sub-surface object ID - * @param surface the surface to be turned into a sub-surface - * @param parent the parent surface - */ void (*get_subsurface)(struct wl_client *client, struct wl_resource *resource, uint32_t id, @@ -5974,34 +2184,18 @@ struct wl_subcompositor_interface { }; -/** - * @ingroup iface_wl_subcompositor - */ #define WL_SUBCOMPOSITOR_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subcompositor - */ #define WL_SUBCOMPOSITOR_GET_SUBSURFACE_SINCE_VERSION 1 #ifndef WL_SUBSURFACE_ERROR_ENUM #define WL_SUBSURFACE_ERROR_ENUM enum wl_subsurface_error { - /** - * wl_surface is not a sibling or the parent - */ WL_SUBSURFACE_ERROR_BAD_SURFACE = 0, }; -#endif /* WL_SUBSURFACE_ERROR_ENUM */ +#endif #ifndef WL_SUBSURFACE_ERROR_ENUM_IS_VALID #define WL_SUBSURFACE_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_subsurface - * Validate a wl_subsurface error value. - * - * @return true on success, false on error. - * @ref wl_subsurface_error - */ static inline bool wl_subsurface_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -6011,149 +2205,44 @@ wl_subsurface_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_SUBSURFACE_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_subsurface - * @struct wl_subsurface_interface - */ struct wl_subsurface_interface { - /** - * remove sub-surface interface - * - * The sub-surface interface is removed from the wl_surface - * object that was turned into a sub-surface with a - * wl_subcompositor.get_subsurface request. The wl_surface's - * association to the parent is deleted. The wl_surface is unmapped - * immediately. - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * reposition the sub-surface - * - * This sets the position of the sub-surface, relative to the - * parent surface. - * - * The sub-surface will be moved so that its origin (top left - * corner pixel) will be at the location x, y of the parent surface - * coordinate system. The coordinates are not restricted to the - * parent surface area. Negative values are allowed. - * - * The initial position is 0, 0. - * - * Position is double-buffered state on the parent surface, see - * wl_subsurface and wl_surface.commit for more information. - * @param x x coordinate in the parent surface - * @param y y coordinate in the parent surface - */ void (*set_position)(struct wl_client *client, struct wl_resource *resource, int32_t x, int32_t y); - /** - * restack the sub-surface - * - * This sub-surface is taken from the stack, and put back just - * above the reference surface, changing the z-order of the - * sub-surfaces. The reference surface must be one of the sibling - * surfaces, or the parent surface. Using any other surface, - * including this sub-surface, will cause a protocol error. - * - * A new sub-surface is initially added as the top-most in the - * stack of its siblings and parent. - * - * Z-order is double-buffered state on the parent surface, see - * wl_subsurface and wl_surface.commit for more information. - * @param sibling the reference surface - */ void (*place_above)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *sibling); - /** - * restack the sub-surface - * - * The sub-surface is placed just below the reference surface. - * - * See wl_subsurface.place_above. - * @param sibling the reference surface - */ void (*place_below)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *sibling); - /** - * set sub-surface to synchronized mode - * - * Change the commit behaviour of the sub-surface to synchronized - * mode. - * - * See wl_subsurface and wl_surface.commit for more information. - */ void (*set_sync)(struct wl_client *client, struct wl_resource *resource); - /** - * set sub-surface to desynchronized mode - * - * Change the commit behaviour of the sub-surface to - * desynchronized mode. - * - * See wl_subsurface and wl_surface.commit for more information. - */ void (*set_desync)(struct wl_client *client, struct wl_resource *resource); }; -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_SET_POSITION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_PLACE_ABOVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_PLACE_BELOW_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_SET_SYNC_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_SET_DESYNC_SINCE_VERSION 1 #ifndef WL_FIXES_ERROR_ENUM #define WL_FIXES_ERROR_ENUM -/** - * @ingroup iface_wl_fixes - * wl_fixes error values - * - * These errors can be emitted in response to wl_fixes requests. - */ enum wl_fixes_error { - /** - * unknown global or the global is not removed - */ WL_FIXES_ERROR_INVALID_ACK_REMOVE = 0, }; -#endif /* WL_FIXES_ERROR_ENUM */ +#endif #ifndef WL_FIXES_ERROR_ENUM_IS_VALID #define WL_FIXES_ERROR_ENUM_IS_VALID -/** - * @ingroup iface_wl_fixes - * Validate a wl_fixes error value. - * - * @return true on success, false on error. - * @ref wl_fixes_error - */ static inline bool wl_fixes_error_is_valid(uint32_t value, uint32_t version) { switch (value) { @@ -6163,68 +2252,14 @@ wl_fixes_error_is_valid(uint32_t value, uint32_t version) { return false; } } -#endif /* WL_FIXES_ERROR_ENUM_IS_VALID */ +#endif -/** - * @ingroup iface_wl_fixes - * @struct wl_fixes_interface - */ struct wl_fixes_interface { - /** - * destroys this object - * - * - */ void (*destroy)(struct wl_client *client, struct wl_resource *resource); - /** - * destroy a wl_registry - * - * This request destroys a wl_registry object. - * - * The client should no longer use the wl_registry after making - * this request. - * - * The compositor will emit a wl_display.delete_id event with the - * object ID of the registry and will no longer emit any events on - * the registry. The client should re-use the object ID once it - * receives the wl_display.delete_id event. - * @param registry the registry to destroy - */ void (*destroy_registry)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *registry); - /** - * acknowledge global removal - * - * Acknowledge the removal of the specified global. - * - * If no global with the specified name exists or the global is not - * removed, the wl_fixes.invalid_ack_remove protocol error will be - * posted. - * - * Due to the Wayland protocol being asynchronous, the wl_global - * objects cannot be destroyed immediately. For example, if a - * wl_global is removed and a client attempts to bind that global - * around same time, it can result in a protocol error due to an - * unknown global name in the bind request. - * - * In order to avoid crashing clients, the compositor should remove - * the wl_global once it is guaranteed that no more bind requests - * will come. - * - * The wl_fixes.ack_global_remove() request is used to signal to - * the compositor that the client will not bind the given global - * anymore. After all clients acknowledge the removal of the - * global, the compositor can safely destroy it. - * - * The client must call the wl_fixes.ack_global_remove() request in - * response to a wl_registry.global_remove() event even if it did - * not bind the corresponding global. - * @param registry the registry object - * @param name unique name of the global - * @since 2 - */ void (*ack_global_remove)(struct wl_client *client, struct wl_resource *resource, struct wl_resource *registry, @@ -6232,17 +2267,8 @@ struct wl_fixes_interface { }; -/** - * @ingroup iface_wl_fixes - */ #define WL_FIXES_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_fixes - */ #define WL_FIXES_DESTROY_REGISTRY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_fixes - */ #define WL_FIXES_ACK_GLOBAL_REMOVE_SINCE_VERSION 2 #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server.h b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server.h index 48fab1dda..6a45c5b58 100644 --- a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server.h +++ b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-server.h @@ -23,15 +23,6 @@ * SOFTWARE. */ -/** \file - * - * \brief Include the server API, deprecations and protocol C API. - * - * \warning Use of this header file is discouraged. Prefer including - * wayland-server-core.h instead, which does not include the - * server protocol header and as such only defines the library - * API, excluding the deprecated API below. - */ #ifndef WAYLAND_SERVER_H #define WAYLAND_SERVER_H @@ -43,16 +34,6 @@ extern "C" { #endif -/* - * The user can set this macro to hide the wl_object, wl_resource and wl_buffer - * objects alongside the associated API. - * - * The structs were meant to be opaque, although we missed that in the early days. - * - * NOTE: the list of structs, functions, etc in this section MUST NEVER GROW. - * Otherwise we will break forward compatibility and applications that used to - * build fine will no longer be able to do so. - */ #ifndef WL_HIDE_DEPRECATED struct wl_object { diff --git a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-util.h b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-util.h index 98c72fded..d411ddd4f 100644 --- a/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-util.h +++ b/app/src/main/cpp/waylandcomp/prebuilt/include/wayland-util.h @@ -23,10 +23,6 @@ * SOFTWARE. */ -/** \file wayland-util.h - * - * \brief Utility classes, functions, and macros. - */ #ifndef WAYLAND_UTIL_H #define WAYLAND_UTIL_H @@ -40,14 +36,12 @@ extern "C" { #endif -/** Visibility attribute */ #if defined(__GNUC__) && __GNUC__ >= 4 #define WL_EXPORT __attribute__ ((visibility("default"))) #else #define WL_EXPORT #endif -/** Deprecated attribute */ #if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || (defined(__cplusplus) && __cplusplus >= 201402L) #define WL_DEPRECATED [[deprecated]] #elif defined(__GNUC__) && __GNUC__ >= 4 @@ -56,14 +50,6 @@ extern "C" { #define WL_DEPRECATED #endif -/** - * Printf-style argument attribute - * - * \param x Ordinality of the format string argument - * \param y Ordinality of the argument to check against the format string - * - * \sa https://gcc.gnu.org/onlinedocs/gcc-3.2.1/gcc/Function-Attributes.html - */ #if defined(__GNUC__) && __GNUC__ >= 4 #define WL_PRINTF(x, y) __attribute__((__format__(__printf__, x, y))) #else @@ -76,405 +62,57 @@ extern "C" { #define WL_TYPEOF(expr) __typeof__(expr) #endif -/** \class wl_object - * - * \brief A protocol object. - * - * A `wl_object` is an opaque struct identifying the protocol object - * underlying a `wl_proxy` or `wl_resource`. - * - * \note Functions accessing a `wl_object` are not normally used by client code. - * Clients should normally use the higher level interface generated by the - * scanner to interact with compositor objects. - * - */ struct wl_object; -/** - * The maximum size of a protocol message. - * - * If a message size exceeds this value, the connection will be dropped. - * Servers will send an invalid_method error before disconnecting. - */ #define WL_MAX_MESSAGE_SIZE 4096 -/** - * Protocol message signature - * - * A wl_message describes the signature of an actual protocol message, such as a - * request or event, that adheres to the Wayland protocol wire format. The - * protocol implementation uses a wl_message within its demarshal machinery for - * decoding messages between a compositor and its clients. In a sense, a - * wl_message is to a protocol message like a class is to an object. - * - * The `name` of a wl_message is the name of the corresponding protocol message. - * - * The `signature` is an ordered list of symbols representing the data types - * of message arguments and, optionally, a protocol version and indicators for - * nullability. A leading integer in the `signature` indicates the _since_ - * version of the protocol message. A `?` preceding a data type symbol indicates - * that the following argument type is nullable. While it is a protocol violation - * to send messages with non-nullable arguments set to `NULL`, event handlers in - * clients might still get called with non-nullable object arguments set to - * `NULL`. This can happen when the client destroyed the object being used as - * argument on its side and an event referencing that object was sent before the - * server knew about its destruction. As this race cannot be prevented, clients - * should - as a general rule - program their event handlers such that they can - * handle object arguments declared non-nullable being `NULL` gracefully. - * - * When no arguments accompany a message, `signature` is an empty string. - * - * Symbols: - * - * * `i`: int - * * `u`: uint - * * `f`: fixed - * * `s`: string - * * `o`: object - * * `n`: new_id - * * `a`: array - * * `h`: fd - * * `?`: following argument (`o` or `s`) is nullable - * - * While demarshaling primitive arguments is straightforward, when demarshaling - * messages containing `object` or `new_id` arguments, the protocol - * implementation often must determine the type of the object. The `types` of a - * wl_message is an array of wl_interface references that correspond to `o` and - * `n` arguments in `signature`, with `NULL` placeholders for arguments with - * non-object types. - * - * Consider the protocol event wl_display `delete_id` that has a single `uint` - * argument. The wl_message is: - * - * \code - * { "delete_id", "u", [NULL] } - * \endcode - * - * Here, the message `name` is `"delete_id"`, the `signature` is `"u"`, and the - * argument `types` is `[NULL]`, indicating that the `uint` argument has no - * corresponding wl_interface since it is a primitive argument. - * - * In contrast, consider a `wl_foo` interface supporting protocol request `bar` - * that has existed since version 2, and has two arguments: a `uint` and an - * object of type `wl_baz_interface` that may be `NULL`. Such a `wl_message` - * might be: - * - * \code - * { "bar", "2u?o", [NULL, &wl_baz_interface] } - * \endcode - * - * Here, the message `name` is `"bar"`, and the `signature` is `"2u?o"`. Notice - * how the `2` indicates the protocol version, the `u` indicates the first - * argument type is `uint`, and the `?o` indicates that the second argument - * is an object that may be `NULL`. Lastly, the argument `types` array indicates - * that no wl_interface corresponds to the first argument, while the type - * `wl_baz_interface` corresponds to the second argument. - * - * \sa wl_argument - * \sa wl_interface - * \sa Wire Format - */ struct wl_message { - /** Message name */ const char *name; - /** Message signature */ const char *signature; - /** Object argument interfaces */ const struct wl_interface **types; }; -/** - * Protocol object interface - * - * A wl_interface describes the API of a protocol object defined in the Wayland - * protocol specification. The protocol implementation uses a wl_interface - * within its marshalling machinery for encoding client requests. - * - * The `name` of a wl_interface is the name of the corresponding protocol - * interface, and `version` represents the version of the interface. The members - * `method_count` and `event_count` represent the number of `methods` (requests) - * and `events` in the respective wl_message members. - * - * For example, consider a protocol interface `foo`, marked as version `1`, with - * two requests and one event. - * - * \code{.xml} - * - * - * - * - * - * \endcode - * - * Given two wl_message arrays `foo_requests` and `foo_events`, a wl_interface - * for `foo` might be: - * - * \code - * struct wl_interface foo_interface = { - * "foo", 1, - * 2, foo_requests, - * 1, foo_events - * }; - * \endcode - * - * \note The server side of the protocol may define interface implementation - * types that incorporate the term `interface` in their name. Take - * care to not confuse these server-side `struct`s with a wl_interface - * variable whose name also ends in `interface`. For example, while the - * server may define a type `struct wl_foo_interface`, the client may - * define a `struct wl_interface wl_foo_interface`. - * - * \sa wl_message - * \sa wl_proxy - * \sa Interfaces - * \sa Versioning - */ struct wl_interface { - /** Interface name */ const char *name; - /** Interface version */ int version; - /** Number of methods (requests) */ int method_count; - /** Method (request) signatures */ const struct wl_message *methods; - /** Number of events */ int event_count; - /** Event signatures */ const struct wl_message *events; }; -/** \class wl_list - * - * \brief Doubly-linked list - * - * On its own, an instance of `struct wl_list` represents the sentinel head of - * a doubly-linked list, and must be initialized using wl_list_init(). - * When empty, the list head's `next` and `prev` members point to the list head - * itself, otherwise `next` references the first element in the list, and `prev` - * refers to the last element in the list. - * - * Use the `struct wl_list` type to represent both the list head and the links - * between elements within the list. Use wl_list_empty() to determine if the - * list is empty in O(1). - * - * All elements in the list must be of the same type. The element type must have - * a `struct wl_list` member, often named `link` by convention. Prior to - * insertion, there is no need to initialize an element's `link` - invoking - * wl_list_init() on an individual list element's `struct wl_list` member is - * unnecessary if the very next operation is wl_list_insert(). However, a - * common idiom is to initialize an element's `link` prior to removal - ensure - * safety by invoking wl_list_init() before wl_list_remove(). - * - * Consider a list reference `struct wl_list foo_list`, an element type as - * `struct element`, and an element's link member as `struct wl_list link`. - * - * The following code initializes a list and adds three elements to it. - * - * \code - * struct wl_list foo_list; - * - * struct element { - * int foo; - * struct wl_list link; - * }; - * struct element e1, e2, e3; - * - * wl_list_init(&foo_list); - * wl_list_insert(&foo_list, &e1.link); // e1 is the first element - * wl_list_insert(&foo_list, &e2.link); // e2 is now the first element - * wl_list_insert(&e2.link, &e3.link); // insert e3 after e2 - * \endcode - * - * The list now looks like [e2, e3, e1]. - * - * The `wl_list` API provides some iterator macros. For example, to iterate - * a list in ascending order: - * - * \code - * struct element *e; - * wl_list_for_each(e, foo_list, link) { - * do_something_with_element(e); - * } - * \endcode - * - * See the documentation of each iterator for details. - * \sa http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/include/linux/list.h - */ struct wl_list { - /** Previous list element */ struct wl_list *prev; - /** Next list element */ struct wl_list *next; }; -/** - * Initializes the list. - * - * \param list List to initialize - * - * \memberof wl_list - */ void wl_list_init(struct wl_list *list); -/** - * Inserts an element into the list, after the element represented by \p list. - * When \p list is a reference to the list itself (the head), set the containing - * struct of \p elm as the first element in the list. - * - * \note If \p elm is already part of a list, inserting it again will lead to - * list corruption. - * - * \param list List element after which the new element is inserted - * \param elm Link of the containing struct to insert into the list - * - * \memberof wl_list - */ void wl_list_insert(struct wl_list *list, struct wl_list *elm); -/** - * Removes an element from the list. - * - * \note This operation leaves \p elm in an invalid state. - * - * \param elm Link of the containing struct to remove from the list - * - * \memberof wl_list - */ void wl_list_remove(struct wl_list *elm); -/** - * Determines the length of the list. - * - * \note This is an O(n) operation. - * - * \param list List whose length is to be determined - * - * \return Number of elements in the list - * - * \memberof wl_list - */ int wl_list_length(const struct wl_list *list); -/** - * Determines if the list is empty. - * - * \param list List whose emptiness is to be determined - * - * \return 1 if empty, or 0 if not empty - * - * \memberof wl_list - */ int wl_list_empty(const struct wl_list *list); -/** - * Inserts all of the elements of one list into another, after the element - * represented by \p list. - * - * \note This leaves \p other in an invalid state. - * - * \param list List element after which the other list elements will be inserted - * \param other List of elements to insert - * - * \memberof wl_list - */ void wl_list_insert_list(struct wl_list *list, struct wl_list *other); -/** - * Retrieves a pointer to a containing struct, given a member name. - * - * This macro allows "conversion" from a pointer to a member to its containing - * struct. This is useful if you have a contained item like a wl_list, - * wl_listener, or wl_signal, provided via a callback or other means, and would - * like to retrieve the struct that contains it. - * - * To demonstrate, the following example retrieves a pointer to - * `example_container` given only its `destroy_listener` member: - * - * \code - * struct example_container { - * struct wl_listener destroy_listener; - * // other members... - * }; - * - * void example_container_destroy(struct wl_listener *listener, void *data) - * { - * struct example_container *ctr; - * - * ctr = wl_container_of(listener, ctr, destroy_listener); - * // destroy ctr... - * } - * \endcode - * - * \note `sample` need not be a valid pointer. A null or uninitialised pointer - * is sufficient. - * - * \param ptr Valid pointer to the contained member - * \param sample Pointer to a struct whose type contains \p ptr - * \param member Named location of \p ptr within the \p sample type - * - * \return The container for the specified pointer - */ #define wl_container_of(ptr, sample, member) \ (WL_TYPEOF(sample))((char *)(ptr) - \ offsetof(WL_TYPEOF(*sample), member)) -/** - * Iterates over a list. - * - * This macro expresses a for-each iterator for wl_list. Given a list and - * wl_list link member name (often named `link` by convention), this macro - * assigns each element in the list to \p pos, which can then be referenced in - * a trailing code block. For example, given a wl_list of `struct message` - * elements: - * - * \code - * struct message { - * char *contents; - * wl_list link; - * }; - * - * struct wl_list *message_list; - * // Assume message_list now "contains" many messages - * - * struct message *m; - * wl_list_for_each(m, message_list, link) { - * do_something_with_message(m); - * } - * \endcode - * - * \param pos Cursor that each list element will be assigned to - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each(pos, head, member) \ for (pos = wl_container_of((head)->next, pos, member); \ &pos->member != (head); \ pos = wl_container_of(pos->member.next, pos, member)) -/** - * Iterates over a list, safe against removal of the list element. - * - * \note Only removal of the current element, \p pos, is safe. Removing - * any other element during traversal may lead to a loop malfunction. - * - * \sa wl_list_for_each() - * - * \param pos Cursor that each list element will be assigned to - * \param tmp Temporary pointer of the same type as \p pos - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each_safe(pos, tmp, head, member) \ for (pos = wl_container_of((head)->next, pos, member), \ tmp = wl_container_of((pos)->member.next, tmp, member); \ @@ -482,37 +120,11 @@ wl_list_insert_list(struct wl_list *list, struct wl_list *other); pos = tmp, \ tmp = wl_container_of(pos->member.next, tmp, member)) -/** - * Iterates backwards over a list. - * - * \sa wl_list_for_each() - * - * \param pos Cursor that each list element will be assigned to - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each_reverse(pos, head, member) \ for (pos = wl_container_of((head)->prev, pos, member); \ &pos->member != (head); \ pos = wl_container_of(pos->member.prev, pos, member)) -/** - * Iterates backwards over a list, safe against removal of the list element. - * - * \note Only removal of the current element, \p pos, is safe. Removing - * any other element during traversal may lead to a loop malfunction. - * - * \sa wl_list_for_each() - * - * \param pos Cursor that each list element will be assigned to - * \param tmp Temporary pointer of the same type as \p pos - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each_reverse_safe(pos, tmp, head, member) \ for (pos = wl_container_of((head)->prev, pos, member), \ tmp = wl_container_of((pos)->member.prev, tmp, member); \ @@ -520,244 +132,75 @@ wl_list_insert_list(struct wl_list *list, struct wl_list *other); pos = tmp, \ tmp = wl_container_of(pos->member.prev, tmp, member)) -/** - * \class wl_array - * - * Dynamic array - * - * A wl_array is a dynamic array that can only grow until released. It is - * intended for relatively small allocations whose size is variable or not known - * in advance. While construction of a wl_array does not require all elements to - * be of the same size, wl_array_for_each() does require all elements to have - * the same type and size. - * - */ struct wl_array { - /** Array size */ size_t size; - /** Allocated space */ size_t alloc; - /** Array data */ void *data; }; -/** - * Initializes the array. - * - * \param array Array to initialize - * - * \memberof wl_array - */ void wl_array_init(struct wl_array *array); -/** - * Releases the array data. - * - * \note Leaves the array in an invalid state. - * - * \param array Array whose data is to be released - * - * \memberof wl_array - */ void wl_array_release(struct wl_array *array); -/** - * Increases the size of the array by \p size bytes. - * - * \param array Array whose size is to be increased - * \param size Number of bytes to increase the size of the array by - * - * \return A pointer to the beginning of the newly appended space, or NULL when - * resizing fails. - * - * \memberof wl_array - */ void * wl_array_add(struct wl_array *array, size_t size); -/** - * Copies the contents of \p source to \p array. - * - * \param array Destination array to copy to - * \param source Source array to copy from - * - * \return 0 on success, or -1 on failure - * - * \memberof wl_array - */ int wl_array_copy(struct wl_array *array, struct wl_array *source); -/** - * Iterates over an array. - * - * This macro expresses a for-each iterator for wl_array. It assigns each - * element in the array to \p pos, which can then be referenced in a trailing - * code block. \p pos must be a pointer to the array element type, and all - * array elements must be of the same type and size. - * - * \param pos Cursor that each array element will be assigned to - * \param array Array to iterate over - * - * \relates wl_array - * \sa wl_list_for_each() - */ #define wl_array_for_each(pos, array) \ for (pos = (array)->data; \ (array)->size != 0 && \ (const char *) pos < ((const char *) (array)->data + (array)->size); \ (pos)++) -/** - * Fixed-point number - * - * A `wl_fixed_t` is a 24.8 signed fixed-point number with a sign bit, 23 bits - * of integer precision and 8 bits of decimal precision. Consider `wl_fixed_t` - * as an opaque struct with methods that facilitate conversion to and from - * `double` and `int` types. - */ typedef int32_t wl_fixed_t; -/** - * Converts a fixed-point number to a floating-point number. - * - * \param f Fixed-point number to convert - * - * \return Floating-point representation of the fixed-point argument - */ static inline double wl_fixed_to_double(wl_fixed_t f) { return f / 256.0; } -/** - * Converts a floating-point number to a fixed-point number. - * - * \param d Floating-point number to convert - * - * \return Fixed-point representation of the floating-point argument - */ static inline wl_fixed_t wl_fixed_from_double(double d) { return (wl_fixed_t) (round(d * 256.0)); } -/** - * Converts a fixed-point number to an integer. - * - * \param f Fixed-point number to convert - * - * \return Integer component of the fixed-point argument - */ static inline int wl_fixed_to_int(wl_fixed_t f) { return f / 256; } -/** - * Converts an integer to a fixed-point number. - * - * \param i Integer to convert - * - * \return Fixed-point representation of the integer argument - */ static inline wl_fixed_t wl_fixed_from_int(int i) { return i * 256; } -/** - * Protocol message argument data types - * - * This union represents all of the argument types in the Wayland protocol wire - * format. The protocol implementation uses wl_argument within its marshalling - * machinery for dispatching messages between a client and a compositor. - * - * \sa wl_message - * \sa wl_interface - * \sa Wire Format - */ union wl_argument { - int32_t i; /**< `int` */ - uint32_t u; /**< `uint` */ - wl_fixed_t f; /**< `fixed` */ - const char *s; /**< `string` */ - struct wl_object *o; /**< `object` */ - uint32_t n; /**< `new_id` */ - struct wl_array *a; /**< `array` */ - int32_t h; /**< `fd` */ + int32_t i; + uint32_t u; + wl_fixed_t f; + const char *s; + struct wl_object *o; + uint32_t n; + struct wl_array *a; + int32_t h; }; -/** - * Dispatcher function type alias - * - * A dispatcher is a function that handles the emitting of callbacks in client - * code. For programs directly using the C library, this is done by using - * libffi to call function pointers. When binding to languages other than C, - * dispatchers provide a way to abstract the function calling process to be - * friendlier to other function calling systems. - * - * A dispatcher takes five arguments: The first is the dispatcher-specific - * implementation associated with the target object. The second is the object - * upon which the callback is being invoked (either wl_proxy or wl_resource). - * The third and fourth arguments are the opcode and the wl_message - * corresponding to the callback. The final argument is an array of arguments - * received from the other process via the wire protocol. - * - * \param user_data Dispatcher-specific implementation data - * \param target Callback invocation target (wl_proxy or `wl_resource`) - * \param opcode Callback opcode - * \param msg Callback message signature - * \param args Array of received arguments - * - * \return 0 on success, or -1 on failure - */ typedef int (*wl_dispatcher_func_t)(const void *user_data, void *target, uint32_t opcode, const struct wl_message *msg, union wl_argument *args); -/** - * Log function type alias - * - * The C implementation of the Wayland protocol abstracts the details of - * logging. Users may customize the logging behavior, with a function conforming - * to the `wl_log_func_t` type, via `wl_log_set_handler_client` and - * `wl_log_set_handler_server`. - * - * A `wl_log_func_t` must conform to the expectations of `vprintf`, and - * expects two arguments: a string to write and a corresponding variable - * argument list. While the string to write may contain format specifiers and - * use values in the variable argument list, the behavior of any `wl_log_func_t` - * depends on the implementation. - * - * \note Take care to not confuse this with `wl_protocol_logger_func_t`, which - * is a specific server-side logger for requests and events. - * - * \param fmt String to write to the log, containing optional format - * specifiers - * \param args Variable argument list - * - * \sa wl_log_set_handler_client - * \sa wl_log_set_handler_server - */ typedef void (*wl_log_func_t)(const char *fmt, va_list args) WL_PRINTF(1, 0); -/** - * Return value of an iterator function - * - * \sa wl_client_for_each_resource_iterator_func_t - * \sa wl_client_for_each_resource - */ enum wl_iterator_result { - /** Stop the iteration */ WL_ITERATOR_STOP, - /** Continue the iteration */ WL_ITERATOR_CONTINUE }; diff --git a/app/src/main/cpp/waylandcomp/protocols/color-management-v1.xml b/app/src/main/cpp/waylandcomp/protocols/color-management-v1.xml index 7f8da78f1..ef02406fd 100644 --- a/app/src/main/cpp/waylandcomp/protocols/color-management-v1.xml +++ b/app/src/main/cpp/waylandcomp/protocols/color-management-v1.xml @@ -1481,7 +1481,6 @@ - diff --git a/app/src/main/cpp/waylandcomp/protocols/linux-dmabuf-v1.xml b/app/src/main/cpp/waylandcomp/protocols/linux-dmabuf-v1.xml index 733717545..a88a20ac4 100644 --- a/app/src/main/cpp/waylandcomp/protocols/linux-dmabuf-v1.xml +++ b/app/src/main/cpp/waylandcomp/protocols/linux-dmabuf-v1.xml @@ -158,7 +158,6 @@ summary="low 32 bits of layout modifier"/> - diff --git a/app/src/main/cpp/waylandcomp/protocols/presentation-time.xml b/app/src/main/cpp/waylandcomp/protocols/presentation-time.xml index c2431c500..c71c2fbe3 100644 --- a/app/src/main/cpp/waylandcomp/protocols/presentation-time.xml +++ b/app/src/main/cpp/waylandcomp/protocols/presentation-time.xml @@ -1,6 +1,5 @@ - Copyright © 2013-2014 Collabora, Ltd. @@ -28,7 +27,6 @@ - The main feature of this interface is accurate presentation timing feedback to ensure smooth video playback while maintaining @@ -41,7 +39,6 @@ the wl_surface.commit and provides feedback on the content update, particularly the final realized presentation time. - When the final realized presentation time is available, e.g. after a framebuffer flip completes, the requested diff --git a/app/src/main/cpp/waylandcomp/protocols/text-input-unstable-v3.xml b/app/src/main/cpp/waylandcomp/protocols/text-input-unstable-v3.xml index 01c1e2104..f75317ace 100644 --- a/app/src/main/cpp/waylandcomp/protocols/text-input-unstable-v3.xml +++ b/app/src/main/cpp/waylandcomp/protocols/text-input-unstable-v3.xml @@ -458,7 +458,6 @@ - diff --git a/app/src/main/cpp/waylandcomp/protocols/wlr-data-control-unstable-v1.xml b/app/src/main/cpp/waylandcomp/protocols/wlr-data-control-unstable-v1.xml index 567a09e52..2771e9e0b 100644 --- a/app/src/main/cpp/waylandcomp/protocols/wlr-data-control-unstable-v1.xml +++ b/app/src/main/cpp/waylandcomp/protocols/wlr-data-control-unstable-v1.xml @@ -142,7 +142,6 @@ - diff --git a/app/src/main/cpp/waylandcomp/protocols/xdg-shell.xml b/app/src/main/cpp/waylandcomp/protocols/xdg-shell.xml index 1df700146..85e45044e 100644 --- a/app/src/main/cpp/waylandcomp/protocols/xdg-shell.xml +++ b/app/src/main/cpp/waylandcomp/protocols/xdg-shell.xml @@ -366,7 +366,6 @@ - @@ -1115,7 +1114,6 @@ - @@ -1139,7 +1137,6 @@ - @@ -1294,7 +1291,6 @@ - diff --git a/app/src/main/cpp/waylandcomp/src/ahb_swapchain.c b/app/src/main/cpp/waylandcomp/src/ahb_swapchain.c index 4a981f1ee..6cf0c8037 100644 --- a/app/src/main/cpp/waylandcomp/src/ahb_swapchain.c +++ b/app/src/main/cpp/waylandcomp/src/ahb_swapchain.c @@ -1,5 +1,3 @@ -/* Zero-copy game frames: banner_ahb_v1 + the game's AHardwareBuffers on the sc_layer SurfaceControl. - * See ahb_swapchain.h and ZERO_COPY_SPIKE.md. */ #define _GNU_SOURCE #include "ahb_swapchain.h" #include "droiddeck_color.h" @@ -19,9 +17,8 @@ #include #include -#define MOD_QCOM_COMPRESSED 0x0500000000000001ULL /* DRM_FORMAT_MOD_QCOM_COMPRESSED (UBWC) */ +#define MOD_QCOM_COMPRESSED 0x0500000000000001ULL -/* linux/dma-buf.h (not in the NDK sysroot's older headers): sync_file export / import. */ struct droiddeck_dma_buf_sync_file { uint32_t flags; int32_t fd; }; #define DROIDDECK_DMA_BUF_SYNC_READ 1u #define DROIDDECK_DMA_BUF_IOCTL_EXPORT_SYNC_FILE _IOWR('b', 2, struct droiddeck_dma_buf_sync_file) @@ -29,47 +26,43 @@ struct droiddeck_dma_buf_sync_file { uint32_t flags; int32_t fd; }; extern volatile int g_zero_copy; -/* One per wl_buffer the game attached an AHardwareBuffer to. Lives while the wl_buffer exists or - * the buffer is still on the layer / held by SurfaceFlinger, whichever is longer. */ struct ahb_buf { - uint64_t id; /* the token sc_layer.c hands back */ - struct dmabuf_buffer *b; /* one reference held for the life of this record */ - AHardwareBuffer *ahb; /* our reference to the game's buffer */ + uint64_t id; + struct dmabuf_buffer *b; + AHardwareBuffer *ahb; int w, h; uint32_t stride, image_count; - uint32_t format; /* AHARDWAREBUFFER_FORMAT_* gralloc gave it (10-bit for HDR10) */ + uint32_t format; uint64_t modifier; struct wl_client *client; - struct wl_resource *resource; /* the wl_buffer; NULL once the client destroyed it */ + struct wl_resource *resource; struct wl_listener resource_destroy; - int on_layer; /* set on the SurfaceControl (or not yet released by SurfaceFlinger) */ - int release_pending; /* the surface let go of it while on the layer */ - int64_t deferred_ns; /* when it did (the perf line: how long the display kept it) */ - struct surface *surface; /* for the paced release; NULL = release at once */ - struct wl_event_source *fence_src; /* fallback: waiting the release fence in the event loop */ + int on_layer; + int release_pending; + int64_t deferred_ns; + struct surface *surface; + struct wl_event_source *fence_src; int fence_fd; struct wl_list link; }; static struct wl_list g_bufs; -static struct wl_list g_clients; /* bound banner_ahb_v1 resources (wl_resource links) */ +static struct wl_list g_clients; static uint64_t g_next_id = 1; static struct wl_event_loop *g_loop; static unsigned g_stat_zero_copy; static int g_export_failed_logged, g_import_failed_logged; -static int g_advertised; /* the global exists */ -static int g_mode_sent = -1; /* the mode clients were last told (-1 = none yet) */ -static struct surface *g_announced; /* "presenting X without a copy" said for this surface */ -static volatile int64_t g_last_zero_copy_ns; /* when a frame last went on the layer without a copy */ +static int g_advertised; +static int g_mode_sent = -1; +static struct surface *g_announced; +static volatile int64_t g_last_zero_copy_ns; -/* SurfaceFlinger's callback thread -> compositor thread: released tokens + their fences. */ struct released { uint64_t id; int fd; }; static pthread_mutex_t g_rel_lock = PTHREAD_MUTEX_INITIALIZER; static struct released *g_rel; static int g_rel_n, g_rel_cap; static int g_rel_pipe[2] = {-1, -1}; -/* Which swapchains were announced (one log line per swapchain, not per image). */ struct chain_seen { struct wl_client *client; int w, h; uint32_t image_count; uint64_t modifier; uint32_t format; }; static struct chain_seen g_last_chain; @@ -96,7 +89,6 @@ static void buf_free(struct ahb_buf *ab) { free(ab); } -/* Free once neither the client (wl_buffer) nor the display (layer) refers to it. */ static void maybe_free(struct ahb_buf *ab) { if (!ab->resource && !ab->on_layer && !ab->fence_src) buf_free(ab); } @@ -105,11 +97,10 @@ static void on_buffer_resource_destroyed(struct wl_listener *l, void *data) { struct ahb_buf *ab = wl_container_of(l, ab, resource_destroy); wl_list_remove(&ab->resource_destroy.link); ab->resource = NULL; - ab->release_pending = 0; /* nothing left to release to */ + ab->release_pending = 0; maybe_free(ab); } -/* ---- release: SurfaceFlinger is done with the buffer -> the game's next acquire waits, then wl_buffer.release */ static void send_deferred_release(struct ahb_buf *ab) { if (ab->release_pending && ab->resource) @@ -129,10 +120,6 @@ static int on_fence_readable(int fd, uint32_t mask, void *data) { return 0; } -/* Compositor thread. Puts the display's release fence into the dma-buf so the game's driver waits - * on it before rendering into the buffer again (Mesa exports every fence of the dma-buf as the - * acquire semaphore), then gives the buffer back. If the kernel refuses the import, the release - * itself waits for the fence instead. */ static void handle_released(uint64_t id, int fd) { struct ahb_buf *ab = find_buf(id); if (!ab) { if (fd >= 0) close(fd); return; } @@ -148,7 +135,6 @@ static void handle_released(uint64_t id, int fd) { droiddeck_log("layer", "zero-copy: DMA_BUF_IOCTL_IMPORT_SYNC_FILE failed (%s): releases wait for the display's fence here instead", strerror(errno)); } - /* Wait it out in the event loop (a sync_file is readable once signalled). */ struct pollfd p = {.fd = fd, .events = POLLIN}; if (poll(&p, 1, 0) > 0) { close(fd); @@ -156,7 +142,7 @@ static void handle_released(uint64_t id, int fd) { ab->fence_fd = fd; ab->fence_src = wl_event_loop_add_fd(g_loop, fd, WL_EVENT_READABLE, on_fence_readable, ab); if (!ab->fence_src) { close(fd); ab->fence_fd = -1; } - else return; /* released from on_fence_readable */ + else return; } } } @@ -179,7 +165,6 @@ static int on_release_pipe(int fd, uint32_t mask, void *data) { return 0; } -/* SurfaceFlinger's callback thread. */ void ahb_swapchain_layer_released(void *token, int release_fd) { pthread_mutex_lock(&g_rel_lock); if (g_rel_n == g_rel_cap) { @@ -193,7 +178,6 @@ void ahb_swapchain_layer_released(void *token, int release_fd) { if (g_rel_pipe[1] >= 0) { char c = 1; if (write(g_rel_pipe[1], &c, 1) < 0) {} } } -/* ---- present */ int ahb_swapchain_has_ahb(const struct dmabuf_buffer *b) { return b && *droiddeck_dmabuf_ahb_slot((struct dmabuf_buffer *)b) != NULL; @@ -203,8 +187,6 @@ int ahb_swapchain_present(struct dmabuf_buffer *b, struct surface *s, int scene_ struct ahb_buf *ab = b ? *droiddeck_dmabuf_ahb_slot(b) : NULL; if (!ab) return -1; int dmabuf_fd = droiddeck_dmabuf_fd(b); - /* Acquire fence: the game's render fence, which its driver put into the dma-buf before the - * commit (Mesa's implicit sync). The display waits on it, not the CPU. */ int acquire = -1; struct droiddeck_dma_buf_sync_file exp = {.flags = DROIDDECK_DMA_BUF_SYNC_READ, .fd = -1}; if (dmabuf_fd >= 0 && ioctl(dmabuf_fd, DROIDDECK_DMA_BUF_IOCTL_EXPORT_SYNC_FILE, &exp) == 0 && exp.fd >= 0) { @@ -215,14 +197,14 @@ int ahb_swapchain_present(struct dmabuf_buffer *b, struct surface *s, int scene_ droiddeck_log("layer", "zero-copy: DMA_BUF_IOCTL_EXPORT_SYNC_FILE failed (%s): waiting for each frame on the CPU instead", strerror(errno)); } - struct pollfd p = {.fd = dmabuf_fd, .events = POLLIN}; /* readable = the writers are done */ + struct pollfd p = {.fd = dmabuf_fd, .events = POLLIN}; int r; do { r = poll(&p, 1, 100); } while (r < 0 && errno == EINTR); } int r = sc_layer_present_ahb(ab->ahb, ab->w, ab->h, acquire, (void *)(uintptr_t)ab->id, scene_w, scene_h, s ? droiddeck_surface_color(s) : NULL, ab->format); if (r < 0) return -1; - if (r == 1) return 0; /* nothing of it on screen: not on the layer, nothing to release later */ + if (r == 1) return 0; if (!ab->on_layer) { ab->on_layer = 1; g_stat_zero_copy++; @@ -272,7 +254,6 @@ uint32_t ahb_swapchain_ahb_format(const struct dmabuf_buffer *b) { return ab ? ab->format : 0; } -/* ---- banner_ahb_v1 */ static void ahb_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } @@ -287,10 +268,8 @@ static void ahb_attach(struct wl_client *c, struct wl_resource *r, struct wl_res return; } void **slot = droiddeck_dmabuf_ahb_slot(b); - if (*slot) { close(sock); return; } /* once per buffer */ + if (*slot) { close(sock); return; } - /* The handle was written into the socket before the request was sent; a short wait is just - * insurance against a slow sender. */ struct pollfd p = {.fd = sock, .events = POLLIN}; int pr; do { pr = poll(&p, 1, 1000); } while (pr < 0 && errno == EINTR); @@ -357,10 +336,7 @@ static void bind_ahb(struct wl_client *c, void *data, uint32_t ver, uint32_t id) if (!r) { wl_client_post_no_memory(c); return; } wl_resource_set_implementation(r, &ahb_impl, NULL, on_client_resource_destroyed); wl_list_insert(&g_clients, wl_resource_get_link(r)); - /* The mode travels with the bind, so the client's registry roundtrip already has it when it - * decides about its first swapchain. */ if (ver >= BANNER_AHB_V1_MODE_SINCE_VERSION) banner_ahb_v1_send_mode(r, g_zero_copy ? 1u : 0u); - /* One line per program, not per surface-format query (each binds its own). */ if (last_named != c) { last_named = c; droiddeck_log("layer", "zero-copy: %s bound banner_ahb_v1 version %u (%s)", droiddeck_client_name(c), ver, @@ -386,7 +362,6 @@ void ahb_swapchain_set_mode(int on, int live) { told++; } } - /* The next chain and the next fullscreen frame get announced again. */ memset(&g_last_chain, 0, sizeof(g_last_chain)); g_announced = NULL; if (live) diff --git a/app/src/main/cpp/waylandcomp/src/ahb_swapchain.h b/app/src/main/cpp/waylandcomp/src/ahb_swapchain.h index 2c72a12b5..777448273 100644 --- a/app/src/main/cpp/waylandcomp/src/ahb_swapchain.h +++ b/app/src/main/cpp/waylandcomp/src/ahb_swapchain.h @@ -1,28 +1,5 @@ #ifndef AHB_SWAPCHAIN_H #define AHB_SWAPCHAIN_H -/* - * Zero-copy game frames (layer mode, DROIDDECK_WAYLAND_ZERO_COPY=1 at launch, the drawer's "Zero-copy - * presentation" switch live): the guest half of the design in ZERO_COPY_SPIKE.md. Our Wayland Turnip - * (banners-turnip-wayland, patches/wayland/banner_ahb_wsi.py) binds the private global banner_ahb_v1 - * (version 2), which this compositor advertises on every session where a display layer is possible, - * and follows its `mode` event: while the mode is on, each swapchain the game creates has its images - * allocated as gralloc AHardwareBuffers, shared as dma-bufs through zwp_linux_dmabuf_v1 as before (so - * the blit path still works) and handed to us once per wl_buffer over a socketpair - * (banner_ahb_v1.attach). When such a buffer is the one fullscreen frame, it goes straight onto the - * sc_layer SurfaceControl: SurfaceFlinger / the display scan out the game's own buffer, no copy - * anywhere. A mode flip (ahb_swapchain_set_mode, from the drawer through the host queue) is - * broadcast to every bound client; a client whose live swapchain was built for the other mode - * retires it (VK_ERROR_OUT_OF_DATE_KHR) so the program rebuilds it, and until then its buffers keep - * presenting as before: an attached AHardwareBuffer stays with its wl_buffer whatever the mode is. - * - * Fences stay implicit, in the dma-buf itself: Mesa imports the render fence into the dma-buf - * before it commits, we export it as the layer's acquire fence (DMA_BUF_IOCTL_EXPORT_SYNC_FILE), - * and import SurfaceFlinger's release fence back (DMA_BUF_IOCTL_IMPORT_SYNC_FILE) before sending - * wl_buffer.release, so Mesa's acquire (wsi_create_sync_for_dma_buf_wait) waits for the display. - * - * Compositor thread unless noted. Without a usable display layer (sc_layer_available() == 0) the - * global is not created and nothing here runs. - */ #include #include #include @@ -30,53 +7,28 @@ struct dmabuf_buffer; struct surface; -/* ---- compositor.c -> ahb_swapchain.c */ -/* Create the banner_ahb_v1 global (when a display layer is possible at all) and the release queue. */ void ahb_swapchain_init(struct wl_display *display); -/* Zero-copy on/off, live: sets g_zero_copy, tells every bound client (banner_ahb_v1.mode) and logs - * it (live = 1: a drawer switch mid-session; 0: the launch default). Compositor thread. */ void ahb_swapchain_set_mode(int on, int live); -/* Milliseconds since a game frame was last put on the layer without a copy (-1 = never). Any thread: - * the drawer's status line. */ int ahb_swapchain_last_frame_age_ms(void); -/* 1 if the buffer carries an AHardwareBuffer from the game. */ int ahb_swapchain_has_ahb(const struct dmabuf_buffer *b); -/* Show b (the topmost fullscreen frame, shown by surface s) on the layer without a copy. - * 0 = done (or the same frame is already up), -1 = unavailable (draw the old way). */ int ahb_swapchain_present(struct dmabuf_buffer *b, struct surface *s, int scene_w, int scene_h); -/* Surface s lets go of buffer b (its wl_buffer `buffer`, NULL if the client destroyed it); paced = - * the limiter's cadence applies. Returns 1 when the buffer is on the layer: the release is sent - * from here once SurfaceFlinger's release fence is known. 0 = the caller releases as usual. */ int ahb_swapchain_defer_release(struct dmabuf_buffer *b, struct wl_resource *buffer, struct surface *s, int paced); -/* s is being destroyed: forget it (its deferred releases still go out, unpaced). */ void ahb_swapchain_surface_gone(struct surface *s); -/* Zero-copy frames since the last call (the 10 s summary). */ unsigned ahb_swapchain_stats_take(void); -/* 1 when the banner_ahb_v1 global exists (a display layer is possible): part of the HDR gate. */ int ahb_swapchain_advertised(void); -/* The AHARDWAREBUFFER_FORMAT_* of the game's buffer behind b, 0 when it has none. */ uint32_t ahb_swapchain_ahb_format(const struct dmabuf_buffer *b); -/* ---- sc_layer.c -> ahb_swapchain.c (SurfaceFlinger's callback thread): the layer let go of the - * buffer behind `token`; release_fd (owned by the callee, -1 = none) signals when the display is - * done reading it. */ void ahb_swapchain_layer_released(void *token, int release_fd); -/* ---- ahb_swapchain.c -> compositor.c (hooks) */ struct dmabuf_buffer *droiddeck_dmabuf_from_resource(struct wl_resource *buffer); -int droiddeck_dmabuf_fd(const struct dmabuf_buffer *b); /* plane 0's dma-buf */ +int droiddeck_dmabuf_fd(const struct dmabuf_buffer *b); void droiddeck_dmabuf_size(const struct dmabuf_buffer *b, int *w, int *h); -void **droiddeck_dmabuf_ahb_slot(struct dmabuf_buffer *b); /* this module's per-buffer state */ +void **droiddeck_dmabuf_ahb_slot(struct dmabuf_buffer *b); void droiddeck_dmabuf_ref(struct dmabuf_buffer *b); void droiddeck_dmabuf_unref(struct dmabuf_buffer *b); -/* Give a wl_buffer back to its client now (paced = 0) or on the FPS limiter's cadence (s != NULL). - * since_ns = when the compositor let go of it (CLOCK_MONOTONIC; the perf line's release latency). */ void droiddeck_release_buffer(struct surface *s, struct wl_resource *buffer, int paced, int64_t since_ns); -/* Redraw the scene on the next tick (the present path may have changed). */ void droiddeck_request_redraw(void); -/* "" (program) of the window a surface belongs to, for the log. */ void droiddeck_surface_describe(const struct surface *s, char *out, size_t size); -/* The surface's current image description (droiddeck_color.h), NULL = none / HDR gate closed. */ struct droiddeck_color; const struct droiddeck_color *droiddeck_surface_color(const struct surface *s); diff --git a/app/src/main/cpp/waylandcomp/src/blend.frag b/app/src/main/cpp/waylandcomp/src/blend.frag index 88739de25..54e1ac495 100644 --- a/app/src/main/cpp/waylandcomp/src/blend.frag +++ b/app/src/main/cpp/waylandcomp/src/blend.frag @@ -1,5 +1,4 @@ #version 450 -// Wayland buffers carry premultiplied alpha; the pipeline blends ONE, ONE_MINUS_SRC_ALPHA. layout(binding = 0) uniform sampler2D Surface; layout(location = 0) in vec2 uv; layout(location = 0) out vec4 outColor; diff --git a/app/src/main/cpp/waylandcomp/src/blend.vert b/app/src/main/cpp/waylandcomp/src/blend.vert index 5c87cc352..660082100 100644 --- a/app/src/main/cpp/waylandcomp/src/blend.vert +++ b/app/src/main/cpp/waylandcomp/src/blend.vert @@ -1,5 +1,4 @@ #version 450 -// One surface as a quad: dst = the target rectangle in NDC, src = the buffer rectangle in UV. layout(push_constant) uniform PC { vec4 dst; vec4 src; } pc; layout(location = 0) out vec2 uv; diff --git a/app/src/main/cpp/waylandcomp/src/blend_frag.h b/app/src/main/cpp/waylandcomp/src/blend_frag.h index 9d2af54a3..d4409cefa 100644 --- a/app/src/main/cpp/waylandcomp/src/blend_frag.h +++ b/app/src/main/cpp/waylandcomp/src/blend_frag.h @@ -1,4 +1,3 @@ - // 1115.1.0 #pragma once const uint32_t blend_frag_code[] = { 0x07230203,0x00010000,0x0008000b,0x00000014,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/waylandcomp/src/blend_pass.c b/app/src/main/cpp/waylandcomp/src/blend_pass.c index 842287b07..9a4d48d0b 100644 --- a/app/src/main/cpp/waylandcomp/src/blend_pass.c +++ b/app/src/main/cpp/waylandcomp/src/blend_pass.c @@ -1,19 +1,16 @@ -/* Alpha composition: one surface drawn source-over onto the scene - see blend_pass.h. Compositor thread. */ #define _POSIX_C_SOURCE 200809L #include "blend_pass.h" #include "vk_loader.h" -#include "vk_present.h" /* droiddeck_log, vkp_gpu_name */ +#include "vk_present.h" #include <string.h> -/* SPIR-V, pre-compiled (glslangValidator -V blend.vert --vn blend_vert_code, the same for blend.frag) - * and committed next to the GLSL: the NDK build compiles no shaders. */ #include "blend_vert.h" #include "blend_frag.h" struct pc_blend { float dst[4]; float src[4]; }; static VkDevice g_dev; -static int g_ready; /* 0 = not built, 1 = ok, -1 = unavailable (said once) */ +static int g_ready; static VkSampler g_sampler; static VkDescriptorSetLayout g_dsl; static VkPipelineLayout g_pl; @@ -21,8 +18,6 @@ static VkDescriptorPool g_dpool; static VkShaderModule g_vert, g_frag; static unsigned g_frame = 1; -/* One render pass + pipeline per target format: the screen swapchain's, the scene image's, the HDR - * composition's. */ #define NFMT 4 static struct { VkFormat fmt; VkRenderPass rp; VkPipeline pipe; } g_out[NFMT]; @@ -60,8 +55,6 @@ static VkShaderModule shader(const uint32_t *code, size_t size) { return m; } -/* The target keeps what the blits put there (LOAD) and stays in TRANSFER_DST_OPTIMAL for the blits - * that follow. The dependencies order the pass between them. */ static VkRenderPass make_rp(VkFormat fmt) { VkAttachmentDescription att = {.format = fmt, .samples = VK_SAMPLE_COUNT_1_BIT, .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD, .storeOp = VK_ATTACHMENT_STORE_OP_STORE, @@ -111,7 +104,6 @@ static VkPipeline make_pipe(VkRenderPass rp) { .frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE, .lineWidth = 1.0f}; VkPipelineMultisampleStateCreateInfo ms = {.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT}; - /* The target's alpha is left alone: the screen is opaque whatever is drawn onto it. */ VkPipelineColorBlendAttachmentState ba = { .blendEnable = VK_TRUE, .srcColorBlendFactor = VK_BLEND_FACTOR_ONE, .dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, .colorBlendOp = VK_BLEND_OP_ADD, @@ -201,8 +193,6 @@ static VkImageView make_view(VkImage img, VkFormat fmt) { return v; } -/* The slot to recycle: a free one, else the one unused for longest - never one this command buffer - * already refers to. -1 = every slot is in this frame. */ #define OLDEST(arr, n, out) do { \ unsigned age_ = 0; \ (out) = -1; \ diff --git a/app/src/main/cpp/waylandcomp/src/blend_pass.h b/app/src/main/cpp/waylandcomp/src/blend_pass.h index 2faade252..d6f5a8fff 100644 --- a/app/src/main/cpp/waylandcomp/src/blend_pass.h +++ b/app/src/main/cpp/waylandcomp/src/blend_pass.h @@ -1,27 +1,12 @@ #ifndef BLEND_PASS_H #define BLEND_PASS_H -/* - * Alpha composition. The scene is composed with blits, which replace what is under them; a surface - * with translucent pixels (gamescope's overlay and notification planes over a game, a toolkit's - * shadowed popup) is drawn through this pass instead, source-over with premultiplied alpha. - * - * Compositor thread. Objects are built on the first use; if the driver refuses them the pass is - * unavailable for the session (logged once) and the caller keeps blitting. - */ #define VK_USE_PLATFORM_ANDROID_KHR #include <vulkan/vulkan.h> -/* Called from dev_init (device up). */ void blendp_bind_device(VkDevice dev); -/* A source or target image is about to be destroyed: drop what is cached for it. Frames are fenced, - * so nothing submitted still uses it. */ void blendp_forget_image(VkImage img); -/* A new command buffer is being recorded: what the previous one used may be recycled again. */ void blendp_begin_frame(void); -/* Record one surface over `dst`. `dst` is in TRANSFER_DST_OPTIMAL before and after. `src` was created - * with SAMPLED usage and is in GENERAL (src_general) or TRANSFER_SRC_OPTIMAL, and is left there. - * `blit` is the draw as the blit path maps it. 0 = recorded, -1 = unavailable (nothing recorded). */ int blendp_draw(VkCommandBuffer cmd, VkImage src, VkFormat src_fmt, int src_w, int src_h, int src_general, VkImage dst, VkFormat dst_fmt, int dst_w, int dst_h, const VkImageBlit *blit); diff --git a/app/src/main/cpp/waylandcomp/src/blend_vert.h b/app/src/main/cpp/waylandcomp/src/blend_vert.h index f402c6a92..a296cb0c0 100644 --- a/app/src/main/cpp/waylandcomp/src/blend_vert.h +++ b/app/src/main/cpp/waylandcomp/src/blend_vert.h @@ -1,4 +1,3 @@ - // 1115.1.0 #pragma once const uint32_t blend_vert_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000003a,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/waylandcomp/src/cardboard.c b/app/src/main/cpp/waylandcomp/src/cardboard.c index ffb1918b0..8031d61d7 100644 --- a/app/src/main/cpp/waylandcomp/src/cardboard.c +++ b/app/src/main/cpp/waylandcomp/src/cardboard.c @@ -1,8 +1,3 @@ -/* Cardboard pass - see cardboard.h. Compositor thread unless noted. - * - * The shader is cardboard.frag, pre-compiled like the rest of the compositor's SPIR-V: - * glslangValidator -V --vn cardboard_frag_code -o cardboard_frag.h cardboard.frag - */ #define _POSIX_C_SOURCE 200809L #include "cardboard.h" #include <math.h> @@ -20,15 +15,14 @@ #ifndef CARDBOARD_HOST_TEST #include "vk_loader.h" -#include "vk_present.h" /* droiddeck_log */ -/* upscale_vert.h defines its array non-static, and effects_chain.c includes it too: a private name here. */ +#include "vk_present.h" + #define upscale_vert_code cardboard_upscale_vert_code #include "upscale_vert.h" #undef upscale_vert_code #include "cardboard_frag.h" #endif -/* ------------------------------------------------------------------ pure helpers */ static float clampf(float v, float lo, float hi) { return v < lo ? lo : v > hi ? hi : v; } @@ -47,29 +41,25 @@ 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]) { - /* rot0..rot2: the pose's rows, each with one scalar riding in w. */ 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->ipd_m * 0.5f; out[7] = p->screen_dist_m; out[11] = p->screen_width_m * 0.5f; - /* lens */ out[12] = p->k1; out[13] = p->k2; out[14] = tanf(p->fov_deg * 0.5f * (float)M_PI / 180.0f); out[15] = p->chroma; - /* view */ out[16] = p->lens_inset; 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; /* one eye's half */ + 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 * 0.5f) / (float)out_h : 1.0f; - out[20] = out[21] = out[22] = out[23] = 0.0f; /* reserved */ + out[20] = out[21] = out[22] = out[23] = 0.0f; } #ifndef CARDBOARD_HOST_TEST -/* ------------------------------------------------------------------ settings + pose */ static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; static struct cardboard_params g_set = { @@ -78,8 +68,8 @@ 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 int g_live; /* the last pose came from a sensor */ -static void shm_publish(int live); /* below: the guest's copy of the pose */ +static int g_live; +static void shm_publish(int live); void vkp_cardboard_set(const struct cardboard_params *p) { struct cardboard_params c = *p; @@ -96,9 +86,8 @@ void vkp_cardboard_set(const struct cardboard_params *p) { 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); } -/* The guest's copy of the pose (headpose_shm.h), mapped once a path is known. */ static struct droiddeck_headpose *g_shm; -static int g_eye_w, g_eye_h; /* the last eye viewport drawn, for the guest */ +static int g_eye_w, g_eye_h; void vkp_cardboard_set_pose_file(const char *path) { pthread_mutex_lock(&g_lock); @@ -120,7 +109,6 @@ void vkp_cardboard_set_pose_file(const char *path) { pthread_mutex_unlock(&g_lock); } -/* Under g_lock. */ static void shm_publish(int live) { if (!g_shm) return; struct timespec ts; @@ -174,15 +162,14 @@ int vkp_cardboard_pose_changed(void) { return 1; } -/* ------------------------------------------------------------------ Vulkan objects */ #define CB_FORMAT VK_FORMAT_R8G8B8A8_UNORM -#define CB_PC_SIZE (4 * 4 + 24 * 4) /* ndc + the 24 floats: 112 bytes, under the 128 guaranteed */ -#define CB_MAX_SOURCES 8 /* source views per frame: the real frame + generated ones */ +#define CB_PC_SIZE (4 * 4 + 24 * 4) +#define CB_MAX_SOURCES 8 static VkDevice g_dev; static VkPhysicalDeviceMemoryProperties g_mp; -static int g_ready; /* 0 = not built, 1 = ok, -1 = failed (said once) */ +static int g_ready; static VkRenderPass g_rp; static VkDescriptorSetLayout g_dsl; static VkPipelineLayout g_pl; @@ -234,9 +221,6 @@ static VkImageView make_view(VkImage img) { return v; } -/* The panel-sized output: rendered into, then blitted 1:1 to the swapchain. Kept uncompressed - * (mutable format) like the effect chain's targets: UBWC render-then-blit targets corrupt on - * Adreno 740. */ static int target_ensure(int w, int h) { if (g_t.img && g_t.w == w && g_t.h == h) return 0; target_destroy(); @@ -295,7 +279,6 @@ static int objects_ensure(void) { if (g_ready) return g_ready == 1 ? 0 : -1; if (!g_dev || !g_vk.CreateGraphicsPipelines) goto fail; { - /* One colour target, overwritten whole, left GENERAL for the 1:1 blit that follows. */ VkAttachmentDescription att = {.format = CB_FORMAT, .samples = VK_SAMPLE_COUNT_1_BIT, .loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, .storeOp = VK_ATTACHMENT_STORE_OP_STORE, .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, @@ -444,4 +427,4 @@ VkImage vkp_cardboard_run(VkCommandBuffer cmd, VkImage src, int src_w, int src_h return g_t.img; } -#endif /* CARDBOARD_HOST_TEST */ +#endif diff --git a/app/src/main/cpp/waylandcomp/src/cardboard.frag b/app/src/main/cpp/waylandcomp/src/cardboard.frag index 55167fc88..4dbc67fc2 100644 --- a/app/src/main/cpp/waylandcomp/src/cardboard.frag +++ b/app/src/main/cpp/waylandcomp/src/cardboard.frag @@ -1,55 +1,36 @@ #version 450 -// Cardboard pass (cardboard.c): the finished frame -> two eye viewports, side by side, each -// barrel-distorted for a phone-in-a-viewer lens. Runs on upscale.vert's full quad, so uv covers -// the whole panel with (0,0) at the top left. -// -// Source modes: -// 0 = virtual screen: the frame is a flat screen hanging in front of the viewer. Each eye casts -// a ray through its lens, rotated by the head pose, from its own position (half the IPD to -// either side), so the screen has real stereo depth and stays put while the head turns. -// 1 = side by side: the frame already holds a left and a right eye image (SteamVR, an SBS 3D -// game or video); each eye samples its own half. The head pose belongs to whoever rendered -// it, so none is applied here. -// -// Regenerate cardboard_frag.h after editing (glslangValidator, see cardboard.c). layout(binding = 0) uniform sampler2D Source; layout(location = 0) in vec2 uv; layout(location = 0) out vec4 outColor; layout(push_constant) uniform PC { - vec4 ndc; // upscale.vert's quad - vec4 rot0; // xyz: head -> world rotation, row 0; w: half the IPD (m) - vec4 rot1; // xyz: row 1; w: distance to the virtual screen (m) - vec4 rot2; // xyz: row 2; w: half the virtual screen's width (m) - vec4 lens; // x: k1, y: k2, z: tan of half the eye's vertical field of view, w: chromatic spread - vec4 view; // x: lens centre inset (fraction of an eye viewport), y: source aspect w/h, - // z: source mode, w: eye viewport aspect w/h + vec4 ndc; + vec4 rot0; + vec4 rot1; + vec4 rot2; + vec4 lens; + vec4 view; + } pc; const vec3 BACKDROP = vec3(0.015, 0.015, 0.02); -// Lens: a point on the panel at tangent-space offset t (from the lens centre) shows the ray that -// leaves the eye at t * (1 + k1 r^2 + k2 r^4). Sampling outwards like this draws the picture -// barrel-shaped; the viewer's lens pincushions it back. vec2 undistort(vec2 t, float scale) { float r2 = dot(t, t); return t * scale * (1.0 + pc.lens.x * r2 + pc.lens.y * r2 * r2); } -// What one eye sees along tangent direction tan (x right, y down): colour, or the backdrop. vec3 eyeSample(vec2 tan, float eye) { if (pc.view.z > 0.5) { - // Side by side: this eye's half of the source covers the eye's field of view. float tanH = pc.lens.z; vec2 s = vec2(tan.x / (tanH * pc.view.w), tan.y / tanH) * 0.5 + 0.5; 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() takes columns; the push constants carry rows. 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 = R * vec3((eye < 0.5 ? -1.0 : 1.0) * pc.rot0.w, 0.0, 0.0); - if (d.z > -1e-4) return BACKDROP; // looking away from the screen + if (d.z > -1e-4) return BACKDROP; float t = (-pc.rot1.w - o.z) / d.z; vec3 hit = o + d * t; float halfW = pc.rot2.w, halfH = halfW / pc.view.y; @@ -61,7 +42,6 @@ vec3 eyeSample(vec2 tan, float eye) { void main() { float eye = uv.x < 0.5 ? 0.0 : 1.0; vec2 local = vec2(uv.x * 2.0 - eye, uv.y); - // Lens centres sit inset towards the nose by view.x of a viewport. 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; @@ -69,14 +49,12 @@ void main() { vec3 c; if (pc.lens.w > 0.0) { - // Chromatic aberration: the lens bends red least and blue most; pre-spread to match. c.r = eyeSample(undistort(t, 1.0 - pc.lens.w), eye).r; c.g = eyeSample(undistort(t, 1.0), eye).g; c.b = eyeSample(undistort(t, 1.0 + pc.lens.w), eye).b; } else { c = eyeSample(undistort(t, 1.0), eye); } - // Fade out towards each viewport's edge so the lens rim never shows a hard cut. vec2 e = min(local, 1.0 - local); float fade = smoothstep(0.0, 0.03, min(e.x * pc.view.w, e.y)); outColor = vec4(c * fade, 1.0); diff --git a/app/src/main/cpp/waylandcomp/src/cardboard.h b/app/src/main/cpp/waylandcomp/src/cardboard.h index ce39d8645..3fd99e6b7 100644 --- a/app/src/main/cpp/waylandcomp/src/cardboard.h +++ b/app/src/main/cpp/waylandcomp/src/cardboard.h @@ -1,20 +1,5 @@ #ifndef CARDBOARD_H #define CARDBOARD_H -/* - * Cardboard mode: the last stage of the compositor pass. The finished frame (after the effects and - * frame generation) is drawn twice, once per eye, side by side on the panel, barrel-distorted for a - * phone-in-a-viewer lens (cardboard.frag). The frame either hangs as a flat virtual screen in front - * of the viewer, world-locked by the phone's rotation sensor (3DoF), or is already a side-by-side - * stereo pair (SteamVR, SBS 3D content) that only gets the lens treatment. - * - * Present-pass order with Cardboard on (vk_present.c, render_impl): - * scene -> effects -> frame generation -> vkp_cardboard_run (whole panel) -> 1:1 blit -> present - * The Fullscreen Mode mapping does not apply: the pass fills the output itself. - * - * Settings and the head pose come from any thread (JNI, the sensor thread); the compositor thread - * snapshots them per frame. A new pose redraws even a static scene (vkp_cardboard_pose_changed), so - * the virtual screen stays put while the head turns. - */ #define VK_USE_PLATFORM_ANDROID_KHR #include <vulkan/vulkan.h> @@ -22,41 +7,27 @@ enum { CARDBOARD_SOURCE_SCREEN = 0, CARDBOARD_SOURCE_SBS = 1 }; struct cardboard_params { int enabled; - int source; /* CARDBOARD_SOURCE_* */ - float ipd_m; /* distance between the eyes, metres */ - float screen_dist_m; /* virtual screen distance, metres */ - float screen_width_m;/* virtual screen width, metres */ - float k1, k2; /* lens distortion coefficients */ - float fov_deg; /* vertical field of view of one eye before distortion */ - float lens_inset; /* lens centre inset towards the nose, fraction of an eye viewport */ - float chroma; /* chromatic aberration pre-correction (0 = off) */ + int source; + float ipd_m; + float screen_dist_m; + float screen_width_m; + float k1, k2; + float fov_deg; + float lens_inset; + float chroma; }; -/* Any thread. Values are clamped to sane ranges. */ void vkp_cardboard_set(const struct cardboard_params *p); -/* Any thread: the head -> world rotation, row-major 3x3. Identity = looking straight at the screen. - * live = a sensor produced it. Also published to the guest (headpose_shm.h) once a file is set. */ void vkp_cardboard_set_pose(const float m[9], int live); -/* Any thread, once: where the guest's copy of the pose lives (<files>/.wayland-rt/droiddeck-headpose). */ void vkp_cardboard_set_pose_file(const char *path); -/* Compositor thread. */ void vkp_cardboard_bind_device(VkDevice dev, const VkPhysicalDeviceMemoryProperties *memprops); -/* 1 while Cardboard mode is on (the frame must take the compositor pass). */ int vkp_cardboard_active(void); -/* 1 when the settings or the pose changed since the last call: the caller redraws. */ int vkp_cardboard_pose_changed(void); -/* Called once per frame before any vkp_cardboard_run: releases the previous frame's source views - * (that frame is fenced by now). */ void vkp_cardboard_begin_frame(void); -/* Records the pass into cmd: src (src_w x src_h, R8G8B8A8, in GENERAL) -> a panel-sized image - * (out_w x out_h), returned in GENERAL. VK_NULL_HANDLE when the pass is unavailable (the caller - * presents the frame the usual way). */ VkImage vkp_cardboard_run(VkCommandBuffer cmd, VkImage src, int src_w, int src_h, int out_w, int out_h); -/* Pure helpers, shared with the host test (tools/tests). */ void cardboard_clamp(struct cardboard_params *p); -/* The 24 floats after the quad's ndc in the shader's push constants. */ 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]); diff --git a/app/src/main/cpp/waylandcomp/src/cardboard_frag.h b/app/src/main/cpp/waylandcomp/src/cardboard_frag.h index 7b60cd1f0..76f72467a 100644 --- a/app/src/main/cpp/waylandcomp/src/cardboard_frag.h +++ b/app/src/main/cpp/waylandcomp/src/cardboard_frag.h @@ -1,4 +1,3 @@ - // 1116.2.0 #pragma once const uint32_t cardboard_frag_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000016d,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/waylandcomp/src/compositor.c b/app/src/main/cpp/waylandcomp/src/compositor.c index 3d0794667..68911fc20 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -1,23 +1,3 @@ -/* - * droiddeck-wayland - the embedded compositor of the Wayland display path. - * - * Clients are the Wine processes of a container (winewayland.drv) and, through them, - * Mesa's Vulkan WSI. Each process is its own client, so a Windows virtual desktop is - * described with banner_desktop_v1: the desktop owner marks one surface as the desktop, - * and every process reports its top-level windows' desktop positions and the Windows - * stacking order. - * - * The scene is the desktop surface at the bottom, then every mapped xdg_toplevel at its - * reported position in stacking order, each with its subsurface tree (a game's Vulkan - * swapchain is a subsurface of its window) and wp_viewport crop/scale applied. It is - * redrawn once per event-loop pass after any commit or layout change, stretched to the - * Android surface. - * - * Input goes to the desktop surface in desktop coordinates: the Wine server routes it to - * the window under the pointer (or the focus window) in whichever process owns it, the - * same as winex11's virtual desktop. Without a desktop (a client that doesn't speak - * banner_desktop_v1) the topmost window under the pointer gets it. - */ #define _GNU_SOURCE 1 #define _POSIX_C_SOURCE 200809L #include <stdio.h> @@ -58,19 +38,11 @@ #define WLOGI(...) __android_log_print(ANDROID_LOG_INFO, "DroidDeckWayland", __VA_ARGS__) #define WLOGE(...) __android_log_print(ANDROID_LOG_ERROR, "DroidDeckWayland", __VA_ARGS__) -/* ------------------------------------------------------------------ session log - * One readable, time-stamped file per Wayland session in Download/Wayland-logs, also - * mirrored to logcat: which programs connect, the desktop, windows opening and closing, - * Vulkan frames arriving, a frame-rate summary every 10 seconds, and errors. */ #define SESSION_LOG_DIR "/storage/emulated/0/Download/Wayland-logs" static FILE *g_log; -/* Lines logged before the session file exists are kept here and written into it, in order, the - * moment it opens - the compositor runs on its own thread, so anything the app reports as that - * thread starts (the display's HDR capability, for one) would otherwise only reach logcat. The - * lock also serialises the file writes, which now come from the app's threads too. */ #define PRELOG_MAX 32 static pthread_mutex_t g_log_lock = PTHREAD_MUTEX_INITIALIZER; static char *g_prelog[PRELOG_MAX]; @@ -104,8 +76,6 @@ static void open_session_log(void) { localtime_r(&now, &tm); strftime(stamp, sizeof(stamp), "%Y-%m-%d_%H-%M-%S", &tm); - /* The app names the file when it has a session folder to put it in, so one session's logs sit - * together; without that we keep our own dated file. The folder is made on the Java side. */ const char *given = getenv("BL_WAYLAND_LOG"); if (given && *given) { snprintf(path, sizeof(path), "%s", given); @@ -131,8 +101,6 @@ static void open_session_log(void) { "Time Area Event\n" "------------ -------- -----------------------------------------------------\n", stamp, path); - /* Publish the file and flush anything logged before it existed, in one critical section, so a - * line from another thread can never land ahead of the header or be dropped between the two. */ pthread_mutex_lock(&g_log_lock); for (int i = 0; i < g_prelog_n; i++) { fputs(g_prelog[i], f); free(g_prelog[i]); g_prelog[i] = NULL; } g_prelog_n = 0; @@ -141,7 +109,6 @@ static void open_session_log(void) { WLOGI("session log: %s", path); } -/* At most ~10 lines a second for chatty events (window moves), so a drag can't flood logcat. */ static int log_budget(void) { static struct timespec window; static int used; @@ -152,13 +119,12 @@ static int log_budget(void) { } #ifndef BTN_LEFT -#define BTN_LEFT 0x110 /* linux/input-event-codes.h */ +#define BTN_LEFT 0x110 #endif #ifndef BTN_RIGHT #define BTN_RIGHT 0x111 #endif -/* Java sends pointer coordinates in this space, covering the whole output surface. */ #define INPUT_SPACE_W 1920 #define INPUT_SPACE_H 1080 @@ -180,7 +146,7 @@ static const char *client_name(struct wl_client *client) { static void on_client_destroyed(struct wl_listener *l, void *data) { struct client_info *ci = wl_container_of(l, ci, destroy), **pp; droiddeck_log("program", "disconnected: %s (pid %d)", ci->name, (int)ci->pid); - droiddeck_color_client_gone(ci->client); /* HDR summary for a program that presented HDR (gate open only) */ + droiddeck_color_client_gone(ci->client); for (pp = &g_clients; *pp; pp = &(*pp)->next) if (*pp == ci) { *pp = ci->next; break; } free(ci); @@ -202,7 +168,7 @@ static void on_client_created(struct wl_listener *l, void *data) { if ((fd = open(path, O_RDONLY | O_CLOEXEC)) >= 0) { if ((n = read(fd, buf, sizeof(buf) - 1)) > 0) { char *base = buf, *p; - buf[n] = 0; /* argv[0] only; Wine puts the Windows program path there */ + buf[n] = 0; for (p = buf; *p; p++) if (*p == '\\' || *p == '/') base = p + 1; if (*base) snprintf(ci->name, sizeof(ci->name), "%s", base); } @@ -215,149 +181,121 @@ static void on_client_created(struct wl_listener *l, void *data) { droiddeck_log("program", "connected over Wayland: %s (pid %d)", ci->name, (int)ci->pid); } -/* ------------------------------------------------------------------ surfaces */ enum surface_role { ROLE_NONE, ROLE_TOPLEVEL, ROLE_SUBSURFACE, ROLE_DESKTOP, ROLE_CURSOR }; struct surface { struct wl_resource *resource; - struct wl_list link; /* g_surfaces */ + struct wl_list link; - /* Pending state, applied on commit. */ struct wl_resource *pending_buffer; struct wl_listener pending_buffer_destroy; int pending_attach; int pending_src_set, pending_dst_set; - float pending_src[4]; /* x, y, w, h; w < 0 = unset */ - int pending_dst[2]; /* w, h; w < 0 = unset */ + float pending_src[4]; + int pending_dst[2]; struct wl_list pending_frames; - struct wl_list pending_feedback; /* wp_presentation feedback asked for before commit */ + struct wl_list pending_feedback; - /* Current content. */ - struct vkp_image *shm_img; /* our copy of the last wl_shm buffer */ - struct wl_resource *dmabuf; /* current dmabuf wl_buffer (NULL once the client destroyed it) */ - struct dmabuf_buffer *dmabuf_buf; /* its imported image, kept until replaced or the surface goes */ + + struct vkp_image *shm_img; + struct wl_resource *dmabuf; + struct dmabuf_buffer *dmabuf_buf; struct wl_listener dmabuf_destroy; int buf_w, buf_h, has_content; int buf_alpha; - int opaque[4], pending_opaque[4], pending_opaque_set; /* x, y, w, h; w = 0: none */ + int opaque[4], pending_opaque[4], pending_opaque_set; int src_set, dst_set; float src[4]; int dst[2]; - struct wl_list frames; /* frame callbacks for the next redraw */ - struct wl_list feedback; /* presentation feedback for the current content */ - int drawn; /* part of the last rendered scene */ - int64_t next_release_ns; /* FPS limiter: when this surface's last buffer goes back */ - int releases_pending; /* FPS limiter: buffers of this surface still to be released */ + struct wl_list frames; + struct wl_list feedback; + int drawn; + int64_t next_release_ns; + int releases_pending; enum surface_role role; - /* A role-less surface's last buffer, held unreleased: it may become the pointer's image - * (wl_pointer.set_cursor after the commit, which is wlroots' order). */ struct wl_resource *idle_buffer; struct wl_listener idle_buffer_destroy; struct wl_resource *xdg_surface, *xdg_toplevel; struct wl_resource *viewport; - /* Toplevel placement on the scene. */ - int mapped; /* in g_toplevels */ + + int mapped; struct wl_list toplevel_link; - int keep_on_top; /* Cardboard mode's SteamVR window: above every other toplevel (raise_kept_on_top) */ + int keep_on_top; int x, y, placed; uint32_t hwnd; - /* Subsurface tree. */ struct surface *parent; struct wl_resource *subsurface; - struct wl_list children; /* bottom to top */ + struct wl_list children; struct wl_list child_link; int sub_x, sub_y, sub_pending_x, sub_pending_y, sub_pending; int below_parent; - int fullscreen; /* xdg_toplevel.set_fullscreen: no client decorations */ - int toplevel_committed; /* the toplevel's initial commit was answered */ + int fullscreen; + int toplevel_committed; - char *title; /* xdg_toplevel title, for the session log */ - int announced_vulkan; /* logged its first dmabuf frame */ - uint32_t hdr_fmt_logged; /* HDR session: the buffer format last named in the log */ + char *title; + int announced_vulkan; + uint32_t hdr_fmt_logged; }; -static struct wl_list g_surfaces; /* every surface */ -static struct wl_list g_toplevels; /* mapped toplevels, bottom to top */ +static struct wl_list g_surfaces; +static struct wl_list g_toplevels; static struct surface *g_desktop; -static uint32_t *g_zorder; /* last reported Windows stacking order, top first */ +static uint32_t *g_zorder; static size_t g_zorder_count; static struct wl_event_source *g_render_idle, *g_frame_timer; -/* Rendering is driven by the screen's vsync (Java's Choreographer ticks, nativeVsync): one scene - * per refresh showing the newest buffers, so the event loop is never parked in the swapchain and - * clients get their buffers back as soon as they're replaced. Without ticks (an older app, no - * window) a fallback timer renders instead. */ -static int g_dirty; /* something changed since the last render */ -static int64_t g_last_vsync_ns; /* last tick, 0 = none yet */ -static int64_t g_tick_vsync_ns; /* the vsync that tick is for (Choreographer's frame time) */ -static int64_t g_refresh_ns = 16666667; /* measured tick interval (pacing only, never reported) */ + + + + +static int g_dirty; +static int64_t g_last_vsync_ns; +static int64_t g_tick_vsync_ns; +static int64_t g_refresh_ns = 16666667; static struct wl_event_source *g_fallback_timer; static int g_fallback_armed; -/* FPS limiter (the in-game drawer's): frames per second, 0 = unlimited. Set from the app thread, - * read here. Like the X11 path, which delays the "your buffer is free" notice, the limit paces - * when a replaced buffer is released to its client, so the game itself slows to the cap. */ volatile int g_fps_limit; -/* Shortcut launches: explorer's windows (the desktop, taskbar, Start menu) aren't drawn, matching - * the X11 renderer's unviewable "explorer.exe". They still exist for input routing. */ volatile int g_hide_shell; -/* Experimental layer mode (DROIDDECK_WAYLAND_ZERO_COPY=1 in the container's environment): a single - * fullscreen window is shown on its own Android layer (sc_layer.c) instead of being blitted into the - * screen swapchain. Set from the app before the compositor starts; read per frame. */ volatile int g_zero_copy; -static int g_zero_copy_paused; /* effects hold the layer path off (render_scene) */ +static int g_zero_copy_paused; static int g_zero_copy_fx_skip_said; -/* Compressed (UBWC) game buffers: zwp_linux_dmabuf_v1 advertises DRM_FORMAT_MOD_QCOM_COMPRESSED next - * to LINEAR for every format the renderer's own driver can import that way, so Turnip's Wayland WSI - * in the game allocates UBWC swapchain images instead of resolving each frame to a linear copy. - * DROIDDECK_WAYLAND_UBWC=0 in the container's environment turns the advertisement off (A/B switch). Set - * from the app before the compositor starts; read at every zwp_linux_dmabuf_v1 bind. */ volatile int g_ubwc = 1; -/* Debug (DROIDDECK_WAYLAND_NO_RENDER_NODE=1): name no DRM device in the dma-buf feedback, as a phone - * that exposes no /dev/dri node to apps does, to reproduce its OpenGL path here. Set from the app - * before the compositor starts. */ volatile int g_no_render_node; -/* The panel's refresh rate in mHz, from the app; wl_output advertises it so Wine's display modes - * carry the real rate (games pick their saved 144 Hz mode, as on X11). 0 = 60 Hz. */ volatile int g_output_refresh_mhz; -/* The advertised output size: the container's screen (what the app's X server reports on X11), - * so Wine's display-mode list stops at the desktop size. 0 = 1920x1080. */ volatile int g_output_w, g_output_h; struct pending_release { struct wl_resource *buffer; struct wl_listener destroy; - struct surface *surface; /* NULL once the surface is gone */ + struct surface *surface; int64_t at_ns; - int64_t since_ns; /* when the compositor let go of it (the perf line) */ + int64_t since_ns; struct wl_list link; }; static struct wl_list g_pending_releases; static int g_release_timer_fd = -1; static struct wl_event_source *g_release_source; -static int g_scene_w, g_scene_h; /* size of the last drawn scene */ -static int g_desktop_w, g_desktop_h; /* last size the desktop had content at */ -static unsigned g_stat_frames, g_stat_dmabuf, g_stat_shm; /* since the last 10 s summary */ +static int g_scene_w, g_scene_h; +static int g_desktop_w, g_desktop_h; +static unsigned g_stat_frames, g_stat_dmabuf, g_stat_shm; -/* The 10 s `perf` line (on_stats_timer): where the compositor thread's time goes and how long games - * wait for their buffers back. The driver-side times (acquire, present, fence waits) are counted in - * vk_present.c (vkp_perf_take). Compositor thread only. */ static struct { - unsigned ticks; /* screen refreshes (Choreographer ticks) */ - unsigned scenes; /* render_scene() calls ... */ - int64_t scene_ns, scene_max_ns; /* ... and their wall time */ - unsigned copy_scenes; /* scenes drawn into the screen swapchain (the copy path) */ - unsigned releases, releases_held; /* wl_buffer.release after a replacement; held = 1 ms or more */ - int64_t release_ns, release_max_ns; /* replaced (or taken off the layer) -> released to the game */ + unsigned ticks; + unsigned scenes; + int64_t scene_ns, scene_max_ns; + unsigned copy_scenes; + unsigned releases, releases_held; + int64_t release_ns, release_max_ns; } g_perf; static void schedule_render(void); @@ -367,7 +305,6 @@ static int64_t now_ns(void) { return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec; } -/* A game got a replaced buffer back; since_ns = when the compositor let go of it (0 = not counted). */ static void perf_note_release(int64_t since_ns) { if (since_ns <= 0) return; int64_t d = now_ns() - since_ns; @@ -378,7 +315,6 @@ static void perf_note_release(int64_t since_ns) { if (d >= 1000000LL) g_perf.releases_held++; } -/* ------------------------------------------------------------------ buffer release pacing */ static void pending_release_free(struct pending_release *pr) { if (pr->surface && pr->surface->releases_pending > 0) pr->surface->releases_pending--; @@ -387,7 +323,6 @@ static void pending_release_free(struct pending_release *pr) { free(pr); } -/* A surface is going away: its queued releases still go out on time, they just stop counting. */ static void pending_releases_forget_surface(struct surface *s) { struct pending_release *pr; wl_list_for_each(pr, &g_pending_releases, link) @@ -413,7 +348,7 @@ static void arm_release_timer(void) { static int on_release_timer(int fd, uint32_t mask, void *data) { struct pending_release *pr, *tmp; uint64_t expirations; - int64_t now = now_ns() + 300000; /* anything due within 0.3 ms goes now */ + int64_t now = now_ns() + 300000; if (read(fd, &expirations, sizeof(expirations)) < 0 && errno != EAGAIN) return 0; wl_list_for_each_safe(pr, tmp, &g_pending_releases, link) { if (pr->at_ns <= now) { @@ -427,24 +362,8 @@ static int on_release_timer(int fd, uint32_t mask, void *data) { return 0; } -/* Give a replaced buffer back to its client: now, or on the limiter's cadence. - * - * Each release takes the next slot of a per-surface cadence (one slot per interval), so a game - * gets one buffer back per interval however fast it commits. The schedule is kept honest at both - * ends: a slot in the past is brought to now (the game was slower than the cap, no catch-up burst - * is owed), and it never runs further ahead than the releases actually queued - with p releases - * still pending the new one lands at most (p + 1) intervals out, nothing pending means at most - * one interval. Without that bound the schedule kept slots that were consumed but never - * delivered (a swapchain rebuild destroys buffers with queued releases) or that a coarser earlier - * limit had spaced out, and the game's first frames after that waited on empty slots. */ static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_t since_ns) { const int limit = g_fps_limit; - /* Without a limit of its own a game is paced to the screen. A swapchain that does not wait for - * frame callbacks (MAILBOX, IMMEDIATE) is held back by nothing else here, so handing a replaced - * buffer straight back let it draw as fast as the GPU allowed and everything above the refresh - * rate was thrown away - 178 frames a second for 91 on screen, measured (WinNative, maxjivi05). - * One release per refresh is the back-pressure X11 gives, and the screen shows the same frames - * either way. */ const int64_t interval = limit > 0 ? 1000000000LL / limit : g_refresh_ns; if (interval <= 0 || g_release_timer_fd < 0) { wl_buffer_send_release(buffer); perf_note_release(since_ns); return; } int64_t now = now_ns(); @@ -466,9 +385,7 @@ static void release_buffer(struct surface *s, struct wl_resource *buffer, int64_ arm_release_timer(); } -/* ------------------------------------------------------------------ seat state */ -/* Each Wine process is a separate client with its own wl_pointer / wl_keyboard. */ struct seat_pointer { struct wl_resource *ptr; struct wl_resource *focus; }; struct seat_keyboard { struct wl_resource *kb; struct wl_resource *focus; }; struct seat_touch { struct wl_resource *touch; struct wl_resource *focus; }; @@ -478,39 +395,25 @@ static struct seat_pointer g_ptrs[MAX_PTRS]; static int g_nptrs; static struct seat_keyboard g_kbs[MAX_PTRS]; static int g_nkbs; -/* Modifier state in the keymap's real-modifier bits (Shift=0x1, Lock=0x2, Control=0x4, Mod1=0x8, - * Mod4=0x40). A client of the wayland backend - gamescope, labwc - takes modifiers only from - * wl_keyboard.modifiers and ignores what a Shift key event does to its xkb state, so without this - * every capital arrived lowercase. */ static uint32_t g_mods_depressed, g_mods_locked; -static uint32_t g_mod_keys_held; /* one bit per modifier key below, so a repeat press is harmless */ +static uint32_t g_mod_keys_held; static struct seat_touch g_touches[MAX_PTRS]; static int g_ntouches; -/* ---- the client's cursor (wl_pointer.set_cursor) ------------------------------------------- - * Not composited: under zero-copy the program's buffer goes straight to an Android layer and there - * is nothing to composite into. The cursor surface is snapshotted here instead and the app draws it - * as its overlay, above every present path (Bannerlator's design). What it buys: the real SHAPE - - * labwc's resize arrows at a window's edges and corners, a text field's I-beam - and an - * authoritative HIDE (set_cursor with a NULL surface, what a mouse-look game does). - * wl_shm cursors (Wine, pixman) are copied directly; a GPU cursor (labwc on Vulkan/GLES, which - * wlroots hands over as a dma-buf, usually UBWC) is read back through vkp_image_readback. */ #define CURSOR_MAX_PX (256 * 256) static pthread_mutex_t g_cursor_lock = PTHREAD_MUTEX_INITIALIZER; -static struct surface *g_cursor_surface; /* compositor thread only */ -static struct surface *g_cursor_shown; /* compositor thread: whose image g_cursor_px holds */ -static int g_cursor_hx, g_cursor_hy; /* hotspot, surface-local */ -static uint32_t g_cursor_px[CURSOR_MAX_PX]; /* guarded by g_cursor_lock: ARGB8888 snapshot */ -static uint32_t g_cursor_rb[CURSOR_MAX_PX]; /* compositor thread: GPU readback staging */ -static int g_cursor_w, g_cursor_h; /* 0 = nothing to draw */ -static int g_cursor_hidden = 1; /* the client asked for no pointer */ -static int g_cursor_serial; /* bumped on every change; the app polls it */ +static struct surface *g_cursor_surface; +static struct surface *g_cursor_shown; +static int g_cursor_hx, g_cursor_hy; +static uint32_t g_cursor_px[CURSOR_MAX_PX]; +static uint32_t g_cursor_rb[CURSOR_MAX_PX]; +static int g_cursor_w, g_cursor_h; +static int g_cursor_hidden = 1; +static int g_cursor_serial; -/* The last image again, after a hide: the same surface set as the pointer once more with nothing - * newly committed to it (wlroots re-sets its cursor surface each time the pointer enters). */ static void cursor_publish_shown(void) { pthread_mutex_lock(&g_cursor_lock); - if (g_cursor_w > 0) { g_cursor_hidden = 0; g_cursor_serial++; } /* serial: the hotspot may be new */ + if (g_cursor_w > 0) { g_cursor_hidden = 0; g_cursor_serial++; } pthread_mutex_unlock(&g_cursor_lock); } @@ -536,23 +439,16 @@ static void cursor_publish_shm(struct wl_shm_buffer *shm, int hx, int hy) { 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); if (w <= 0 || h <= 0 || (int64_t)w * h > CURSOR_MAX_PX) return; - /* libwayland only promises stride >= width; each row copied here is width * 4 bytes, so a - * shorter stride would read past the end of the client's pool. */ if ((int64_t)stride < (int64_t)w * 4) return; wl_shm_buffer_begin_access(shm); cursor_publish_pixels((const uint8_t *)wl_shm_buffer_get_data(shm), w, h, (size_t)stride, hx, hy); wl_shm_buffer_end_access(shm); } -/* The serial alone, without the lock: the app asks on every pointer move, and the snapshot below - * copies up to 256 KB of pixels across JNI and holds the lock the compositor thread takes to - * change the cursor. A torn read is only a missed or an extra snapshot. */ int droiddeck_cursor_serial(void) { return __atomic_load_n(&g_cursor_serial, __ATOMIC_RELAXED); } -/* Read by the app (UI thread). out = [serial, hidden, w, h, hotspotX, hotspotY, pixels...]. - * Returns the number of ints written, or 0 if out is too small. */ int droiddeck_cursor_snapshot(int *out, int cap) { int n = 0; pthread_mutex_lock(&g_cursor_lock); @@ -571,14 +467,12 @@ int droiddeck_cursor_snapshot(int *out, int cap) { struct active_touch { struct surface *surface; int pointer_fallback; }; static struct active_touch g_touch_points[MAX_TOUCH_POINTS]; static void touch_cancel_surface(struct surface *surface); -static struct surface *g_grab; /* no-desktop fallback: surface holding the button */ -static struct surface *g_key_target; /* no-desktop fallback: last clicked surface */ -static struct surface *g_ime_click; /* last clicked program window: where text input goes */ +static struct surface *g_grab; +static struct surface *g_key_target; +static struct surface *g_ime_click; static int g_input_pipe[2] = {-1, -1}; struct input_msg { int type; int p1; int p2; int p3; int p4; }; -/* Pointer constraints (zwp_pointer_constraints_v1) and relative pointers - * (zwp_relative_pointer_manager_v1); see the "pointer constraints" section. */ struct surface; static void constraints_surface_gone(struct surface *s); static void constraints_surface_commit(struct surface *s); @@ -586,7 +480,6 @@ static void constraints_pointer_gone(struct wl_resource *pointer); static void relative_pointers_pointer_gone(struct wl_resource *pointer); static void constraints_focus_entered(struct wl_resource *target, struct wl_client *client); -/* ------------------------------------------------------------------ dmabuf buffers */ void dmabuf_buffer_unref(struct dmabuf_buffer *b) { if (!b || --b->refs > 0) return; @@ -609,7 +502,6 @@ static struct dmabuf_buffer *get_dmabuf(struct wl_resource *buffer) { return NULL; } -/* ------------------------------------------------------------------ surface state */ static void surface_size(const struct surface *s, int *w, int *h) { if (s->dst_set) { *w = s->dst[0]; *h = s->dst[1]; } @@ -617,7 +509,6 @@ static void surface_size(const struct surface *s, int *w, int *h) { else { *w = s->buf_w; *h = s->buf_h; } } -/* "Title" (program) for the log; subsurfaces are named after the window they belong to. */ static void describe(const struct surface *s, char *out, size_t size) { const struct surface *w = s; while (w->parent) w = w->parent; @@ -636,8 +527,6 @@ static struct surface *toplevel_by_hwnd(uint32_t hwnd) { static void raise_kept_on_top(void); -/* Reorder the mapped toplevels to match the last reported Windows z-order. Windows the - * report doesn't mention keep their relative order below the ones it does. */ static void apply_zorder(void) { for (size_t i = g_zorder_count; i-- > 0;) { struct surface *s = toplevel_by_hwnd(g_zorder[i]); @@ -648,7 +537,6 @@ static void apply_zorder(void) { raise_kept_on_top(); } -/* Windows marked keep_on_top go back to the top of the stack (the end of g_toplevels). */ static void raise_kept_on_top(void) { struct surface *s, *tmp, *kept[4]; int n = 0; @@ -669,11 +557,8 @@ static void map_toplevel(struct surface *s) { droiddeck_log("window", "opened %s %dx%d at %d,%d%s", name, w, h, s->x, s->y, s->placed ? "" : " (no desktop position yet)"); } - wl_list_insert(g_toplevels.prev, &s->toplevel_link); /* new windows start on top */ + wl_list_insert(g_toplevels.prev, &s->toplevel_link); raise_kept_on_top(); - /* Without a Wine desktop the clients are whole programs (the Linux desktop, a gamescope a - * program on it started over it): a window that opens on top takes the keys too, as it would - * on any desktop, rather than the one clicked last that is now underneath it. */ if (!g_desktop) g_key_target = s; apply_zorder(); } @@ -692,14 +577,9 @@ static void unmap_toplevel(struct surface *s) { droiddeck_text_input_refocus(); } -/* Let go of the surface's dmabuf content. paced = 1: the buffer was replaced, give it back on the - * limiter's cadence; paced = 0: the surface is going away, give it back at once (a buffer of a - * destroyed surface is not shown again and must not sit unreleased with the client). */ static void drop_dmabuf(struct surface *s, int paced) { if (s->dmabuf) { wl_list_remove(&s->dmabuf_destroy.link); - /* A buffer on the zero-copy layer is the display's until SurfaceFlinger says otherwise: - * ahb_swapchain.c releases it then. */ if (!ahb_swapchain_defer_release(s->dmabuf_buf, s->dmabuf, s, paced)) { if (paced) release_buffer(s, s->dmabuf, now_ns()); else wl_buffer_send_release(s->dmabuf); @@ -710,8 +590,6 @@ static void drop_dmabuf(struct surface *s, int paced) { s->dmabuf_buf = NULL; } -/* The client destroyed the wl_buffer we are showing (a swapchain rebuild): keep showing its - * image (s->dmabuf_buf holds it) until the next commit replaces it; only the resource is gone. */ static void on_dmabuf_destroyed(struct wl_listener *l, void *data) { struct surface *s = wl_container_of(l, s, dmabuf_destroy); wl_list_remove(&s->dmabuf_destroy.link); @@ -737,7 +615,6 @@ static void fire_frames(struct wl_list *frames) { } } -/* Copy a committed wl_shm buffer into the surface's image and release the buffer. */ static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_resource *buffer) { 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); @@ -759,10 +636,9 @@ static void take_shm(struct surface *s, struct wl_shm_buffer *shm, struct wl_res s->has_content = s->shm_img != NULL; g_stat_shm++; struct client_info *ci = client_info_of(wl_resource_get_client(s->resource)); - if (ci && ci->asked_feedback) ci->shm_frames++; /* since it asked for GPU buffers */ + if (ci && ci->asked_feedback) ci->shm_frames++; } -/* A single-pixel buffer (wl_single_pixel.c) as a 1x1 image, which the surface's viewport stretches. */ static void take_pixel(struct surface *s, const uint8_t bgra[4], struct wl_resource *buffer) { if (s->shm_img && (vkp_image_width(s->shm_img) != 1 || vkp_image_height(s->shm_img) != 1)) { vkp_image_destroy(s->shm_img); @@ -777,29 +653,14 @@ static void take_pixel(struct surface *s, const uint8_t bgra[4], struct wl_resou s->has_content = s->shm_img != NULL; } -/* The window the app's performance HUD follows: the latest one at least as big as the last to - * start presenting GPU frames (take_dmabuf) (X11 binds the HUD to the _MESA_DRV window and counts X presents instead). JNI upcalls. */ static struct surface *g_hud_surface; -/* When the HUD's window last presented a new frame (now_ns), and when it last committed at all, - * a repeat of the frame it shows included. A replacement may take over only once it has gone - * quiet in the second sense: gamescope repeats a paused game's frame under a busy overlay, and - * that overlay must not become the game. */ static int64_t g_hud_last_ns, g_hud_alive_ns; -/* That window committed a frame the screen has not drawn yet. */ static int g_hud_fresh; -extern void droiddeck_on_game_surface(const char *window, const char *gpu); /* window NULL = gone */ +extern void droiddeck_on_game_surface(const char *window, const char *gpu); extern void droiddeck_on_game_frame(void); -/* The program behind that window: its Linux pid (the Wayland client's credentials) and executable name - * ("" when /proc gave none) - the app arms its CPU affinity on it (X11 does that from window events). */ extern void droiddeck_on_game_program(int pid, const char *program); static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_resource *buffer) { - /* The buffer this surface already shows, committed again. Gamescope's Wayland backend attaches - * every plane on every frame it outputs, so its game plane comes back unchanged whenever only - * something above it moved (MangoHud's graph, Steam's overlay). The surface still holds it and - * never released it, so the client cannot have drawn into it: it is no new frame. Counted as - * one, a game capped at 30 read 50-60 fps in the HUD, and frame generation took those repeats - * as source frames ("LSFG 2x 59 -> 54" instead of 30 -> 60). */ const int repeat = s->dmabuf == buffer && s->dmabuf_buf == b; if (!repeat) { drop_dmabuf(s, 1); @@ -820,11 +681,6 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re } } if (s->shm_img) { vkp_image_destroy(s->shm_img); s->shm_img = NULL; } - /* The size of the frames a window commits, when it changes after the first announcement: the - * program rebuilt its swapchain. It earns a line of its own, because a game whose swapchain - * came back smaller than it was is exactly what a picture suddenly filling one corner of the - * screen looks like from here - and the two sizes are the whole of the evidence. Rare by - * nature, so there is nothing to rate-limit. */ if (s->announced_vulkan && s->buf_w && s->buf_h && (s->buf_w != b->width || s->buf_h != b->height)) { char name[160]; @@ -843,8 +699,6 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re s->announced_vulkan = 1; describe(s, name, sizeof(name)); if (b->img) - /* Imported for the copy path. Whether its frames ALSO go on the display layer without a copy - * is decided per frame (fullscreen, zero-copy on) and counted in the 10 s lines. */ droiddeck_log("vulkan", "%s is presenting GPU frames through Wayland: %dx%d, format %c%c%c%c, %s (%s)", name, b->width, b->height, b->format & 0xff, (b->format >> 8) & 0xff, (b->format >> 16) & 0xff, (b->format >> 24) & 0xff, @@ -859,13 +713,6 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re droiddeck_log("error", "could not import GPU frames from %s (%dx%d, modifier %#llx)", name, b->width, b->height, (unsigned long long)b->modifier); } - /* The HUD follows the window whether its frames are copied or go straight to the layer: a - * zero-copy frame the compositor never imported is still a presented game frame. A - * program's small surfaces - gamescope's 1x1 cursor, Qt's menus and tooltips (melonDS) - - * came after the game's and drew too rarely to count, which left the HUD on 0 fps while the - * game ran at 60. So a new window takes the HUD only when it is at least as big (a - * full-screen gamescope over the desktop still does), and a window gone quiet for a second - * gives way to whatever draws next. */ if (s != g_hud_surface && (b->img || ahb_swapchain_has_ahb(b)) && (!g_hud_surface || now_ns() - g_hud_alive_ns > 1000000000LL || (announced_now && (int64_t)b->width * b->height >= (int64_t)g_hud_surface->buf_w * g_hud_surface->buf_h))) { @@ -876,8 +723,6 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re struct client_info *ci = client_info_of(wl_resource_get_client(s->resource)); if (ci) droiddeck_on_game_program((int)ci->pid, strncmp(ci->name, "pid ", 4) ? ci->name : ""); } - /* HDR session (gate open): a DXVK game switches to HDR by REBUILDING its swapchain on the same - * surface, so the one-shot line above never sees the 10-bit buffers - name every format change. */ if (b->format != s->hdr_fmt_logged && droiddeck_color_hdr_open()) { char name[160]; uint32_t af = ahb_swapchain_ahb_format(b); @@ -892,17 +737,12 @@ static void take_dmabuf(struct surface *s, struct dmabuf_buffer *b, struct wl_re : "the compositor's driver could NOT import it (nothing can show it)"); s->hdr_fmt_logged = b->format; } - /* Every buffer that reaches take_dmabuf is a presented GPU frame, so the HUD counts it. - * Asking whether the compositor could import it, as this used to, is a question about the - * copy path and not about whether a frame happened: a game whose buffers go straight to - * the display layer draws on screen while the counter sat at 0.0 fps and 1000.0 ms. */ 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; droiddeck_on_game_frame(); } } } -/* ---- hooks for ahb_swapchain.c (zero-copy layers) */ 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; } @@ -918,7 +758,6 @@ const struct droiddeck_color *droiddeck_surface_color(const struct surface *s) { return s ? droiddeck_color_of(s->resource) : NULL; } -/* ------------------------------------------------------------------ wl_surface */ static void surface_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); @@ -941,9 +780,6 @@ static void frame_callback_destroy(struct wl_resource *r) { wl_list_remove(wl_resource_get_link(r)); } -/* wp_presentation: tells Mesa's Vulkan driver when a frame reached the screen or was replaced - * before it did. Without it the driver waits for a frame callback (one per refresh) for every - * frame, which paced games to the screen rate even in mailbox mode. */ static void feedback_resource_destroy(struct wl_resource *r) { wl_list_remove(wl_resource_get_link(r)); } @@ -956,20 +792,12 @@ static void feedback_discard_all(struct wl_list *list) { } } -/* The current mode's refresh interval in ns, from the rate the app read off the display. */ static uint32_t nominal_refresh_ns(void) { int mhz = g_output_refresh_mhz; if (mhz <= 0) mhz = 60000; return (uint32_t)(1000000000000LL / (int64_t)mhz); } -/* The vblank a scene drawn at render_ns belongs to, for wp_presentation: the vsync of the tick that - * drew it (Choreographer's frame time). Gamescope's Wayland backend anchors its vblank clock to this - * and wakes a red zone before the next refresh. Reporting the end of the render instead (a few ms - * past the tick) put its wakeups on top of the next tick: some frames missed it and showed twice, - * others were replaced before they were shown (a 60 Hz Thor got ~50 distinct frames a second from - * gamescope). Not the refresh after it either: gamescope then wakes a whole refresh late and runs - * at half rate. Without a fresh tick (the fallback timer) it is the render time. */ static int64_t presented_ns(int64_t render_ns) { if (!g_tick_vsync_ns || render_ns - g_last_vsync_ns > (int64_t)nominal_refresh_ns() || render_ns < g_tick_vsync_ns) @@ -982,12 +810,6 @@ static void feedback_present_all(struct wl_list *list, int64_t t) { uint64_t sec = (uint64_t)(t / 1000000000LL); uint32_t nsec = (uint32_t)(t % 1000000000LL); wl_resource_for_each_safe(fb, tmp, list) { - /* The protocol's "refresh" is the NOMINAL interval of the current mode, a constant - not - * the interval this frame happened to take. Reporting the measured average instead makes a - * variable-refresh panel look like a mode change on every frame: gamescope re-derives its - * pacing each time ("Changed refresh to: 119.2 ... 121.4hz", 826 times in a minute on a - * Galaxy Z Fold, none on a fixed-refresh device). Report the panel's rate, which the app - * gives us and wl_output already advertises. */ wp_presentation_feedback_send_presented(fb, (uint32_t)(sec >> 32), (uint32_t)sec, nsec, nominal_refresh_ns(), 0, 0, WP_PRESENTATION_FEEDBACK_KIND_VSYNC); @@ -1023,12 +845,8 @@ static void surface_frame(struct wl_client *c, struct wl_resource *r, uint32_t c wl_resource_set_implementation(callback, NULL, NULL, frame_callback_destroy); wl_list_insert(s->pending_frames.prev, wl_resource_get_link(callback)); } -/* The bounding box serves pointer confinement. The exact flag also tells opaque-region handling - * whether the bounding box itself is the whole region. */ struct region { int set, exact; int x, y, w, h; }; -/* A wl_surface's opaque region is pending until its next commit. An exact region that covers - * the surface lets the blit path keep handling an otherwise alpha-capable buffer. */ static void surface_set_opaque(struct wl_client *c, struct wl_resource *r, struct wl_resource *region) { struct surface *s = wl_resource_get_user_data(r); @@ -1049,9 +867,6 @@ static void surface_set_input(struct wl_client *c, struct wl_resource *r, static void send_toplevel_configure(struct surface *s); -/* The pointer image from a cursor surface's buffer: wl_shm is copied, a dma-buf (labwc on a GPU - * renderer) is read back once the client's render into it is done - its implicit fence, as the - * zero-copy path waits for it (ahb_swapchain_present). */ static void cursor_publish_buffer(struct surface *s, struct wl_resource *buffer) { struct dmabuf_buffer *db = get_dmabuf(buffer); struct wl_shm_buffer *shm = db ? NULL : wl_shm_buffer_get(buffer); @@ -1084,7 +899,6 @@ static void on_idle_buffer_destroyed(struct wl_listener *l, void *data) { s->idle_buffer = NULL; } -/* Only what could be a pointer image is held; anything larger goes straight back. */ static void surface_hold_idle(struct surface *s, struct wl_resource *buffer) { struct dmabuf_buffer *db = get_dmabuf(buffer); struct wl_shm_buffer *shm = db ? NULL : wl_shm_buffer_get(buffer); @@ -1096,7 +910,6 @@ static void surface_hold_idle(struct surface *s, struct wl_resource *buffer) { wl_resource_add_destroy_listener(buffer, &s->idle_buffer_destroy); } -/* Lets go of the held buffer; release = give it back to the client (not while it is going away). */ static struct wl_resource *surface_take_idle(struct surface *s) { struct wl_resource *buffer = s->idle_buffer; if (!buffer) return NULL; @@ -1115,10 +928,6 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) { struct surface *s = wl_resource_get_user_data(r); struct surface *child; - /* xdg-shell: the initial commit of a toplevel is answered with a configure. One is also sent - * at get_toplevel, which Wine's driver has always had; a client that waits for the reply to its - * commit on a queue of its own (wlroots' Wayland backend - the desktop's labwc) can read that - * early one onto the wrong queue and would wait for ever. */ if (s->xdg_toplevel && !s->toplevel_committed) { s->toplevel_committed = 1; send_toplevel_configure(s); @@ -1145,7 +954,6 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) { struct wl_shm_buffer *shm = buffer && !db ? wl_shm_buffer_get(buffer) : NULL; uint8_t pixel[4]; - /* The previous content is replaced before reaching the screen. */ feedback_discard_all(&s->feedback); if (buffer) { @@ -1155,18 +963,15 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) { s->pending_buffer = NULL; s->pending_attach = 0; - surface_drop_idle(s); /* replaced, whatever the surface is now */ + surface_drop_idle(s); if (s->role == ROLE_CURSOR) { - /* The client's pointer image, copied out for the app's overlay (see cursor_publish_*) - * rather than composited, so it survives the zero-copy and HDR layer paths. */ if (!buffer) { - cursor_publish_hidden(); /* wlroots clears its cursor surface to hide the pointer */ + cursor_publish_hidden(); } else { cursor_publish_buffer(s, buffer); wl_buffer_send_release(buffer); } } else if (s->role == ROLE_NONE) { - /* Role-less surface: never drawn, but its buffer is kept for a set_cursor to come. */ if (buffer) surface_hold_idle(s, buffer); } else if (db) { take_dmabuf(s, db, buffer); @@ -1183,7 +988,6 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) { } } - /* Subsurface positions are applied when the parent commits. */ wl_list_for_each(child, &s->children, child_link) { if (child->sub_pending) { child->sub_x = child->sub_pending_x; @@ -1197,7 +1001,7 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) { wl_list_insert_list(s->feedback.prev, &s->pending_feedback); wl_list_init(&s->pending_feedback); constraints_surface_commit(s); - droiddeck_color_commit(s->resource); /* wp_color_management_surface_v1 state (returns at once when HDR is off) */ + droiddeck_color_commit(s->resource); if (s->role == ROLE_TOPLEVEL && s->xdg_toplevel) { if (s->has_content) map_toplevel(s); @@ -1278,7 +1082,6 @@ static void surface_resource_destroy(struct wl_resource *r) { schedule_render(); } -/* ------------------------------------------------------------------ wl_region */ static void region_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void region_add(struct wl_client *c, struct wl_resource *r, @@ -1310,7 +1113,6 @@ static const struct wl_region_interface region_impl = { .subtract = region_subtract, }; -/* ------------------------------------------------------------------ wl_compositor */ static void compositor_create_surface(struct wl_client *c, struct wl_resource *r, uint32_t id) { struct surface *s = calloc(1, sizeof(*s)); @@ -1344,8 +1146,6 @@ static void bind_compositor(struct wl_client *c, void *data, uint32_t ver, uint3 wl_resource_set_implementation(r, &compositor_impl, NULL, NULL); } -/* --------------------------------------------------------------- wl_subcompositor - * winewayland puts a window's Vulkan/GL swapchain in a subsurface over the window. */ static void subsurface_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void subsurface_set_position(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y) { @@ -1358,7 +1158,6 @@ static void subsurface_set_position(struct wl_client *c, struct wl_resource *r, static void restack_child(struct surface *s, struct surface *sibling, int above) { if (!s || !s->parent || !sibling) return; if (sibling == s->parent) { - /* Relative to the parent: directly above it or directly below it. */ s->below_parent = !above; wl_list_remove(&s->child_link); if (above) wl_list_insert(&s->parent->children, &s->child_link); @@ -1415,7 +1214,7 @@ static void subcompositor_get_subsurface(struct wl_client *c, struct wl_resource s->parent = p; s->below_parent = 0; s->sub_x = s->sub_y = 0; - wl_list_insert(p->children.prev, &s->child_link); /* new subsurfaces go on top */ + wl_list_insert(p->children.prev, &s->child_link); } static const struct wl_subcompositor_interface subcompositor_impl = { .destroy = subcompositor_destroy, @@ -1426,9 +1225,6 @@ static void bind_subcompositor(struct wl_client *c, void *data, uint32_t ver, ui wl_resource_set_implementation(r, &subcompositor_impl, NULL, NULL); } -/* ---------------------------------------------------------------- wp_viewporter - * winewayland crops window buffers to the window size and scales swapchains to the - * client area with viewports, so they must be honoured for things to line up. */ static void viewport_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void viewport_set_source(struct wl_client *c, struct wl_resource *r, @@ -1479,18 +1275,12 @@ static void bind_viewporter(struct wl_client *c, void *data, uint32_t ver, uint3 wl_resource_set_implementation(r, &viewporter_impl, NULL, NULL); } -/* ------------------------------------------------------------------ xdg_shell */ static void xdg_toplevel_destroy_req(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void xdg_toplevel_noop_parent(struct wl_client *c, struct wl_resource *r, struct wl_resource *p) {} static void xdg_toplevel_set_title(struct wl_client *c, struct wl_resource *r, const char *title) { struct surface *s = wl_resource_get_user_data(r); if (!s) return; - /* A window already on screen being renamed is something else taking it over: gamescope keeps - * one toplevel and retitles it as the game takes the screen. Unlogged, that left the log - * showing a window "closed" under a name it had never been logged as opening - which reads - * like a missing event and is really a rename. A title set before the window is mapped needs - * no line: the "opened" line carries it. */ int announce = s->mapped && title && *title && (!s->title || strcmp(s->title, title) != 0); char before[160]; if (announce) describe(s, before, sizeof(before)); @@ -1504,9 +1294,6 @@ static void xdg_toplevel_set_title(struct wl_client *c, struct wl_resource *r, c } static void xdg_toplevel_set_app_id(struct wl_client *c, struct wl_resource *r, const char *id) { struct surface *s = wl_resource_get_user_data(r); - /* Cardboard mode's SteamVR window (tools/vr/src/presenter.cpp) stays above every other window, - * gamescope's included: gamescope reopens its window when Steam notices VR, and a reopened window - * would otherwise come up over it (new windows start on top). */ if (s && id && !strcmp(id, "droiddeck-cardboard-vr")) s->keep_on_top = 1; } static void xdg_toplevel_show_menu(struct wl_client *c, struct wl_resource *r, struct wl_resource *seat, @@ -1517,10 +1304,6 @@ static void xdg_toplevel_resize(struct wl_client *c, struct wl_resource *r, stru uint32_t serial, uint32_t edges) {} static void xdg_toplevel_set_i32(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) {} static void xdg_toplevel_noop(struct wl_client *c, struct wl_resource *r) {} -/* The output's size; active, and fullscreen when asked. gamescope waits for the fullscreen - * state before it drops its libdecor frame, and never draws until it arrives; it was told the - * same size on its command line. A nested wlroots compositor (labwc) has no other way to learn - * it: given 0x0 it opens its default 1280x720 and the whole desktop is upscaled from that. */ static void send_toplevel_configure(struct surface *s) { struct wl_array states; wl_array_init(&states); @@ -1586,7 +1369,6 @@ static void xdg_surface_get_toplevel(struct wl_client *c, struct wl_resource *r, s->xdg_toplevel = tl; send_toplevel_configure(s); } -/* A popup is dismissed as soon as it is made (see the positioner below). */ static void popup_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void popup_grab(struct wl_client *c, struct wl_resource *r, struct wl_resource *seat, uint32_t serial) {} static void popup_reposition(struct wl_client *c, struct wl_resource *r, struct wl_resource *positioner, @@ -1620,11 +1402,6 @@ static void xdg_surface_resource_destroy(struct wl_resource *r) { static void xdg_wm_base_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } -/* Popups are not drawn here, but their requests must still land somewhere: a resource without an - * implementation makes libwayland abort on the first request to it - the whole compositor, and the - * app with it. A positioner takes every request and keeps nothing; a popup is dismissed at once - * (popup_done), which is what a client does with a popup it cannot show. Seen with a gamescope - * started from the Linux desktop beside labwc. */ static void positioner_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void positioner_set_size(struct wl_client *c, struct wl_resource *r, int32_t w, int32_t h) {} static void positioner_set_anchor_rect(struct wl_client *c, struct wl_resource *r, int32_t x, int32_t y, @@ -1674,7 +1451,6 @@ static void bind_xdg_wm_base(struct wl_client *c, void *data, uint32_t ver, uint wl_resource_set_implementation(r, &xdg_wm_base_impl, NULL, NULL); } -/* ------------------------------------------------------------------ banner_desktop_v1 */ static void desktop_destroy_req(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void desktop_set_desktop(struct wl_client *c, struct wl_resource *r, struct wl_resource *surface) { @@ -1723,14 +1499,10 @@ static void bind_desktop(struct wl_client *c, void *data, uint32_t ver, uint32_t wl_resource_set_implementation(r, &desktop_impl, NULL, NULL); } -/* ------------------------------------------------------------------ wl_output */ -/* wl_output v3+ has a request (release); without an implementation the first client to send it - * aborted the compositor. */ static void output_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static const struct wl_output_interface output_impl = { .release = output_release }; -/* Every bound wl_output, so a resized output (a foldable opening) can send its new mode. */ static struct wl_list g_outputs; static void output_resource_destroy(struct wl_resource *r) { wl_list_remove(wl_resource_get_link(r)); } @@ -1758,7 +1530,6 @@ static void bind_output(struct wl_client *c, void *data, uint32_t ver, uint32_t } } -/* ------------------------------------------------------------------ rendering */ struct draw_list { struct vkp_draw *d; int n, cap; }; @@ -1768,21 +1539,15 @@ static struct vkp_image *surface_image(struct surface *s) { return s->dmabuf_buf ? s->dmabuf_buf->img : NULL; } -/* HDR (gate open) only: the scene's HDR surface whose current frame is one of the game's gralloc buffers - * that the compositor's own driver could NOT import (a 10-bit UBWC layout it does not take, say). Such a - * frame has no draw, so neither layer_candidate() nor ahb_layer_only_candidate() (toplevels only) can - * find it - and a Vulkan game's swapchain is a SUBSURFACE of its window. Remembered here while the scene - * is built so it can still go on the display layer, which never needed the import. Reset per scene; - * never set while the HDR gate is closed. */ static struct surface *g_hdr_unimported; -static int g_hdr_unimported_rect[4], g_hdr_unimported_below; /* scene x,y,w,h; draws under it */ +static int g_hdr_unimported_rect[4], g_hdr_unimported_below; static void note_hdr_unimported(const struct draw_list *dl, struct surface *s, int ox, int oy) { if (!s->dmabuf_buf || s->dmabuf_buf->img || !ahb_swapchain_has_ahb(s->dmabuf_buf)) return; const struct droiddeck_color *c = droiddeck_color_of(s->resource); if (!c || !c->dataspace) return; if (s->src_set && (s->src[0] != 0 || s->src[1] != 0 || (int)(s->src[2] + 0.5f) != s->buf_w || - (int)(s->src[3] + 0.5f) != s->buf_h)) return; /* cropped: the layer shows whole buffers */ + (int)(s->src[3] + 0.5f) != s->buf_h)) return; int dw, dh; surface_size(s, &dw, &dh); g_hdr_unimported = s; @@ -1791,8 +1556,6 @@ static void note_hdr_unimported(const struct draw_list *dl, struct surface *s, i g_hdr_unimported_below = dl->n; } -/* A client that describes opaque regions can intentionally leave part of an alpha buffer see-through. - * Gamescope uses this for its full-size Steam notification and overlay planes. */ static int surface_translucent(const struct surface *s, int w, int h) { if (!s->buf_alpha) return 0; const struct client_info *ci = client_info_of(wl_resource_get_client(s->resource)); @@ -1836,8 +1599,6 @@ static void add_tree(struct draw_list *dl, struct surface *s, int ox, int oy, in if (!c->below_parent) add_tree(dl, c, ox + c->sub_x, oy + c->sub_y, depth + 1); } -/* Scene size: the desktop's, or without one the largest window's (a single - * fullscreen game then fills the screen exactly as before). */ static void scene_size(int *w, int *h) { struct surface *s; long long best = 0; @@ -1863,13 +1624,6 @@ static void fire_all_frames(void) { wl_list_for_each(s, &g_surfaces, link) fire_frames(&s->frames); } -/* With no output window (the app is in the background, or the SurfaceView is being rebuilt) - * nothing gets drawn, but the clients must not be left waiting on us. A FIFO swapchain blocks - * in its present until its wp_presentation feedback and frame callback arrive, and a client - * blocked there stops pumping messages: winhandler.exe then wedges behind it and the app - * concludes the program has exited and closes the whole session. (DXVK's mailbox presents - * never wait, which is why only OpenGL/Zink clients died on a background/resume.) Answer every - * pending frame and feedback as discarded, and keep doing so until a window is back. */ static void pace_without_output(void) { struct surface *s; wl_list_for_each(s, &g_surfaces, link) { @@ -1881,19 +1635,15 @@ static void pace_without_output(void) { static int on_frame_timer(void *data) { pace_without_output(); - if (vkp_has_window()) schedule_render(); /* it is back: draw the current scene */ + if (vkp_has_window()) schedule_render(); else if (g_frame_timer) wl_event_source_timer_update(g_frame_timer, 16); return 0; } -/* Layer mode: the index of a draw that shows a whole client GPU frame over the whole scene (one - * fullscreen game), or -1. Only the top two positions are looked at: the game must be the topmost - * draw, or have exactly ONE draw above it, which then goes on the overlay layer (sc_layer.h). - * Anything deeper would need more display layers than HWC will compose. */ static int layer_candidate(const struct draw_list *dl, int scene_w, int scene_h) { for (int i = dl->n - 1; i >= 0 && i >= dl->n - 2; i--) { const struct vkp_draw *d = &dl->d[i]; - if (d->blend) continue; /* the translucent plane must not replace the opaque game layer */ + if (d->blend) continue; if (!vkp_image_is_dmabuf(d->img)) continue; if (d->dx != 0 || d->dy != 0 || d->dw != scene_w || d->dh != scene_h) continue; if (d->sx != 0 || d->sy != 0 || (int)d->sw != vkp_image_width(d->img) || @@ -1903,7 +1653,6 @@ static int layer_candidate(const struct draw_list *dl, int scene_w, int scene_h) return -1; } -/* Zero-copy: the surface whose current GPU frame `img` is (layer_candidate found the draw). */ static struct surface *surface_for_image(const struct vkp_image *img) { struct surface *s; wl_list_for_each(s, &g_surfaces, link) @@ -1911,9 +1660,6 @@ static struct surface *surface_for_image(const struct vkp_image *img) { return NULL; } -/* Zero-copy: the topmost window whose frame is one of the game's AHardwareBuffers that this - * renderer could NOT import (so it has no draw), covering the whole scene at 0,0 with nothing of - * its own above it: it can only be shown on the layer. NULL otherwise. */ static struct surface *ahb_layer_only_candidate(int scene_w, int scene_h) { struct surface *s; int w, h; @@ -1928,39 +1674,16 @@ static struct surface *ahb_layer_only_candidate(int scene_w, int scene_h) { return s; } -/* ---- HDR (droiddeck_color.h) in the scene - * An HDR frame is only right on the game's own display layer: everything the compositor draws itself - - * the scene blit, the effects chain, frame generation, its swapchain - is 8-bit sRGB, and a PQ-encoded - * frame pushed through it as it stands comes out washed out. So a scene with HDR in it takes one of - * three routes (hdr_plan): - * 0 the HDR game ALONE on its display layer (zero-copy, or one copy) - with at most one SDR window - * above it on the overlay layer where this display can compose that. Best: nothing is converted. - * 1 the HDR PICTURE: everything else without frame generation - screen effects on, a window above - * the game on a display that cannot take a second layer, a windowed game, zero-copy off. The whole - * scene is composed into ONE 10-bit PQ BT.2020 picture (SDR content placed at the SDR white; the - * effects run on it in 10-bit) and put on the game layer tagged BT2020_PQ (hdr_compose.h). - * 2 frame generation: its extra frames need presents of their own, so the scene is composed into PQ - * and presented through an HDR10 swapchain - or, where the screen surface offers none, - * tone-mapped to SDR (correct colours instead of washed out). - * 3 a frame the compositor could not import: only its display layer can show it, so effects and - * frame generation are skipped for it (said in the log). - * The drawer's HDR output switch (droiddeck_color_output) OFF: the same routes, tone-mapped - route 0 - * becomes route 1 (the whole scene composed, tone-mapped to sRGB, onto the game layer UNtagged), route 2 - * tone-maps into an SDR swapchain, route 3 cannot (nothing can read those frames) and stays HDR, said - * in the log. The game is told nothing; it keeps rendering HDR. - * Nothing here runs while the HDR gate is closed. */ struct hdr_scene { struct vkp_hdr_frame hf; - unsigned char *is_hdr; /* per draw (calloc'd; free after the frame) */ + unsigned char *is_hdr; int n_hdr; - const struct droiddeck_color *color; /* the topmost HDR draw's description: what the picture is tagged */ - struct surface *who; /* its surface, for the log */ - const char *why; /* route 1: why the game cannot be alone on its layer */ + const struct droiddeck_color *color; + struct surface *who; + const char *why; }; -/* The HDR surface note_hdr_unimported() saw in THIS scene, when it covers the whole scene at 0,0 and - * nothing is drawn above it: the display layer is the only place its frame can be shown. */ static struct surface *hdr_unimported_fullscreen(const struct draw_list *dl, int w, int h) { if (!g_hdr_unimported || g_hdr_unimported_below != dl->n) return NULL; if (g_hdr_unimported_rect[0] != 0 || g_hdr_unimported_rect[1] != 0 || @@ -1973,11 +1696,9 @@ static int is_hdr_surface(struct surface *s) { return c && c->dataspace; } -/* Which route this scene takes (see above); fills hs. 0 whenever the gate is closed or no HDR is drawn. */ static int hdr_plan(const struct draw_list *dl, int w, int h, int fx_on, int framegen, struct hdr_scene *hs) { memset(hs, 0, sizeof(*hs)); if (!droiddeck_color_hdr_open()) return 0; - /* A fullscreen HDR frame this compositor could not import has no draw: layer only (route 3). */ struct surface *un = hdr_unimported_fullscreen(dl, w, h); if (!un) { struct surface *lo = ahb_layer_only_candidate(w, h); @@ -1991,7 +1712,7 @@ static int hdr_plan(const struct draw_list *dl, int w, int h, int fx_on, int fra if (!is_hdr_surface(s)) continue; hs->is_hdr[i] = 1; hs->n_hdr++; - hs->color = droiddeck_color_of(s->resource); /* later draws are above: the topmost wins */ + hs->color = droiddeck_color_of(s->resource); hs->who = s; } if (!hs->n_hdr) { free(hs->is_hdr); hs->is_hdr = NULL; return 0; } @@ -2000,9 +1721,6 @@ static int hdr_plan(const struct draw_list *dl, int w, int h, int fx_on, int fra hs->hf.peak_nits = hs->color->max_cll > 0.0f ? hs->color->max_cll : hs->color->has_st2086 ? hs->color->max_lum : 1000.0f; if (framegen) return 2; - /* Route 0 when the game can be alone on its layer: no effects, zero-copy on, the fullscreen draw - * is the HDR game, and above it nothing - or one window the overlay layer can carry. Not while the - * HDR output switch is off: its frame as it stands is PQ, and only the composition can tone-map it. */ int li = g_zero_copy ? layer_candidate(dl, w, h) : -1; int over = li >= 0 ? dl->n - 1 - li : 0; if (!hs->hf.tonemap && !fx_on && li >= 0 && hs->is_hdr[li] && @@ -2016,11 +1734,10 @@ static int hdr_plan(const struct draw_list *dl, int w, int h, int fx_on, int fra return 1; } -/* One line per change of route (and of its reason, and of the HDR output switch). */ static void hdr_note_route(int route, const struct hdr_scene *hs) { static int said = -1, said_tm = -1; static const char *said_why; - const int tm = route != 0 && !droiddeck_color_output(); /* route 0 never runs with the switch off */ + const int tm = route != 0 && !droiddeck_color_output(); if (route == said && tm == said_tm && (route != 1 || hs->why == said_why)) return; int was = said; said = route; said_why = hs->why; said_tm = tm; @@ -2049,7 +1766,7 @@ static void hdr_note_route(int route, const struct hdr_scene *hs) { if (tm) droiddeck_log("color", "HDR output is switched off, but %s's HDR frames cannot be imported by the compositor, " "so nothing can tone-map them: they stay HDR on its own display layer", name); - break; /* otherwise hdr_note_precedence says it */ + break; default: if (was == 1 || was == 2) droiddeck_log("color", "%s is back on its own display layer (no composition needed)", name); @@ -2057,7 +1774,6 @@ static void hdr_note_route(int route, const struct hdr_scene *hs) { } } -/* Say when effects / frame generation start or stop being skipped for an HDR game (once per change). */ static void hdr_note_precedence(struct surface *hs, int fx_on, int framegen) { static int fx_said = -1, fg_said = -1; int fx_skip = hs && fx_on, fg_skip = hs && framegen; @@ -2081,7 +1797,6 @@ static void hdr_note_precedence(struct surface *hs, int fx_on, int framegen) { } } -/* This scene went through the copy path: count every HDR-described surface in it, with the reason. */ static void hdr_count_copied(const struct draw_list *dl, const char *reason) { for (int i = 0; i < dl->n; i++) { struct surface *s = surface_for_image(dl->d[i].img); @@ -2098,11 +1813,11 @@ static void render_scene(void) { struct surface *s; int w, h; const int64_t t_scene = now_ns(); - int copy = 0; /* this scene went through the screen swapchain (the perf line's "copy") */ + int copy = 0; g_dirty = 0; g_hud_fresh = 0; - g_hdr_unimported = NULL; /* found again while the scene is built (HDR gate open only) */ + g_hdr_unimported = NULL; wl_list_for_each(s, &g_surfaces, link) s->drawn = 0; scene_size(&w, &h); if (w != g_scene_w || h != g_scene_h) { @@ -2113,7 +1828,7 @@ static void render_scene(void) { } if (g_desktop && !g_hide_shell) add_tree(&dl, g_desktop, 0, 0, 0); wl_list_for_each(s, &g_toplevels, toplevel_link) { - if (g_desktop && !s->placed) continue; /* wait for its position */ + if (g_desktop && !s->placed) continue; if (g_hide_shell && !strcmp(client_name(wl_resource_get_client(s->resource)), "explorer.exe")) continue; add_tree(&dl, s, s->placed ? s->x : 0, s->placed ? s->y : 0, 0); } @@ -2121,13 +1836,10 @@ static void render_scene(void) { int rendered = 0; int fx_on = vkp_effects_active(); int framegen = vkp_framegen_active(); - /* Cardboard mode draws the whole panel itself, so the game never gets a display layer of its own. */ const int cardboard = vkp_cardboard_active(); - /* HDR (see hdr_plan): route 0 = the old paths below; 1 / 2 = composed; 3 = layer only, effects and - * frame generation skipped. Always 0 while the HDR gate is closed: nothing changes for that session. */ struct hdr_scene hs; int hdr_route = hdr_plan(&dl, w, h, fx_on, framegen, &hs); - const char *hdr_copy = NULL; /* why this scene's HDR frames (if any) took the copy path */ + const char *hdr_copy = NULL; if (droiddeck_color_hdr_open()) { hdr_note_route(hdr_route, &hs); hdr_note_precedence(hdr_route == 3 ? hs.who : NULL, fx_on, framegen); @@ -2138,10 +1850,10 @@ static void render_scene(void) { vkp_apply_window_request(); sc_layer_hide_overlay(); if (sc_layer_present_hdr_scene(dl.d, dl.n, &hs.hf, w, h, hs.color) == 0) { - rendered = vkp_base_black(w, h) == 0; /* black under the composed picture, presented only when it is not already */ + rendered = vkp_base_black(w, h) == 0; if (fell_back) { fell_back = 0; droiddeck_log("color", "the composed picture is on the game's display layer again"); } } else { - hdr_route = 2; /* no layer this frame: the picture goes through the swapchain instead */ + hdr_route = 2; if (!fell_back) { fell_back = 1; droiddeck_log("color", "the composed picture could not go on the game's display layer (no layer, or the " @@ -2159,25 +1871,11 @@ static void render_scene(void) { else if (rendered) hdr_copy = "the HDR composition pass is unavailable on this driver"; } if (hdr_route == 0 || hdr_route == 3) { - /* What still forces the whole scene back through the compositor pass (and so through the app's - * own swapchain), and what no longer does: - * - frame generation ALWAYS does: its extra frames each need a present of their own on - * consecutive vblanks, and a display layer latches one buffer per refresh; - * - screen effects do NOT any more when the game is alone on screen - the chain runs and its - * result is copied into the game layer's own gralloc buffer (sc_layer_present_pass), so the - * game keeps its layer and the display does the scene -> output mapping; - * - screen effects DO when a window is drawn above the game: the chain has to see the whole - * scene to look the way it does on the copy path, and here the game is only part of it. */ const int pass_on = fx_on || framegen; int li = (g_zero_copy && !framegen && !cardboard) ? layer_candidate(&dl, w, h) : -1; - int over = li >= 0 ? dl.n - 1 - li : 0; /* draws above the fullscreen game (0 or 1) */ + int over = li >= 0 ? dl.n - 1 - li : 0; const int fx_blocks = (li >= 0 && fx_on && over > 0); if (fx_blocks) { li = -1; over = 0; } - /* A window above the game needs a SECOND display layer, and on some displays that costs the - * hardware composition the first layer was worth having (sc_layer_overlay_affordable). Where it - * does, the whole scene goes down the copy path exactly as it did before the layer set existed. - * Decided HERE, before the game is committed to a layer: deciding it after the present would - * show the game layer and hide it again on every frame. */ const int ov_blocks = (li >= 0 && over > 0 && !fx_blocks && !sc_layer_overlay_affordable()); if (ov_blocks) { li = -1; over = 0; } const int blocked = framegen ? 1 : (fx_blocks ? 2 : (ov_blocks ? 3 : 0)); @@ -2194,10 +1892,7 @@ static void render_scene(void) { droiddeck_log("effects", "zero-copy resumed: the game is back on its own display layer"); } struct surface *ls = li >= 0 ? surface_for_image(dl.d[li].img) : NULL; - /* A frame this renderer could not import can only be shown on the layer, effects or not. */ if (li < 0) ls = ahb_layer_only_candidate(w, h); - /* HDR: the same, for a game's SUBSURFACE swapchain whose 10-bit frame the compositor could not - * import (NULL whenever the HDR gate is closed - nothing is ever noted then). */ if (li < 0 && !ls) ls = hdr_unimported_fullscreen(&dl, w, h); if (cardboard) ls = NULL; if (ls && pass_on && li < 0 && !g_zero_copy_fx_skip_said) { @@ -2207,24 +1902,8 @@ static void render_scene(void) { framegen ? "frame generation" : "effects"); } if (li >= 0 || ls) { - /* Layer mode: the fullscreen window goes on the game layer and (at most) one window above - * it on the overlay layer; whatever is under the game is hidden by it, so the screen - * surface only needs to be black. The game's frame goes on its layer as is when it is one - * of the game's own AHardwareBuffers (zero-copy, ahb_swapchain.c), through the effects - * chain's result when a Look is on, else through one blit into the pool. If a layer can't - * take its frame, the whole scene is drawn the usual way. */ - /* The layers go up FIRST and the base surface's black frame is presented after them. The - * order matters: a present holds an acquired swapchain image, and the acquire semaphore is - * a vblank gate - putting the effects chain or the layer transaction behind it costs a - * whole refresh (measured on the Pocket FIT: 72 fps that way, 111 fps this way, with Retro - * CRT at 1920x1080). The window request the app may have left is applied here instead, - * since vkp_render no longer runs before the layer path. */ vkp_apply_window_request(); int r = -1; - /* The game's own buffer can only go on the layer AS IS - with a Look on, the frame has to - * go through the chain first, so the raw buffer is skipped in favour of the pass (a frame - * this renderer could not import has no draw, li < 0, and stays zero-copy with the effects - * skipped, said once above). */ if (!(fx_on && li >= 0) && ls && ls->dmabuf_buf && ahb_swapchain_has_ahb(ls->dmabuf_buf)) r = ahb_swapchain_present(ls->dmabuf_buf, ls, w, h); if (r != 0 && li >= 0) @@ -2232,24 +1911,16 @@ static void render_scene(void) { : sc_layer_present(dl.d[li].img, w, h, ls ? droiddeck_surface_color(ls) : NULL); if (r == 0) { if (ls) ls->drawn = 1; - /* The one window above the game keeps the game off the copy path entirely: it goes on - * its own layer, cropped and placed by the display. */ int go[8], ov = 0; if (over == 1 && li >= 0 && vkp_map_draw(&dl.d[li + 1], go)) ov = sc_layer_present_overlay(dl.d[li + 1].img, go, dl.d[li + 1].blend) == 0 ? 1 : -1; if (ov <= 0) sc_layer_hide_overlay(); - if (ov < 0) { /* the overlay layer refused it: draw the whole scene the old way */ + if (ov < 0) { sc_layer_hide(); rendered = vkp_render(w, h, dl.d, dl.n) == 0; copy = 1; hdr_copy = "the overlay layer refused the window above it"; } else { - /* Black under the opaque layers. It is a plain black frame (no effects, no frame - * generation on it: they would only run on black), and it is presented only when the - * base surface does not already show one - entering layer mode, after a copy-path or - * frame-generation frame, after a swapchain rebuild. Every other layer frame draws and - * presents nothing here: the display layers are all that changes on screen. This loop - * is paced by the Choreographer's ticks (on_vsync), not by this present. */ rendered = vkp_base_black(w, h) == 0; } } else { @@ -2267,24 +1938,18 @@ static void render_scene(void) { : framegen ? "frame generation needs the compositor pass" : "it is not the one fullscreen window (only that one gets its own display layer)"; } - } /* routes 0 and 3 */ + } free(hs.is_hdr); if (hdr_copy && rendered && droiddeck_color_hdr_open()) hdr_count_copied(&dl, hdr_copy); if (rendered) { int64_t t = presented_ns(t_scene); g_stat_frames++; - /* A surface outside the scene (role-less, not placed yet, a hidden helper window such - * as wined3d's device window) was not shown, so its feedback is discarded rather than - * left pending: a FIFO present waits on it, and a client blocked there never commits - * again. */ wl_list_for_each(s, &g_surfaces, link) { if (s->drawn) feedback_present_all(&s->feedback, t); else feedback_discard_all(&s->feedback); } fire_all_frames(); } else { - /* No output surface yet (or it went away): keep clients paced without it, now and on a - * timer that re-arms itself until a window is back (see pace_without_output). */ pace_without_output(); if (!g_frame_timer) g_frame_timer = wl_event_loop_add_timer(wl_display_get_event_loop(g_display), @@ -2309,7 +1974,6 @@ static int on_fallback_timer(void *data) { static void schedule_render(void) { g_dirty = 1; if (!g_display) return; - /* With vsync ticks flowing the next tick renders; otherwise render on a timer. */ if (g_last_vsync_ns && now_ns() - g_last_vsync_ns < 100000000LL) return; if (g_fallback_armed) return; if (!g_fallback_timer) @@ -2320,14 +1984,10 @@ static void schedule_render(void) { wl_event_source_timer_update(g_fallback_timer, 8); } -/* With frame generation on, every scene is the engine's next source frame, so one drawn for - * another window (Steam's overlay) counted as a game frame and took its present slots. Such a - * change waits for the game's next frame, unless the game has been still for 100 ms. */ static int scene_waits_for_game(int64_t now) { return g_hud_surface && !g_hud_fresh && now - g_hud_last_ns < 100000000LL && vkp_framegen_active(); } -/* A screen refresh (Choreographer tick): draw the newest state once. */ static void on_vsync(int64_t frame_time_ns) { int64_t now = now_ns(); g_perf.ticks++; @@ -2337,25 +1997,18 @@ static void on_vsync(int64_t frame_time_ns) { } g_last_vsync_ns = now; g_tick_vsync_ns = frame_time_ns; - /* The app's surface may have been replaced or taken away since the last frame: apply that now - * (the app never waits for us), and redraw the scene onto a new one. */ if (vkp_apply_window_request()) g_dirty = 1; - /* A screen-effect setting changed (JNI, any thread): redraw so it shows on a static scene too. */ if (vkp_effects_sync()) g_dirty = 1; - /* Cardboard mode: a new head pose (or setting) moves the virtual screen, static scene or not. */ if (vkp_cardboard_pose_changed()) g_dirty = 1; if (g_dirty && !scene_waits_for_game(now)) render_scene(); } -/* ------------------------------------------------------------------ wl_seat */ static uint32_t now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000); } -/* wl_pointer.set_cursor: the client tells us its pointer image, or asks for none. A NULL surface - * means "no cursor" and is authoritative. The surface itself is snapshotted on its next commit. */ static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint32_t serial, struct wl_resource *surface, int32_t hx, int32_t hy) { if (!surface) { @@ -2366,14 +2019,11 @@ static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint3 } struct surface *s = wl_resource_get_user_data(surface); if (!s) return; - if (s->role != ROLE_NONE && s->role != ROLE_CURSOR) return; /* already has another role */ + if (s->role != ROLE_NONE && s->role != ROLE_CURSOR) return; s->role = ROLE_CURSOR; g_cursor_surface = s; g_cursor_hx = hx; g_cursor_hy = hy; - /* Its content may already be committed: wlroots' Wayland backend attaches and commits the - * cursor surface first and sets it as the pointer after, and sets it again with nothing new - * committed each time the pointer re-enters. */ struct wl_resource *buffer = surface_take_idle(s); if (buffer) { cursor_publish_buffer(s, buffer); @@ -2417,8 +2067,6 @@ static void seat_get_keyboard(struct wl_client *c, struct wl_resource *r, uint32 if (!k) { wl_client_post_no_memory(c); return; } wl_resource_set_implementation(k, &keyboard_impl, NULL, keyboard_res_destroy); if (g_nkbs < MAX_PTRS) { g_kbs[g_nkbs].kb = k; g_kbs[g_nkbs].focus = NULL; g_nkbs++; } - /* Send the xkb keymap the app extracted to $XDG_RUNTIME_DIR/keymap.xkb. Without a keymap the - * client can't interpret our evdev key codes. */ const char *rt = getenv("XDG_RUNTIME_DIR"); char path[512]; snprintf(path, sizeof(path), "%s/keymap.xkb", rt ? rt : "."); @@ -2467,9 +2115,6 @@ static struct seat_keyboard *keyboard_for(struct wl_client *client) { return NULL; } -/* A client may hold more than one wl_pointer/wl_keyboard: gamescope creates a second pair and - * reads input on its own thread, so an event sent only to the first object is never seen. - * Every delivery below walks all of the client's objects. */ #define for_each_pointer_of(client, sp) \ for (int _i = 0; _i < g_nptrs; _i++) \ if (((sp) = &g_ptrs[_i]), wl_resource_get_client((sp)->ptr) == (client)) @@ -2480,7 +2125,6 @@ static struct seat_keyboard *keyboard_for(struct wl_client *client) { for (int _i = 0; _i < g_ntouches; _i++) \ if (((st) = &g_touches[_i]), wl_resource_get_client((st)->touch) == (client)) -/* Topmost window whose scene rectangle contains the point (no-desktop fallback). */ static struct surface *toplevel_at(double x, double y) { struct surface *s; wl_list_for_each_reverse(s, &g_toplevels, toplevel_link) { @@ -2532,59 +2176,38 @@ static void keyboard_focus(struct wl_resource *target) { g_mods_depressed, 0, g_mods_locked, 0); } wl_array_release(&keys); - droiddeck_clipboard_keyboard_focus(client); /* wl_data_device selection follows focus */ + droiddeck_clipboard_keyboard_focus(client); } -/* ------------------------------------------------------------------ pointer constraints - * zwp_pointer_constraints_v1 and zwp_relative_pointer_manager_v1: what winewayland uses for - * SetCursorPos, ClipCursor and hidden-cursor (mouse-look) games. - * - * A lock freezes the pointer where it is: no wl_pointer.motion is sent while it holds, and - * every input delta (a relative event from the app, or the difference between absolute - * positions) reaches the locking program as zwp_relative_pointer_v1.relative_motion. A confine - * clamps the pointer to the surface (and the program's region inside it). Relative motion is - * also sent while unlocked, to any program holding a relative pointer. - * - * Input normally goes to the desktop surface; while a constraint holds, pointer focus moves - * to the constrained surface, so the constraining program receives the events itself - * (winewayland only enables relative motion for a window its pointer has entered). One - * constraint holds at a time; a newer request ends the older one. A lock's cursor position - * hint is applied on the surface's commit and is where the pointer ends up when the lock - * ends (winewayland's SetCursorPos: lock, hint, commit, unlock). */ struct constraint { - struct wl_list link; /* g_constraints */ - struct wl_resource *resource; /* zwp_locked_pointer_v1 or zwp_confined_pointer_v1 */ - struct wl_resource *pointer; /* the program's wl_pointer; NULL once released */ - struct surface *surface; /* NULL once the surface is gone */ + struct wl_list link; + struct wl_resource *resource; + struct wl_resource *pointer; + struct surface *surface; int is_lock; uint32_t lifetime; int active; - int defunct; /* a oneshot that ended, or surface/pointer gone */ - struct region region, pending_region; /* confine area, surface-local; unset = whole surface */ + int defunct; + struct region region, pending_region; int pending_region_set; - int hint_set, pending_hint_set; /* lock: cursor position hint, surface-local */ + int hint_set, pending_hint_set; double hint_x, hint_y, pending_hint_x, pending_hint_y; }; static struct wl_list g_constraints; static struct constraint *g_active_constraint; struct relative_pointer { - struct wl_list link; /* g_relative_pointers */ + struct wl_list link; struct wl_resource *resource; - struct wl_resource *pointer; /* the program's wl_pointer; NULL once released */ + struct wl_resource *pointer; }; static struct wl_list g_relative_pointers; -/* Last pointer position in scene coordinates (buttons and scrolls from the app's X-server - * input path arrive without one). */ static double g_ptr_x, g_ptr_y; -/* Last absolute input position: absolute input becomes deltas while the pointer is locked. */ static double g_raw_x, g_raw_y; static int g_raw_valid; -/* JNI: the app switches its touch/mouse path to deltas while a lock holds and re-syncs its - * pointer to x,y when it ends. */ extern void droiddeck_on_pointer_lock(int locked, int x, int y); static void sync_lock_notify(void) { @@ -2600,13 +2223,11 @@ static void constraint_describe(const struct constraint *k, char *out, size_t si else snprintf(out, size, "a closed window"); } -/* Scene position of the constrained surface. */ static void constraint_origin(const struct constraint *k, int *x, int *y) { *x = *y = 0; if (k->surface && k->surface != g_desktop && k->surface->placed) { *x = k->surface->x; *y = k->surface->y; } } -/* Keep x,y (scene coordinates) inside the confine area. */ static void confine_clamp(const struct constraint *k, double *x, double *y) { int ox, oy, w, h, rx = 0, ry = 0, rw, rh; if (!k->surface) return; @@ -2637,7 +2258,6 @@ static void constraint_send_state(struct constraint *k, int on) { } } -/* A constraint stops holding. tell_client = 0 when its resource is being destroyed. */ static void constraint_end(struct constraint *k, int tell_client, const char *why) { char name[160]; if (!k->active) return; @@ -2667,7 +2287,6 @@ static void constraint_activate(struct constraint *k) { k->active = 1; g_active_constraint = k; if (!k->is_lock) confine_clamp(k, &g_ptr_x, &g_ptr_y); - /* Pointer focus follows the constraint: the program gets the events itself. */ constraint_origin(k, &ox, &oy); pointer_focus(k->surface->resource, wl_fixed_from_double(g_ptr_x - ox), wl_fixed_from_double(g_ptr_y - oy)); constraint_send_state(k, 1); @@ -2687,7 +2306,6 @@ static void constraint_activate(struct constraint *k) { wl_display_flush_clients(g_display); } -/* Pointer focus entered target: a persistent constraint waiting on it takes hold again. */ static void constraints_focus_entered(struct wl_resource *target, struct wl_client *client) { struct constraint *k; if (g_active_constraint) return; @@ -2719,7 +2337,6 @@ static void constraints_pointer_gone(struct wl_resource *pointer) { } } -/* Double-buffered state (position hint, confine region) applies on the surface's commit. */ static void constraints_surface_commit(struct surface *s) { struct constraint *k; wl_list_for_each(k, &g_constraints, link) { @@ -2764,7 +2381,6 @@ static void locked_pointer_set_cursor_position_hint(struct wl_client *c, struct k->pending_hint_y = wl_fixed_to_double(y); } static void locked_pointer_set_region(struct wl_client *c, struct wl_resource *r, struct wl_resource *region) { - /* A lock's region only says where it may take hold; a lock here takes hold anywhere. */ } static const struct zwp_locked_pointer_v1_interface locked_pointer_impl = { .destroy = constraint_destroy_req, @@ -2842,7 +2458,6 @@ static void bind_pointer_constraints(struct wl_client *c, void *data, uint32_t v wl_resource_set_implementation(r, &pointer_constraints_impl, NULL, NULL); } -/* --- relative pointers --- */ static void relative_pointer_resource_destroy(struct wl_resource *r) { struct relative_pointer *rp = wl_resource_get_user_data(r); @@ -2884,8 +2499,6 @@ static void relative_pointers_pointer_gone(struct wl_resource *pointer) { if (rp->pointer == pointer) rp->pointer = NULL; } -/* relative_motion to every relative pointer of the program owning the focus (winewayland wants - * it whenever it holds one, locked or not). Returns whether any was sent. */ static int send_relative_motion(struct wl_client *client, double dx, double dy) { struct relative_pointer *rp; struct timespec ts; @@ -2904,12 +2517,7 @@ static int send_relative_motion(struct wl_client *client, double dx, double dy) return sent; } -/* ------------------------------------------------------------------ pointer events */ -/* One pointer event: a motion, then an optional button change (button 0 = none). Absolute - * x,y are scene coordinates; with `relative` they are a delta. While a lock holds the pointer - * stays put and the delta (or the change between absolute positions) becomes relative motion - * for the locking program; a confine keeps the pointer inside its area. */ static void pointer_input(double x, double y, int relative, uint32_t button, int pressed) { struct surface *target; struct constraint *k = g_active_constraint; @@ -2968,8 +2576,6 @@ static void pointer_input(double x, double y, int relative, uint32_t button, int if (wl_resource_get_version(sp->ptr) >= WL_POINTER_FRAME_SINCE_VERSION) wl_pointer_send_frame(sp->ptr); } - /* A click lands text input (the soft keyboard's IME text) on the window under the pointer: - * that's where Wine's focus goes, and winewayland routes IME updates per process. */ if (button && pressed && !(k && k->is_lock)) { struct surface *clicked = toplevel_at(g_ptr_x, g_ptr_y); if (clicked) g_ime_click = clicked; @@ -2978,24 +2584,18 @@ static void pointer_input(double x, double y, int relative, uint32_t button, int wl_display_flush_clients(g_display); } -/* Absolute motion to scene x,y, then an optional button change. */ static void pointer_event(double x, double y, uint32_t button, int pressed) { pointer_input(x, y, 0, button, pressed); } -/* A delta from the app's relative input path (Relative Mouse, captured mouse, stick-as-mouse). */ static void pointer_delta(double dx, double dy) { pointer_input(dx, dy, 1, 0, 0); } -/* A button change at the current pointer position. */ static void pointer_button(uint32_t button, int pressed) { pointer_input(0, 0, 1, button, pressed); } -/* Java touch events arrive in INPUT_SPACE over the whole output: action 0=press, 1=move, 2=release. - * Output pixels go through the inverse of the scale mode's mapping (letterbox bars, FILL crop, a - * TOP/BOTTOM half), so the touch lands on the scene pixel that is drawn under the finger. */ static void deliver_pointer(const struct input_msg *m) { int w, h, ow, oh; double x, y; @@ -3003,7 +2603,7 @@ static void deliver_pointer(const struct input_msg *m) { vkp_output_size(&ow, &oh); if (ow <= 0 || oh <= 0 || !vkp_output_to_scene((double)m->p2 * ow / INPUT_SPACE_W, (double)m->p3 * oh / INPUT_SPACE_H, &x, &y)) { - x = (double)m->p2 * w / INPUT_SPACE_W; /* nothing drawn yet: plain stretch */ + x = (double)m->p2 * w / INPUT_SPACE_W; y = (double)m->p3 * h / INPUT_SPACE_H; } if (x < 0) x = 0; @@ -3095,9 +2695,6 @@ static void touch_down(int id, double x, double y) { struct surface *target = touch_target_at(x, y); if (!target) return; struct wl_client *client = wl_resource_get_client(target->resource); - /* A down on an id that is still held means that sequence's up was lost: end it first. The - * fingers still down are counted only after that - counted before, a lost up left any_active - * set, and on the pointer path every later touch was dropped (a tap or swipe did nothing). */ if (g_touch_points[id].surface) touch_cancel_client(client); int any_active = 0; for (int i = 0; i < MAX_TOUCH_POINTS; i++) { @@ -3113,14 +2710,13 @@ static void touch_down(int id, double x, double y) { struct seat_touch *st; int has_touch = 0; for_each_touch_of(client, st) has_touch = 1; - /* One line per gesture (first finger), so a test can see where a touch went. */ if (!any_active) { char name[160]; describe(target, name, sizeof(name)); droiddeck_log("touch", "%s gets %s", name, has_touch ? "touch" : "the pointer (it takes no touch)"); } if (!has_touch) { - if (any_active) return; /* one finger drives the pointer; a second one is ignored */ + if (any_active) return; g_touch_points[id].surface = target; g_touch_points[id].pointer_fallback = 1; pointer_event(x, y, BTN_LEFT, 1); @@ -3201,7 +2797,6 @@ static void deliver_key(const struct input_msg *m) { key_event((uint32_t)m->p1, m->p2); } -/* Vertical wheel steps (negative = up) at the current pointer position. */ 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)); @@ -3214,7 +2809,6 @@ static void scroll_event(int steps) { 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) { - /* From version 8 the discrete event is replaced by value120 and must not be sent. */ 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) @@ -3226,16 +2820,15 @@ static void scroll_event(int steps) { wl_display_flush_clients(g_display); } -/* Folds a key into the modifier state; returns whether the state changed. */ static int update_modifiers(uint32_t evdev, int pressed) { static const struct { uint32_t evdev, mask; } mod_keys[] = { - { 42, 0x1 }, { 54, 0x1 }, /* Shift L/R */ - { 29, 0x4 }, { 97, 0x4 }, /* Control L/R */ - { 56, 0x8 }, { 100, 0x8 }, /* Alt L/R, both Mod1 in this keymap */ - { 125, 0x40 }, { 126, 0x40 }, /* Super L/R */ + { 42, 0x1 }, { 54, 0x1 }, + { 29, 0x4 }, { 97, 0x4 }, + { 56, 0x8 }, { 100, 0x8 }, + { 125, 0x40 }, { 126, 0x40 }, }; uint32_t depressed = g_mods_depressed, locked = g_mods_locked; - if (evdev == 58) { /* Caps Lock toggles on press */ + if (evdev == 58) { if (pressed) locked ^= 0x2; } else { int i, n = (int)(sizeof(mod_keys) / sizeof(mod_keys[0])); @@ -3252,13 +2845,7 @@ static int update_modifiers(uint32_t evdev, int pressed) { } static void key_event(uint32_t evdev, int pressed) { - /* Tracked even with nothing to deliver to, so a Shift released unseen is not left held. */ int mods_changed = update_modifiers(evdev, pressed); - /* Keys go to the program window the user last clicked (else the topmost non-shell window), - * never to Wine's desktop surface: winewayland hands each key to the hwnd of the surface that - * holds keyboard focus, and a key handed to explorer's desktop hwnd is queued to explorer's - * thread, not to the foreground program (X11 delivers keys to the focused app window, so - * this mirrors it). The desktop itself is the last resort. */ struct surface *target = NULL; struct wl_resource *ime = droiddeck_ime_target(); if (ime) target = wl_resource_get_user_data(ime); @@ -3283,10 +2870,6 @@ static void key_event(uint32_t evdev, int pressed) { wl_display_flush_clients(g_display); } -/* The output changed size under a running session (a foldable opened or closed, with the - * session following the screen). Every client that has had its first configure is told the new - * size: gamescope's Wayland backend and the desktop's KWin both resize their output to it, and - * the next buffers they commit fill the new panel instead of being letterboxed onto it. */ static void output_resized(void) { struct surface *s; struct wl_resource *r; @@ -3300,17 +2883,16 @@ static void output_resized(void) { wl_display_flush_clients(g_display); } -/* wl event-loop callback: drain queued input events written by the Android UI thread. */ static int on_input_readable(int fd, uint32_t mask, void *data) { struct input_msg m; 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; /* scene motion */ - case 3: pointer_button(m.p1, m.p2); break; /* button at pointer */ + 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: 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; /* relative motion, 1/256 px */ + case 6: pointer_delta(m.p1 / 256.0, m.p2 / 256.0); break; case 7: deliver_touch(&m); break; case 8: output_resized(); break; default: deliver_pointer(&m); break; @@ -3319,8 +2901,6 @@ static int on_input_readable(int fd, uint32_t mask, void *data) { return 0; } -/* Called from JNI (Android UI thread). Queues a pointer event; the compositor thread - * dispatches it. x/y are in INPUT_SPACE (0..1919, 0..1079) over the whole output. */ void droiddeck_wayland_send_pointer(int action, int x, int y) { if (g_input_pipe[1] < 0) return; struct input_msg m = { 0, action, x, y, 0 }; @@ -3335,7 +2915,6 @@ void droiddeck_wayland_send_touch(int action, int pointer_id, int x, int y) { (void)n; } -/* Called from JNI. Queues a key event. evdev = Linux input keycode (e.g. KEY_A=30); state 1=down 0=up. */ void droiddeck_wayland_send_key(int evdev, int state) { if (g_input_pipe[1] < 0) return; struct input_msg m = { 1, evdev, state, 0, 0 }; @@ -3343,7 +2922,6 @@ void droiddeck_wayland_send_key(int evdev, int state) { (void)n; } -/* Called from JNI after g_output_w/h changed: the compositor thread re-sends the size. */ void droiddeck_wayland_output_resized(void) { if (g_input_pipe[1] < 0) return; struct input_msg m = { 8, 0, 0, 0, 0 }; @@ -3351,7 +2929,6 @@ void droiddeck_wayland_output_resized(void) { (void)n; } -/* Called from JNI on every screen refresh (Choreographer). */ 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 }; @@ -3359,9 +2936,6 @@ void droiddeck_wayland_vsync(int64_t frame_time_ns) { (void)n; } -/* Called from JNI with the app's X-server input (on-screen controls, mouse): type 2 = motion - * to scene x,y; 3 = evdev button a pressed/released (b); 4 = a wheel steps (negative = up); - * 6 = relative motion by a,b in 1/256 pixel (the app's Relative Mouse / captured-mouse path). */ void droiddeck_wayland_send_scene_input(int type, int a, int b) { if (g_input_pipe[1] < 0) return; struct input_msg m = { type, a, b, 0, 0 }; @@ -3369,15 +2943,11 @@ void droiddeck_wayland_send_scene_input(int type, int a, int b) { (void)n; } -/* ------------------------------------------------------------------ extension module hooks - * What wl_clipboard.c / wl_text_input.c need from the scene (see droiddeck_ext.h). */ const char *droiddeck_client_name(struct wl_client *client) { return client_name(client); } struct wl_display *droiddeck_get_display(void) { return g_display; } void droiddeck_request_redraw(void) { schedule_render(); } -/* The surface text input follows: the last clicked mapped program window, else the topmost - * mapped window not owned by the desktop's process (explorer). NULL when there is none. */ struct wl_resource *droiddeck_ime_target(void) { struct surface *s; if (g_ime_click && g_ime_click->mapped) return g_ime_click->resource; @@ -3395,10 +2965,8 @@ void droiddeck_surface_scene_origin(struct wl_resource *surface, int *x, int *y) void droiddeck_inject_key(uint32_t evdev, int pressed) { key_event(evdev, pressed); } -/* ------------------------------------------------------------------ 10 s summary */ static struct wl_event_source *g_stats_timer; -/* The last window's zero-copy count, kept for the app (WaylandCompositor.nativeZeroCopyFrames). */ volatile unsigned g_zero_copy_last; static int on_stats_timer(void *data) { @@ -3408,24 +2976,16 @@ static int on_stats_timer(void *data) { unsigned zero_copy = ahb_swapchain_stats_take(); unsigned layer_frames = sc_layer_frames_take(); g_zero_copy_last = zero_copy; - /* Interpolated frames the compositor added (framegen_bridge.c). They are on screen, so they - * count there; they are NOT GPU frames from games and never inflate that number. */ unsigned generated = vkp_framegen_stats_take(); if (g_stat_frames || g_stat_dmabuf || g_stat_shm || generated) { char extra[160] = ""; int off = 0; if (g_zero_copy || zero_copy) off += snprintf(extra + off, sizeof(extra) - (size_t)off, " | %u zero-copy frames", zero_copy); - /* Frames the compositor put on a display layer through one of its OWN gralloc buffers (the - * plain layer blit, or the effects chain's result): still hardware-composed, but not - * zero-copy, so they are counted apart from the line above. */ if (layer_frames) off += snprintf(extra + off, sizeof(extra) - (size_t)off, " | %u layer frames", layer_frames); if (generated) snprintf(extra + off, sizeof(extra) - (size_t)off, " | %u generated frames", generated); droiddeck_log("stats", "last 10 s: %u frames on screen (%.1f fps) | %u GPU frames from games | %u window redraws | %d windows open%s", g_stat_frames + generated, (g_stat_frames + generated) / 10.0, g_stat_dmabuf, g_stat_shm, windows, extra); } - /* The perf line, next to the stats line and only when something happened: the compositor thread's - * time per scene and inside the driver (avg/max), what the base surface did, and how long games - * waited for their buffers back. See the audit memory for how to read it. */ { struct vkp_perf vp; vkp_perf_take(&vp); @@ -3448,10 +3008,10 @@ static int on_stats_timer(void *data) { memset(&g_perf, 0, sizeof(g_perf)); } g_stat_frames = g_stat_dmabuf = g_stat_shm = 0; - droiddeck_color_stats_tick(); /* HDR evidence for the same window (nothing when the HDR gate is closed) */ - /* A program that asked for dma-buf feedback (EGL's GPU path does, first thing) but has drawn - * only wl_shm frames since is an OpenGL program whose EGL gave up on the GPU. The software - * path it fell to has no rasteriser in the Wayland layers, so what it commits is black. */ + droiddeck_color_stats_tick(); + + + for (struct client_info *ci = g_clients; ci; ci = ci->next) { if (!ci->asked_feedback || ci->dmabuf_buffers || ci->shm_gl_said || ci->shm_frames < 150) continue; @@ -3465,12 +3025,6 @@ static int on_stats_timer(void *data) { return 0; } -/* ------------------------------------------------------------------ test input - * $XDG_RUNTIME_DIR/test-input is a FIFO for scripted testing from a root shell, in scene - * (desktop) coordinates: "move X Y", "click X Y", "rclick X Y", "dclick X Y", "down X Y", - * "up X Y", "rdown X Y", "rup X Y", "rel DX DY" (relative motion, like the app's Relative - * Mouse path), "key CODE" (evdev), "keydown CODE", "keyup CODE". - * It lives in the app's private directory, so only the app and root can reach it. */ static char g_test_buf[512]; static size_t g_test_len; @@ -3520,7 +3074,7 @@ static int on_test_input(int fd, uint32_t mask, void *data) { } g_test_len = strlen(start); memmove(g_test_buf, start, g_test_len); - if (g_test_len >= sizeof(g_test_buf) - 1) g_test_len = 0; /* overlong line: drop */ + if (g_test_len >= sizeof(g_test_buf) - 1) g_test_len = 0; } return 0; } @@ -3534,14 +3088,10 @@ static void start_test_input(struct wl_event_loop *loop) { snprintf(path, sizeof(path), "%s/test-input", rt); unlink(path); if (mkfifo(path, 0600) != 0) return; - /* O_RDWR keeps a writer open ourselves, so a closing test shell never leaves us at EOF. */ if ((fd = open(path, O_RDWR | O_NONBLOCK | O_CLOEXEC)) < 0) return; wl_event_loop_add_fd(loop, fd, WL_EVENT_READABLE, on_test_input, NULL); } -/* ------------------------------------------------------------------ entry - * Blocks in the wl event loop, so the JNI wrapper runs it on a dedicated thread. - * XDG_RUNTIME_DIR must be set by the caller before this runs. */ static struct wl_listener g_client_created = { .notify = on_client_created }; @@ -3553,8 +3103,6 @@ int droiddeck_wayland_run(void) { wl_list_init(&g_relative_pointers); open_session_log(); - /* Bring the GPU up before any client can connect: loading Turnip the first time - * can take many seconds, and it must not stall a client mid-handshake. */ if (vkp_ready() != 0) WLOGE("renderer unavailable; clients will connect but nothing is drawn"); struct wl_display *display = wl_display_create(); @@ -3563,10 +3111,6 @@ int droiddeck_wayland_run(void) { return 1; } - /* Use a FIXED socket name so it always matches the guest's WAYLAND_DISPLAY=wayland-0 - * (wl_display_add_socket_auto would drift to wayland-1/2/… if a stale socket exists, - * and the guest only ever looks for wayland-0). Unlink any stale socket+lock first so - * a previous run that didn't clean up can't block the bind. */ const char *rt = getenv("XDG_RUNTIME_DIR"); if (rt && *rt) { char p[512]; @@ -3584,33 +3128,30 @@ int droiddeck_wayland_run(void) { wl_global_create(display, &wl_compositor_interface, 6, NULL, bind_compositor); wl_global_create(display, &wl_subcompositor_interface, 1, NULL, bind_subcompositor); wl_global_create(display, &wp_viewporter_interface, 1, NULL, bind_viewporter); - wl_display_init_shm(display); /* wl_shm global + pool/buffer handling */ - /* libdecor binds wl_output at 4; a lower version is a protocol error for the client. */ + wl_display_init_shm(display); + wl_global_create(display, &wl_output_interface, 4, NULL, bind_output); wl_global_create(display, &xdg_wm_base_interface, 1, NULL, bind_xdg_wm_base); wl_global_create(display, &zwp_linux_dmabuf_v1_interface, 4, NULL, bind_dmabuf); - /* gamescope's Wayland backend refuses a seat older than 8. */ wl_global_create(display, &wl_seat_interface, 9, NULL, bind_seat); wl_global_create(display, &banner_desktop_v1_interface, 1, NULL, bind_desktop); wl_global_create(display, &wp_presentation_interface, 2, NULL, bind_presentation); wl_global_create(display, &zwp_pointer_constraints_v1_interface, 1, NULL, bind_pointer_constraints); wl_global_create(display, &zwp_relative_pointer_manager_v1_interface, 1, NULL, bind_relative_pointer_manager); - droiddeck_ext_init(display); /* clipboard, text input, toplevel icons (own files, see droiddeck_ext.h) */ - ahb_swapchain_init(display); /* zero-copy layers: banner_ahb_v1, advertised whenever a display - * layer is possible; the mode event carries the live switch - * (ahb_swapchain.h) */ - droiddeck_color_init(display); /* HDR10 (opt-in): decides the gate from the app's request + the - * display + the layer path above; creates wp_color_manager_v1 - * only when it is open (droiddeck_color.h). Before any client can - * bind zwp_linux_dmabuf_v1, whose table it widens. */ + droiddeck_ext_init(display); + ahb_swapchain_init(display); + + + droiddeck_color_init(display); + + + wl_list_init(&g_pending_releases); g_release_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); if (g_release_timer_fd >= 0) g_release_source = wl_event_loop_add_fd(wl_display_get_event_loop(display), g_release_timer_fd, WL_EVENT_READABLE, on_release_timer, NULL); - /* Input injection: the Android UI thread writes events to g_input_pipe[1]; - * the wl event loop drains them on this thread. */ g_display = display; if (pipe2(g_input_pipe, O_CLOEXEC | O_NONBLOCK) == 0) { wl_event_loop_add_fd(wl_display_get_event_loop(display), g_input_pipe[0], @@ -3622,7 +3163,7 @@ 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); - wl_display_run(display); /* blocks, dispatches the event loop */ + wl_display_run(display); wl_display_destroy(display); return 0; diff --git a/app/src/main/cpp/waylandcomp/src/compositor_internal.h b/app/src/main/cpp/waylandcomp/src/compositor_internal.h index 0468f007f..0e4229d0a 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor_internal.h +++ b/app/src/main/cpp/waylandcomp/src/compositor_internal.h @@ -1,8 +1,3 @@ -/* - * What compositor.c shares with the protocol modules split out of it (wl_dmabuf.c): the dma-buf - * buffer records, the per-client bookkeeping and the few functions and globals both sides use. - * Everything declared here is hidden: it is internal to libdroiddeckwayland, not part of its ABI. - */ #pragma once #include <stdint.h> #include <sys/types.h> @@ -16,7 +11,6 @@ #define WLOGE(...) __android_log_print(ANDROID_LOG_ERROR, "DroidDeckWayland", __VA_ARGS__) #endif -/* ------------------------------------------------------------------ dmabuf buffers */ #define FOURCC(a, b, c, d) \ ((uint32_t)(a) | ((uint32_t)(b) << 8) | ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24)) @@ -41,39 +35,29 @@ struct dmabuf_buffer { int32_t width, height; uint32_t format; uint64_t modifier; - struct vkp_image *img; /* imported once, reused for every frame */ + struct vkp_image *img; int import_failed; - /* One reference for the wl_buffer resource, one per surface showing the buffer. Mesa destroys - * a swapchain's wl_buffers the moment the game rebuilds its swapchain, i.e. while the last - * committed one is still what is on screen: the import (and the dma-buf memory it pins) stays - * until the surface commits something newer, so the picture never blinks to black. */ int refs; - void *ahb_state; /* zero-copy: ahb_swapchain.c's record (the game's AHardwareBuffer) */ + void *ahb_state; }; -/* Which program each Wayland client is (from /proc/<pid>/cmdline), for the session log. */ struct client_info { struct wl_client *client; struct wl_listener destroy; pid_t pid; char name[64]; - /* An OpenGL program whose EGL gave up on the GPU: it asked for dma-buf feedback (EGL's - * Wayland GPU path always does), never made a dma-buf buffer, and draws wl_shm frames. */ unsigned asked_feedback : 1, shm_gl_said : 1; - /* Clients that describe opaque regions can use alpha outside those regions. Wine windows - * never describe them, and their unused alpha channel must not make them translucent. */ unsigned declares_opaque : 1; unsigned dmabuf_buffers, shm_frames; struct client_info *next; }; -/* ---- compositor.c */ COMPOSITOR_INTERNAL struct client_info *client_info_of(struct wl_client *client); COMPOSITOR_INTERNAL void dmabuf_buffer_unref(struct dmabuf_buffer *b); COMPOSITOR_INTERNAL extern const struct wl_buffer_interface dbuf_buffer_impl; -extern volatile int g_ubwc; /* DROIDDECK_WAYLAND_UBWC (waylandcomp_jni.c sets it) */ -extern volatile int g_no_render_node; /* DROIDDECK_WAYLAND_NO_RENDER_NODE */ +extern volatile int g_ubwc; +extern volatile int g_no_render_node; -/* ---- wl_dmabuf.c */ -COMPOSITOR_INTERNAL extern dev_t g_main_device; /* the render node clients are told to allocate on */ + +COMPOSITOR_INTERNAL extern dev_t g_main_device; COMPOSITOR_INTERNAL void bind_dmabuf(struct wl_client *c, void *data, uint32_t ver, uint32_t id); diff --git a/app/src/main/cpp/waylandcomp/src/droiddeck_color.h b/app/src/main/cpp/waylandcomp/src/droiddeck_color.h index 9591870a1..a578301ae 100644 --- a/app/src/main/cpp/waylandcomp/src/droiddeck_color.h +++ b/app/src/main/cpp/waylandcomp/src/droiddeck_color.h @@ -1,150 +1,63 @@ #ifndef DROIDDECK_COLOR_H #define DROIDDECK_COLOR_H -/* - * HDR10 output on the Wayland backend (opt-in) - the compositor half of HDR_RECON.md Phase A (round 1) - * plus HDR-aware composition (round 2, hdr_compose.h), implemented in wl_color_mgmt.c. The game half - * already ships: Mesa's Wayland WSI in our Turnip is a wp_color_manager_v1 client and exposes - * VK_COLOR_SPACE_HDR10_ST2084_EXT as soon as a compositor advertises BT.2020 + ST 2084; DXVK takes it - * with DXVK_HDR=1 (the app exports it when the setting is on). - * - * THE GATE. Nothing here exists for a session unless all of these hold when the compositor starts: - * - the container's / game's "HDR output" setting is on, or DROIDDECK_WAYLAND_HDR=1 in their - * environment (which overrides the setting either way); - * - the display the game is on lists HDR10 among its supported HDR types (read by the app from - * android.view.Display - a property of the connector, not of the device); - * - display layers with dataspace control (ASurfaceTransaction_setBufferDataSpace, Android 10+) - * and the zero-copy global (banner_ahb_v1): the HDR frame (or the composed HDR picture) is shown - * on the game's own display layer, tagged BT2020_PQ. - * In game, the drawer's HDR output switch (droiddeck_color_set_output) flips between that and the same - * frames tone-mapped to SDR, live; the gate itself never changes during a session. - * A closed gate advertises nothing - no colour-management global, no 10-bit dma-buf formats - so the - * session is byte-for-byte what it was before this file existed, and the session log says why. - * DROIDDECK_WAYLAND_HDR=force skips the display check only (testing the negotiation on an SDR panel; - * SurfaceFlinger then tone-maps the layer, and says so in its composition type). - * - * WHAT AN OPEN GATE OFFERS (exactly the subset Mesa binds, HDR_RECON.md §2.2): wp_color_manager_v1 - * version 1 with the perceptual intent, the parametric creator + mastering-display metadata, - * BT.2020 primaries and the ST 2084 (PQ) transfer function; zwp_linux_dmabuf_v1 adds AB30/XB30 - * (A2B10G10R10, the only 10-bit layout our zero-copy WSI can put in a gralloc buffer). A surface's - * image description is double-buffered on wl_surface.commit; its frames go onto the game's display - * layer tagged BT2020_PQ with the game's SMPTE 2086 / CTA-861.3 metadata (sc_layer.c). - * - * Compositor thread unless noted. - */ #include <stdint.h> #include <wayland-server.h> -/* ADataSpace values (NDK <android/data_space.h>) the layer path emits. */ #define DROIDDECK_ADATASPACE_UNKNOWN 0 -#define DROIDDECK_ADATASPACE_BT2020_PQ 163971072 /* STANDARD_BT2020 | TRANSFER_ST2084 | RANGE_FULL = 0x09c60000 */ -#define DROIDDECK_ADATASPACE_BT2020_HLG 168165376 /* STANDARD_BT2020 | TRANSFER_HLG | RANGE_FULL */ +#define DROIDDECK_ADATASPACE_BT2020_PQ 163971072 +#define DROIDDECK_ADATASPACE_BT2020_HLG 168165376 -/* One image description as the program made it, and what it becomes on a display layer. Immutable: - * a changed description is a new record with a new identity. */ struct droiddeck_color { - uint32_t identity; /* the id the `ready` event carried (never 0) */ - uint32_t primaries, tf; /* wp_color_manager_v1 named values */ - int32_t dataspace; /* ADataSpace on a display layer; 0 = none (treated as sRGB) */ - int has_st2086; /* SMPTE ST 2086: mastering display primaries + luminance */ - float red[2], green[2], blue[2], white[2]; /* CIE 1931 xy */ - float max_lum, min_lum; /* nits */ - int has_cta861; /* CTA-861.3 */ - float max_cll, max_fall; /* nits; 0 = not given */ - char text[256]; /* the session log's one-line summary of it */ + uint32_t identity; + uint32_t primaries, tf; + int32_t dataspace; + int has_st2086; + float red[2], green[2], blue[2], white[2]; + float max_lum, min_lum; + int has_cta861; + float max_cll, max_fall; + char text[256]; }; -/* ---- app -> compositor (JNI, any thread) */ -/* mode 0 = off, 1 = DROIDDECK_WAYLAND_HDR=1, 2 = DROIDDECK_WAYLAND_HDR=force (testing). source names where - * the switch came from; dxvk_hdr = DXVK_HDR=1 is in the game's environment; zero_copy_forced = the - * app turned zero-copy presentation on for this session because HDR needs it. Before the start. */ void droiddeck_color_set_request(int mode, const char *source, int dxvk_hdr, int zero_copy_forced); -/* The display the game is on, as android.view.Display reports it. Before the start (it feeds the - * gate) and again whenever it changes (logged; the gate is decided once per session). */ void droiddeck_color_set_display(int id, const char *name, const char *formats, int hdr10, float max_lum, float max_avg, float min_lum, int ratio_available, float ratio, int api); -/* One reading of Display.getHdrSdrRatio() (API 34+; < 0 = not available). listener = it came from - * the display's ratio listener rather than the app's periodic sampler. */ void droiddeck_color_ratio_sample(float ratio, int listener); -/* Milliseconds since an HDR frame last went onto a display layer; -1 = none this session. */ int droiddeck_color_last_frame_age_ms(void); -/* -1 = the compositor has not decided yet, 0 = closed, 1 = open. */ int droiddeck_color_gate_state(void); -/* The session is ending: write the summary line ("HDR on screen: …"). */ void droiddeck_color_session_end(void); -/* 1 while HDR frames are really on screen: a frame tagged BT2020_PQ reached a display layer or the HDR - * swapchain in the last 1.5 s AND, where the display reports an HDR/SDR ratio, its last reading is - * above 1.01 (the HUD badge). */ int droiddeck_color_hdr_on_screen(void); -/* The HUD / drawer state: 0 = no HDR frames on screen (or not confirmed yet), 1 = HDR frames on screen - * with headroom (as droiddeck_color_hdr_on_screen), 2 = HDR frames on screen but the display has given - * them NO headroom (ratio <= 1.01) for 5 s or more - the brightness slider at maximum, or a screen - * recording (Android turns HDR headroom off while the screen is recorded). */ int droiddeck_color_hdr_state(void); -/* Nits SDR content is placed at inside an HDR picture (default 203, BT.2408); any thread. */ void droiddeck_color_set_sdr_white(float nits); float droiddeck_color_sdr_white(void); -/* The HDR session was asked for (DROIDDECK_WAYLAND_HDR / the setting resolved on or force) - known before - * the compositor starts, so the Vulkan instance can enable VK_EXT_swapchain_colorspace for it. */ int droiddeck_color_requested(void); -/* The drawer's live "HDR output" switch, per session, starting ON. On = HDR frames go to the display as - * HDR; off = the SAME frames are composed tone-mapped to SDR (hdr_compose.h) - the game is told nothing - * and keeps rendering HDR (DXVK_HDR and the colour-manager offer were decided at launch). Compositor - * thread (the app posts it through droiddeck_host_hdr_output); logs the flip and redraws. */ void droiddeck_color_set_output(int on); -/* 1 = HDR output on (any thread). */ int droiddeck_color_output(void); -/* 1 while an HDR game's frames are being shown tone-mapped to SDR (the last one < 1.5 s ago); any thread. */ int droiddeck_color_tonemapped_on_screen(void); -/* Device evidence beside the HDR/SDR headroom (the app, non-root APIs; any thread; HDR sessions only): - * PowerManager thermal status (0..6, -1 unknown) + getThermalHeadroom(10) (< 0 = not available), - * Settings.System SCREEN_BRIGHTNESS (0..255, -1 unknown) + SCREEN_BRIGHTNESS_MODE (1 auto, 0 manual). - * Changes of status and brightness are logged; every no-headroom line, the 10 s line and the verdict - * carry the last values. */ void droiddeck_color_env_sample(int thermal, float headroom, int brightness, int bmode); -/* ---- the explicit HDR headroom request (Android 15+: ASurfaceTransaction_setDesiredHdrHeadroom on the game - * layer, SurfaceView.setDesiredHdrHeadroom on the screen surface). Some phones only boost HDR when asked. */ -/* The ratio to ask for an HDR frame of `c`: content peak (max CLL, else mastering max, else the display's - * peak) / SDR white (203), capped at the display's highest ratio when it reports one; 0 = ask nothing - * (not HDR, or the display reports it cannot boost). `why` gets the one-line reason. Any thread. */ float droiddeck_color_desired_headroom(const struct droiddeck_color *c, char *why, size_t n); -/* What was last asked for (> 0 the ratio, 0 nothing, -1 the API is missing): for the no-headroom lines. */ void droiddeck_color_note_headroom_request(float ratio); -/* Display.getHighestHdrSdrRatio() (Android 16+; <= 0 = not reported). Any thread; logged when it arrives. */ void droiddeck_color_set_highest_ratio(float ratio); -/* The headroom the SCREEN surface should ask for (frames through the HDR10 swapchain in the last 1.5 s), - * 0 = none; `why` gets the reason. The app polls it and applies it. Any thread. */ float droiddeck_color_screen_headroom(char *why, size_t n); -/* ---- compositor.c -> here */ -/* Decide the gate (call after ahb_swapchain_init) and create the global when it is open. */ void droiddeck_color_init(struct wl_display *display); -/* The gate is open: the 10-bit dma-buf formats may be advertised, descriptions may exist. */ int droiddeck_color_hdr_open(void); -/* wl_surface.commit: the pending image description (if any request touched it) becomes current. */ void droiddeck_color_commit(struct wl_resource *surface); -/* The surface's current image description; NULL = none (sRGB, today's default). */ const struct droiddeck_color *droiddeck_color_of(struct wl_resource *surface); -/* A program disconnected: if it presented HDR, its part of the summary is written now. */ void droiddeck_color_client_gone(struct wl_client *client); -/* The 10 s summary tick (one `color` line when anything HDR happened in the window). */ void droiddeck_color_stats_tick(void); -/* A frame of an HDR-described surface went through the compositor's own 8-bit sRGB pass this scene - * (who = the window, reason = why it did not get the display layer). Shown untone-mapped. */ void droiddeck_color_frame_copied(const char *who, const char *reason); -/* ---- sc_layer.c / compositor.c -> here: a NEW frame of an HDR-described surface was shown, and how */ enum droiddeck_hdr_path { - DROIDDECK_HDR_ZERO_COPY = 0, /* the game's own gralloc buffer on its display layer, tagged */ - DROIDDECK_HDR_LAYER_COPY, /* one 8-bit copy of the game's frame on its layer, tagged */ - DROIDDECK_HDR_COMPOSED, /* the whole scene composed into one PQ picture on the game layer (hdr_compose.h) */ - DROIDDECK_HDR_SWAPCHAIN, /* composed into PQ and presented through an HDR10 swapchain (frame generation) */ - DROIDDECK_HDR_TONEMAPPED, /* composed and tone-mapped to SDR (HDR output switched off, or a present that - * cannot carry HDR) - NOT HDR */ + DROIDDECK_HDR_ZERO_COPY = 0, + DROIDDECK_HDR_LAYER_COPY, + DROIDDECK_HDR_COMPOSED, + DROIDDECK_HDR_SWAPCHAIN, + DROIDDECK_HDR_TONEMAPPED, + }; -/* ahb_format = the buffer's AHARDWAREBUFFER_FORMAT_* where one exists (0 otherwise). */ void droiddeck_color_frame_shown(const struct droiddeck_color *c, int path, uint32_t ahb_format); -/* "RGBA1010102 (10-bit)" etc. for an AHARDWAREBUFFER_FORMAT_* value (static buffer). */ const char *droiddeck_ahb_format_name(uint32_t format); #endif diff --git a/app/src/main/cpp/waylandcomp/src/droiddeck_ext.c b/app/src/main/cpp/waylandcomp/src/droiddeck_ext.c index 913f713d3..1172c095b 100644 --- a/app/src/main/cpp/waylandcomp/src/droiddeck_ext.c +++ b/app/src/main/cpp/waylandcomp/src/droiddeck_ext.c @@ -1,11 +1,3 @@ -/* - * Host → compositor message queue plus the init of the extension modules. - * - * The app's threads (UI thread, clipboard listener) hand text to the compositor through - * droiddeck_host_* ; the messages are queued under a mutex and a pipe byte wakes the wl event - * loop, which drains them on the compositor thread - the only thread that may touch - * wl_resources. - */ #define _GNU_SOURCE 1 #include <stdlib.h> #include <string.h> @@ -83,7 +75,7 @@ static int on_wake(int fd, uint32_t mask, void *data) { if (!m) break; switch (m->kind) { case HOST_CLIPBOARD: - clipboard_host_text(m->text, m->len); /* takes the buffer */ + clipboard_host_text(m->text, m->len); m->text = NULL; break; case HOST_TEXT_COMMIT: @@ -114,7 +106,7 @@ void droiddeck_ext_init(struct wl_display *display) { if (pipe2(g_wake, O_CLOEXEC | O_NONBLOCK) == 0) { wl_event_loop_add_fd(loop, g_wake[0], WL_EVENT_READABLE, on_wake, NULL); pthread_mutex_lock(&g_mu); - int queued = g_head != NULL; /* posted before the loop existed (clipboard refresh) */ + int queued = g_head != NULL; pthread_mutex_unlock(&g_mu); if (queued) { char c = 1; ssize_t n = write(g_wake[1], &c, 1); (void)n; } } else diff --git a/app/src/main/cpp/waylandcomp/src/droiddeck_ext.h b/app/src/main/cpp/waylandcomp/src/droiddeck_ext.h index 0dc8ae41a..ca359557e 100644 --- a/app/src/main/cpp/waylandcomp/src/droiddeck_ext.h +++ b/app/src/main/cpp/waylandcomp/src/droiddeck_ext.h @@ -1,53 +1,38 @@ -/* - * Interface between compositor.c and the extension modules that live in their own files: - * wl_clipboard.c wl_data_device_manager + zwlr_data_control_manager_v1 (selection only) - * wl_text_input.c zwp_text_input_manager_v3 (host IME / soft keyboard) - * wl_toplevel_icon.c xdg_toplevel_icon_manager_v1 (accepted, not rendered) - * droiddeck_ext.c the host→compositor message queue shared by the modules - * Everything here runs on the compositor (dispatch) thread unless noted. - */ #pragma once #include <stdint.h> #include <wayland-server.h> void droiddeck_log(const char *tag, const char *fmt, ...); -/* ---- compositor.c → modules */ -void droiddeck_ext_init(struct wl_display *display); /* creates the globals + host queue */ -void droiddeck_clipboard_keyboard_focus(struct wl_client *client); /* keyboard focus moved; NULL = none */ -void droiddeck_text_input_refocus(void); /* the text-input target may have changed */ + +void droiddeck_ext_init(struct wl_display *display); +void droiddeck_clipboard_keyboard_focus(struct wl_client *client); +void droiddeck_text_input_refocus(void); void droiddeck_text_input_surface_gone(struct wl_resource *surface); -/* ---- modules → compositor.c */ const char *droiddeck_client_name(struct wl_client *client); struct wl_display *droiddeck_get_display(void); -/* The surface text input follows (the last clicked window, else the topmost program window). */ struct wl_resource *droiddeck_ime_target(void); -/* Scene (virtual desktop) origin of a toplevel surface; 0,0 when unknown. */ void droiddeck_surface_scene_origin(struct wl_resource *surface, int *x, int *y); -/* Deliver a key press/release through the wl_keyboard path as if typed. */ void droiddeck_inject_key(uint32_t evdev, int pressed); -/* ---- modules → app (JNI upcalls; waylandcomp_jni.c) */ void droiddeck_on_clipboard_text(const char *utf8, int len); void droiddeck_on_text_input(int enabled, const char *program, int x, int y, int w, int h); -/* ---- app → modules (callable from any thread; queued to the compositor thread) */ -void droiddeck_host_clipboard_text(const char *utf8, int len); /* len 0 = clear */ + +void droiddeck_host_clipboard_text(const char *utf8, int len); void droiddeck_host_text_commit(const char *utf8, int len); void droiddeck_host_text_preedit(const char *utf8, int len, int cursor_begin, int cursor_end); void droiddeck_host_text_delete(int before, int after); -void droiddeck_host_zero_copy(int on, int live); /* the drawer's zero-copy switch (ahb_swapchain_set_mode) */ -void droiddeck_host_hdr_output(int on); /* the drawer's HDR output switch (droiddeck_color_set_output) */ +void droiddeck_host_zero_copy(int on, int live); +void droiddeck_host_hdr_output(int on); -/* ---- module internals reached through the queue (droiddeck_ext.c → modules) */ void clipboard_init(struct wl_display *display); -void clipboard_host_text(char *utf8, size_t len); /* takes ownership of utf8 */ +void clipboard_host_text(char *utf8, size_t len); void text_input_init(struct wl_display *display); void text_input_host_commit(const char *utf8, size_t len); void text_input_host_preedit(const char *utf8, size_t len, int cursor_begin, int cursor_end); void text_input_host_delete(int before, int after); void toplevel_icon_init(struct wl_display *display); void single_pixel_init(struct wl_display *display); -/* A wp_single_pixel_buffer_manager_v1 buffer: 1 and its pixel (B, G, R, A, premultiplied), else 0. */ int single_pixel_buffer_get(struct wl_resource *buffer, uint8_t bgra[4]); diff --git a/app/src/main/cpp/waylandcomp/src/effects_chain.c b/app/src/main/cpp/waylandcomp/src/effects_chain.c index 7e446e299..aa5e5cb9d 100644 --- a/app/src/main/cpp/waylandcomp/src/effects_chain.c +++ b/app/src/main/cpp/waylandcomp/src/effects_chain.c @@ -1,16 +1,12 @@ -/* Screen-effect chain - see effects_chain.h. Compositor thread unless noted. */ #define _POSIX_C_SOURCE 200809L #include "effects_chain.h" #include "vk_loader.h" -#include "vk_present.h" /* droiddeck_log */ +#include "vk_present.h" #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -/* The X11 Vulkan renderer's SPIR-V, pre-compiled by app/src/main/cpp/framegen/gen_shaders.sh - * (glslangValidator --vn) and committed next to the GLSL: the NDK build compiles no shaders, so - * there is no build-time tooling for CI to break. Same bytes as the X11 chain runs. */ #include "upscale_vert.h" #include "sgsr_frag.h" #include "sgsr_quality_frag.h" @@ -26,25 +22,24 @@ #include "crt_frag.h" #include "deband_frag.h" -#define FX_FORMAT VK_FORMAT_R8G8B8A8_UNORM /* the default: every SDR frame */ -/* The chain's target format and the scene's view format. R8G8B8A8 always, except while an HDR scene - * runs through the chain (hdr_compose.h): then the scene and the targets are A2B10G10R10, so a PQ - * picture keeps 10 bits through every pass. A change rebuilds the chain's objects once - * (vkp_effects_set_formats). */ +#define FX_FORMAT VK_FORMAT_R8G8B8A8_UNORM + + + + static VkFormat g_fx_fmt = FX_FORMAT, g_scene_fmt = FX_FORMAT; -#define FX_MAX_DIM 4096 /* a FILL-mapped scene can exceed the panel; cap the chain resolution */ +#define FX_MAX_DIM 4096 -/* ------------------------------------------------------------------ settings */ struct fx_settings { - int scaling; /* 0..8 */ - int sharp_pct; /* 0..100, the upscaler's own slider */ + int scaling; + int sharp_pct; int cas_on, cas_pct; int hdr_on; - int deband_on, deband_pct; /* 0..200 */ - int color_on; /* derived: grade not neutral */ - float brightness, contrast, gamma, saturation; /* shader units (already mapped/clamped) */ - float b_raw, c_raw, s_raw; /* slider units, for the log */ + int deband_on, deband_pct; + int color_on; + float brightness, contrast, gamma, saturation; + float b_raw, c_raw, s_raw; int fxaa, toon, crt, ntsc; char look[48]; }; @@ -54,7 +49,7 @@ static struct fx_settings g_set = { .scaling = 0, .sharp_pct = 75, .cas_on = 0, .cas_pct = 60, .deband_pct = 100, .gamma = 1.0f, .saturation = 1.0f, .s_raw = 100.0f, .look = "Off"}; static volatile int g_changed; -static struct fx_settings g_cur; /* the compositor thread's per-frame snapshot */ +static struct fx_settings g_cur; static int g_cur_valid; static void set_begin(void) { pthread_mutex_lock(&g_lock); } @@ -83,7 +78,6 @@ void vkp_effects_set_deband(int enabled, int pct) { static float clampf(float v, float lo, float hi) { return v < lo ? lo : v > hi ? hi : v; } void vkp_effects_set_screen(float brightness, float contrast, float gamma, float saturation, int fxaa, int toon, int crt, int ntsc) { - /* Same mapping as VulkanRendererContext::setColorGrade (GL ColorEffect clamps). */ int on = !(brightness == 0.0f && contrast == 0.0f && gamma == 1.0f && saturation == 100.0f); set_begin(); g_set.color_on = on; @@ -157,18 +151,17 @@ VkFilter vkp_effects_blit_filter(void) { return g_cur.scaling == 2 ? VK_FILTER_NEAREST : VK_FILTER_LINEAR; } -/* ------------------------------------------------------------------ push constants - * Byte-identical to VulkanRendererContext.h: vec4 ndc first (upscale.vert), the rest fragment-only. */ -struct pc_sgsr { float ndc[4]; float vp[4]; float edge; }; /* also NIS: vp + sharpness */ + + +struct pc_sgsr { float ndc[4]; float vp[4]; float edge; }; struct pc_easu { float ndc[4]; uint32_t con0[4], con1[4], con2[4], con3[4]; float outW, outH; }; struct pc_rcas { float ndc[4]; uint32_t con[4]; float outW, outH; }; -struct pc_res { float ndc[4]; float res[2]; float extra; float pad; }; /* fxaa/toon/hdr/crt (res), cas (sharpness), ntsc (frameCount), deband (strength) */ +struct pc_res { float ndc[4]; float res[2]; float extra; float pad; }; struct pc_color { float ndc[4]; float b, c, g, s; }; -#define PC_MAX_SIZE sizeof(struct pc_easu) /* 88 bytes, the largest */ +#define PC_MAX_SIZE sizeof(struct pc_easu) static uint32_t packf(float f) { uint32_t u; memcpy(&u, &f, 4); return u; } -/* Mirrors ffx_fsr1.h FsrEasuCon / FsrRcasCon exactly as the X11 renderer packs them. */ static void easu_con(struct pc_easu *e, float inW, float inH, float outW, float outH) { e->con0[0] = packf(inW / outW); e->con0[1] = packf(inH / outH); e->con0[2] = packf(0.5f * inW / outW - 0.5f); e->con0[3] = packf(0.5f * inH / outH - 0.5f); @@ -183,7 +176,6 @@ static void rcas_con(struct pc_rcas *r, float scale01) { r->con[0] = packf(scale01); r->con[1] = 0; r->con[2] = 0; r->con[3] = 0; } -/* ------------------------------------------------------------------ Vulkan objects */ struct fx_target { VkImage img; VkDeviceMemory mem; VkImageView view; VkFramebuffer fb; VkDescriptorSet ds; @@ -193,16 +185,16 @@ struct fx_target { static VkDevice g_dev; static VkPhysicalDevice g_pd; static VkPhysicalDeviceMemoryProperties g_mp; -static int g_ready; /* 0 = objects not built, 1 = ok, -1 = failed (chain disabled, said once) */ +static int g_ready; static VkRenderPass g_rp; static VkDescriptorSetLayout g_dsl; static VkPipelineLayout g_pl; static VkDescriptorPool g_dpool; -static VkSampler g_sampler; /* linear, clamp-to-edge: what the X11 chain samples its targets with */ +static VkSampler g_sampler; enum { P_SGSR, P_SGSR_HQ, P_NIS, P_EASU, P_RCAS, P_CAS, P_COLOR, P_FXAA, P_TOON, P_HDR, P_NTSC, P_CRT, P_DEBAND, P_COUNT }; static VkPipeline g_pipe[P_COUNT]; -static struct fx_target g_t[2], g_mid; /* ping-pong pair + the EASU output (FSR only) */ -/* The scene image belongs to vk_present; only its view + descriptor live here. */ +static struct fx_target g_t[2], g_mid; + static struct { VkImage img; VkImageView view; VkDescriptorSet ds; int w, h; } g_scene; static uint32_t g_ntsc_frame; @@ -249,15 +241,9 @@ static void target_destroy(struct fx_target *t) { memset(t, 0, sizeof(*t)); } -/* A chain target: rendered into (colour attachment), sampled by the next pass, and blitted to - * the swapchain when it ends up holding the result. Frames are fenced, so a resize never races - * the GPU. 0 = ok. */ static int target_ensure(struct fx_target *t, int w, int h, const char *what) { if (t->img && t->w == w && t->h == h) return 0; target_destroy(t); - /* Unrestricted mutable views keep these render-then-blit targets uncompressed on Turnip. - * UBWC targets corrupt even a constant-colour pass on Adreno 740 (FSR becomes black). - * Only the chain's intermediate images need this; the scene and guest images stay unchanged. */ VkImageCreateInfo ii = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT, .imageType = VK_IMAGE_TYPE_2D, .format = g_fx_fmt, .extent = {(uint32_t)w, (uint32_t)h, 1}, .mipLevels = 1, @@ -316,8 +302,6 @@ static VkShaderModule shader(const uint32_t *code, size_t size) { return m; } -/* One full-quad post pipeline: upscale.vert + the effect's fragment shader, opaque, dynamic - * viewport/scissor - the X11 renderer's createPostPipeline. */ static VkPipeline post_pipeline(VkShaderModule vert, const uint32_t *frag_code, size_t frag_size, const char *name) { VkShaderModule frag = shader(frag_code, frag_size); if (!frag) { droiddeck_log("error", "effects: %s: vkCreateShaderModule failed", name); return VK_NULL_HANDLE; } @@ -370,7 +354,6 @@ void vkp_effects_destroy(void) { objects_destroy(); g_dev = VK_NULL_HANDLE; } void vkp_effects_set_formats(VkFormat scene_fmt, VkFormat target_fmt) { if (target_fmt != g_fx_fmt) { - /* The render pass, the pipelines and the targets are built for one format: start over. */ if (g_ready) objects_destroy(); g_fx_fmt = target_fmt; g_scene_fmt = scene_fmt; @@ -387,14 +370,9 @@ void vkp_effects_set_formats(VkFormat scene_fmt, VkFormat target_fmt) { } } -/* Build every device object once, on the first active frame. 0 = ok, -1 = the chain is - * unavailable for this session (logged once; frames fall back to the plain blit). */ static int objects_ensure(void) { if (g_ready) return g_ready == 1 ? 0 : -1; if (!g_dev || !g_vk.CreateGraphicsPipelines) { g_ready = -1; droiddeck_log("error", "effects: no device for the chain"); return -1; } - /* Render pass: one colour target, overwritten whole (DONT_CARE), left SHADER_READ_ONLY for the - * next pass. The dependencies order this pass after whoever read the target last (previous - * pass / previous frame's blit) and before whoever samples or blits it next. */ VkAttachmentDescription att = {.format = g_fx_fmt, .samples = VK_SAMPLE_COUNT_1_BIT, .loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, .storeOp = VK_ATTACHMENT_STORE_OP_STORE, .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, @@ -465,7 +443,6 @@ fail: return -1; } -/* ------------------------------------------------------------------ recording */ static void barrier(VkCommandBuffer cmd, VkImage img, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags src_stage, VkAccessFlags src_access, @@ -477,7 +454,6 @@ static void barrier(VkCommandBuffer cmd, VkImage img, VkImageLayout from, VkImag g_vk.CmdPipelineBarrier(cmd, src_stage, dst_stage, 0, 0, NULL, 0, NULL, 1, &b); } -/* One pass: sample `src` (a descriptor in SHADER_READ_ONLY_OPTIMAL), draw the full quad into `dst`. */ static void pass(VkCommandBuffer cmd, VkPipeline p, struct fx_target *dst, VkDescriptorSet src, const void *pc, uint32_t pcsz) { VkRenderPassBeginInfo rpi = {.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, .renderPass = g_rp, .framebuffer = dst->fb, @@ -504,36 +480,26 @@ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int sce s = &g_cur; *res_w = scene_w; *res_h = scene_h; - /* The chain's resolution: the scene's mapped size (X11 runs its effects at the output extent, - * with the scene already scaled by the base sampler). Clamped so a FILL-mapped 4K scene does - * not allocate beyond reason; the final blit resizes the remainder. */ int cw = out_w > 0 ? out_w : scene_w, ch = out_h > 0 ? out_h : scene_h; if (cw > FX_MAX_DIM) cw = FX_MAX_DIM; if (ch > FX_MAX_DIM) ch = FX_MAX_DIM; if (cw < 1) cw = 1; if (ch < 1) ch = 1; - /* Spatial upscalers only have work when the scene is shown larger than it is (the X11 - * renderer's "render below display" gate); Sharpen (RCAS) runs at any size. */ const int upscaling = cw > scene_w || ch > scene_h; int spatial = 0; if (spatial_mode(s->scaling) && (s->scaling == 6 || upscaling)) spatial = s->scaling; const int nfx = effect_count(s); const int resize = cw != scene_w || ch != scene_h; - /* Passes that write the ping-pong pair: the scaling/resize stage (if any) + the effects. - * Two targets are needed once any pass reads what an earlier one wrote. */ const int npasses = ((spatial || resize) ? 1 : 0) + nfx; if ((spatial || nfx) && objects_ensure() == 0 && scene_ensure(scene, scene_w, scene_h) == 0 && target_ensure(&g_t[0], cw, ch, "chain") == 0 && (npasses < 2 || target_ensure(&g_t[1], cw, ch, "chain") == 0) && (!(spatial == 4 || spatial == 5) || target_ensure(&g_mid, cw, ch, "EASU") == 0)) { - /* `cur` = the image the next pass samples; `cur_ds` its descriptor. Every pass writes the - * target that is not being read (the pair alternates; the scene is read only first). */ VkDescriptorSet cur_ds; - struct fx_target *cur = NULL; /* NULL = the scene image */ + struct fx_target *cur = NULL; #define FREE_TARGET() ((cur == &g_t[0]) ? &g_t[1] : &g_t[0]) const float sharp01 = (float)s->sharp_pct / 100.0f; - /* ---- stage 1: scaling (scene -> chain res) */ if (spatial) { barrier(cmd, scene, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, @@ -544,20 +510,17 @@ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int sce SET_NDC(pc); pc.vp[0] = 1.0f / (float)scene_w; pc.vp[1] = 1.0f / (float)scene_h; pc.vp[2] = (float)scene_w; pc.vp[3] = (float)scene_h; - /* SGSR EdgeSharpness 0.5 (slider 0) .. 4.5 (slider 100); NIS takes the slider as is. */ pc.edge = spatial == 7 ? sharp01 : 0.5f + sharp01 * 4.0f; pass(cmd, g_pipe[spatial == 3 ? P_SGSR : spatial == 8 ? P_SGSR_HQ : P_NIS], dst, g_scene.ds, &pc, sizeof(pc)); } else if (spatial == 6) { - /* Sharpen: RCAS fetches the scene 1:1 (outputSize = scene size) while the quad - * covers the chain target = nearest scale + sharpen, as the X11 renderer does. */ struct pc_rcas pc = {0}; SET_NDC(pc); rcas_con(&pc, sharp01); pc.outW = (float)scene_w; pc.outH = (float)scene_h; pass(cmd, g_pipe[P_RCAS], dst, g_scene.ds, &pc, sizeof(pc)); - } else { /* FSR (4) / FSR Fit (5): EASU scene -> mid, RCAS mid -> dst, both at chain res. - * Fill vs fit is the Fullscreen Mode's business here (the mapping), so both - * run the same EASU+RCAS pair. */ + } else { + + struct pc_easu e = {0}; struct pc_rcas r = {0}; SET_NDC(e); @@ -571,8 +534,6 @@ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int sce } cur = dst; cur_ds = dst->ds; } else if (resize) { - /* No spatial pass: a plain filtered resize to the chain resolution first (Linear or - * Nearest per the scaling mode), so the effects run at output resolution like X11. */ struct fx_target *dst = FREE_TARGET(); barrier(cmd, scene, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, @@ -588,16 +549,12 @@ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int sce VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); cur = dst; cur_ds = dst->ds; } else { - /* Scene already at output size: the first effect samples it directly. */ barrier(cmd, scene, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT); cur = NULL; cur_ds = g_scene.ds; } - /* ---- stage 2: the effects, in the X11 renderer's locked order - * FXAA -> Toon -> Colour -> CAS -> HDR -> NTSC -> CRT -> Debanding (terminal dither). - * Each writes the free ping-pong target; the one just read becomes free. */ enum { FX_FXAA, FX_TOON, FX_COLOR, FX_CAS, FX_HDR, FX_NTSC, FX_CRT, FX_DEBAND }; int order[8], n = 0; if (s->fxaa) order[n++] = FX_FXAA; @@ -626,7 +583,6 @@ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int sce case FX_CAS: pr.extra = (float)s->cas_pct / 100.0f; pass(cmd, g_pipe[P_CAS], dst, cur_ds, &pr, sizeof(pr)); break; case FX_HDR: pass(cmd, g_pipe[P_HDR], dst, cur_ds, &pr, sizeof(pr)); break; case FX_NTSC: - /* cos/sin(chromaPhase*0.5) has period 4 in the counter: mod 4 keeps the shimmer exact. */ pr.extra = (float)(g_ntsc_frame++ & 3u); pass(cmd, g_pipe[P_NTSC], dst, cur_ds, &pr, sizeof(pr)); break; @@ -638,23 +594,18 @@ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int sce #undef FREE_TARGET if (cur) { - /* The result: written by the last render pass, now for the output blit. */ barrier(cmd, cur->img, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT); *res_w = cur->w; *res_h = cur->h; return cur->img; } - /* Nothing ran after all (cannot happen with nfx > 0): the scene was moved to - * SHADER_READ_ONLY above; bring it to TRANSFER_SRC for the blit. */ barrier(cmd, scene, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT); return scene; } - /* Nothing to do (a spatial mode with no room to upscale and no effect), or the chain is - * unavailable: present the scene as composited. */ barrier(cmd, scene, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT); diff --git a/app/src/main/cpp/waylandcomp/src/effects_chain.h b/app/src/main/cpp/waylandcomp/src/effects_chain.h index 9b07420df..50e5e3494 100644 --- a/app/src/main/cpp/waylandcomp/src/effects_chain.h +++ b/app/src/main/cpp/waylandcomp/src/effects_chain.h @@ -1,85 +1,33 @@ #ifndef EFFECTS_CHAIN_H #define EFFECTS_CHAIN_H -/* - * Screen-effect chain for the embedded Wayland compositor: the X11 Vulkan renderer's post - * chain (the .frag files in app/src/main/cpp/framegen, VulkanRendererContext::recordUpscalePasses) run - * between the composited scene and the output blit, with the same SPIR-V, push-constant - * layouts, uniform ranges and pass order, so a saved preset looks the same on both backends. - * - * Present-pass hook order (vk_present.c, vkp_render): - * scene (all draws blitted 1:1 into a scene-sized image) - * -> vkp_effects_run() this file: scaling -> effects, in the X11 order - * -> vkp_framegen_run() frame generation (feat/wayland-framegen; inserted after this) - * -> update_map / blit Fullscreen Mode + Screen Alignment, onto the swapchain image - * -> present - * When nothing is on, vkp_effects_active() is 0 and vkp_render keeps its plain path (the draws - * are blitted straight through the mapping; only the blit filter follows Linear/Nearest). - * - * Pass order (LOCKED to the X11 Vulkan renderer): - * scaling (SGSR / SGSR HQ / NIS / FSR EASU+RCAS / Sharpen=RCAS; None/Linear/Nearest = a - * filtered resize) -> FXAA -> Toon -> Colour -> CAS -> HDR -> NTSC -> CRT -> Debanding. - * Scaling resizes the scene to its mapped output size (there is no render scale on Wayland); - * every later pass runs at that resolution and ping-pongs between two targets. - * - * Settings come from any thread (JNI); the compositor thread snapshots them once per frame and - * logs one `effects` line per change. Vulkan objects are created lazily on the first active frame. - */ #define VK_USE_PLATFORM_ANDROID_KHR #include <vulkan/vulkan.h> -/* Compositor thread, from dev_init: the device the chain builds its objects on. */ void vkp_effects_bind_device(VkDevice dev, VkPhysicalDevice pd, const VkPhysicalDeviceMemoryProperties *memprops); -/* Compositor thread: pick up settings changed since the last call. Returns 1 when something - * changed (the caller redraws even a static scene); logs the `effects` line. */ int vkp_effects_sync(void); -/* Compositor thread: 1 when the frame must go through the scene image + chain (any effect on, - * or a spatial scaling mode). Modes None/Linear/Nearest alone are NOT active: the plain blit - * handles them through vkp_effects_blit_filter(). */ int vkp_effects_active(void); -/* The filter for the scene -> output resize: NEAREST for scaling mode 2, else LINEAR. */ VkFilter vkp_effects_blit_filter(void); -/* THE HOOK. Records the chain into `cmd`. `scene` (scene_w x scene_h, R8G8B8A8_UNORM, in - * TRANSFER_DST_OPTIMAL after the composite) -> the image to present, returned in - * TRANSFER_SRC_OPTIMAL with its size in res_w / res_h. out_w/out_h = the scene's mapped size on - * the output (what the scaling modes resize to; the caller maps the result over the whole scene - * afterwards, so Fullscreen Mode / Alignment still apply after the chain). Returns `scene` itself - * (transitioned to TRANSFER_SRC_OPTIMAL) when there is nothing to do or the chain is unavailable. */ VkImage vkp_effects_run(VkCommandBuffer cmd, VkImage scene, int scene_w, int scene_h, int out_w, int out_h, int *res_w, int *res_h); -/* Compositor thread: drop every Vulkan object (device teardown / device lost). */ void vkp_effects_destroy(void); -/* Compositor thread: release the cached source view/descriptor before its owner destroys img. - * A later image can reuse the same Vulkan handle and dimensions; that is not the same resource. */ void vkp_effects_forget_image(VkImage img); -/* Compositor thread, before vkp_effects_run(): the format of the scene image it will be handed and the - * format its own targets use. R8G8B8A8 / R8G8B8A8 for every SDR frame (the default); A2B10G10R10 / - * A2B10G10R10 while an HDR picture runs through it (hdr_compose.h). A target-format change rebuilds the - * chain's objects on its next run (once per switch). */ void vkp_effects_set_formats(VkFormat scene_fmt, VkFormat target_fmt); -/* ---- settings (any thread; values 1:1 with the X11 Vulkan renderer's setters) ---- */ -/* 0=None 1=Linear 2=Nearest 3=SGSR 4=FSR 5=FSR Fit 6=Sharpen 7=NIS 8=SGSR HQ */ void vkp_effects_set_scaling(int mode); -/* Slider 0..100: RCAS lobe scale (FSR/Sharpen), SGSR EdgeSharpness 0.5..4.5, NIS sharpness. */ void vkp_effects_set_upscale_sharpness(int pct); -/* CAS toggle + slider 0..100 (0 = the pass is off). */ void vkp_effects_set_cas(int enabled, int pct); void vkp_effects_set_hdr(int enabled); -/* Terminal dither: slider 0..200 -> strength/100 LSBs. */ void vkp_effects_set_deband(int enabled, int pct); -/* Colour grade in slider units (brightness/contrast -100..100, gamma 0.5..3, saturation 0..200 - * percent; neutral = 0,0,1,100 turns the pass off) + the four shader toggles. */ void vkp_effects_set_screen(float brightness, float contrast, float gamma, float saturation, int fxaa, int toon, int crt, int ntsc); -/* The Look the current controls match ("Custom" when none), for the log line only. */ void vkp_effects_set_look(const char *name); #endif diff --git a/app/src/main/cpp/waylandcomp/src/framegen_bridge.c b/app/src/main/cpp/waylandcomp/src/framegen_bridge.c index ac2100365..3c311b54f 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_bridge.c +++ b/app/src/main/cpp/waylandcomp/src/framegen_bridge.c @@ -1,4 +1,3 @@ -/* Frame generation on the Wayland backend - see framegen_bridge.h. */ #define _POSIX_C_SOURCE 200809L #include "framegen_bridge.h" #include "framegen_engine.h" @@ -15,20 +14,18 @@ void droiddeck_log(const char *tag, const char *fmt, ...) __attribute__((format( #define FGLOG(...) droiddeck_log("framegen", __VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "DroidDeckWayland", __VA_ARGS__) -/* ---- app controls (written from any thread, read on the compositor thread) ------------- */ static _Atomic int g_kind = VKP_FG_ENGINE_LSFG; static _Atomic int g_armed; static _Atomic int g_mult = 2; -static _Atomic int g_target; /* LSFG adaptive fps, 0 = fixed multiplier */ +static _Atomic int g_target; static _Atomic int g_model = 3; static _Atomic int g_preset = 2; static _Atomic int g_cfg_dirty = 1; -static float g_flow = 0.8f, g_refresh_hz; /* under g_lock */ -static char *g_cache_path; /* under g_lock */ -static _Atomic int g_cache_gen; /* bumps when the path changes */ +static float g_flow = 0.8f, g_refresh_hz; +static char *g_cache_path; +static _Atomic int g_cache_gen; static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; -/* ---- device -------------------------------------------------------------------------- */ static VkDevice g_dev; static VkInstance g_inst; static PFN_vkGetInstanceProcAddr g_gipa; @@ -50,12 +47,10 @@ static struct { PFN_vkDeviceWaitIdle DeviceWaitIdle; } vk; -/* ---- engine + generation ring (compositor thread) ------------------------------------ */ -static int g_engine_kind = -1; /* kind of the running engine, -1 = none */ -static int g_engine_cache_gen = -1; /* the LSFG cache it loaded */ + +static int g_engine_kind = -1; +static int g_engine_cache_gen = -1; static int g_engine_ok; -/* The engine of this kind (with this cache) could not start or build its chain; not retried - * every frame - a new engine selection or a new cache clears it. */ static _Atomic int g_failed_kind = -1; static _Atomic int g_failed_cache_gen = -1; @@ -65,11 +60,10 @@ static uint32_t g_ring_n, g_ring_w, g_ring_h; static VkFormat g_ring_fmt; static uint64_t g_source_frames; -static uint32_t g_planned; /* generations planned for the current source frame */ +static uint32_t g_planned; static int g_was_armed, g_generating_logged; static uint32_t g_built_w, g_built_h; static VkFormat g_built_fmt; -/* Deeper formats (FP16 for HDR frames) the engine failed to build its chain in this session. */ static VkFormat g_refused_fmt[4]; static int g_refused_n; static int format_refused(VkFormat f) { @@ -77,10 +71,10 @@ static int format_refused(VkFormat f) { return 0; } -/* ---- stats ------------------------------------------------------------------------- */ -static _Atomic unsigned g_stat_generated; /* since the last stats_take */ + +static _Atomic unsigned g_stat_generated; static uint64_t g_total_generated; -static float g_presented_rate, g_source_rate; /* smoothed, 0.5 s windows */ +static float g_presented_rate, g_source_rate; static uint32_t g_present_accum, g_source_accum; static int64_t g_window_start_ns; @@ -89,7 +83,6 @@ static int64_t now_ns(void) { return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec; } -/* ====================================================================== app controls */ void vkp_framegen_set_engine(int kind) { if (kind != VKP_FG_ENGINE_LSFG && kind != VKP_FG_ENGINE_WINFG) kind = VKP_FG_ENGINE_LSFG; @@ -100,7 +93,6 @@ void vkp_framegen_set_engine(int kind) { void vkp_framegen_set_armed(int armed, int multiplier, int target_fps) { if (target_fps < 0) target_fps = 0; - /* Adaptive sizes the ring and the swapchain for the most it may ever generate. */ if (target_fps) multiplier = VKP_FG_MAX_GENERATIONS + 1; if (multiplier < 2) multiplier = 2; if (multiplier > VKP_FG_MAX_GENERATIONS + 1) multiplier = VKP_FG_MAX_GENERATIONS + 1; @@ -147,8 +139,6 @@ static void mark_failed(int kind) { atomic_store(&g_failed_kind, kind); } -/* Same codes as VulkanRenderer.getFrameGenProblem(); computed from the live state so an engine - * switch from the app is answered at once. */ int vkp_framegen_problem(void) { if (!g_dev_ready) return -1; int kind = atomic_load(&g_kind); @@ -162,8 +152,6 @@ const char *vkp_framegen_caps_reason(void) { return fge_caps_reason(); } void vkp_framegen_stats(float out[6]) { float accepted = 0.0f, src = 0.0f, thermal = -1.0f; if (g_engine_ok) fge_telemetry(&accepted, &src, &thermal); - /* Planned is what is generated; the governor is off on both paths. Our own measured - * source rate serves both engines (Win-FG measures none). */ out[0] = (float)g_planned; out[1] = (float)g_planned; out[2] = g_source_rate; @@ -174,7 +162,6 @@ void vkp_framegen_stats(float out[6]) { unsigned vkp_framegen_stats_take(void) { return atomic_exchange(&g_stat_generated, 0u); } -/* ====================================================================== device setup */ const void *vkp_framegen_device_features(VkInstance inst, PFN_vkGetInstanceProcAddr gipa, VkPhysicalDevice pd) { @@ -199,7 +186,6 @@ void vkp_framegen_device_ready(VkDevice dev, VkQueue queue, uint32_t qfam, int f void vkp_framegen_device_lost(void) { g_dev_lost = 1; } -/* ====================================================================== ring */ static void destroy_ring(void) { if (!g_ring_n) return; @@ -220,9 +206,6 @@ static int memory_type(uint32_t bits, VkMemoryPropertyFlags want) { return -1; } -/* The generation targets: written by the chain through a storage view, read by the screen - * blit. Kept in GENERAL for their whole life (both uses allow it), so there is no layout dance - * per frame - only access barriers. */ static int ensure_ring(uint32_t w, uint32_t h, VkFormat fmt, uint32_t n) { if (n > VKP_FG_MAX_GENERATIONS) n = VKP_FG_MAX_GENERATIONS; if (g_ring_n == n && g_ring_w == w && g_ring_h == h && g_ring_fmt == fmt) return 1; @@ -257,7 +240,6 @@ static int ensure_ring(uint32_t w, uint32_t h, VkFormat fmt, uint32_t n) { return 1; } -/* ====================================================================== present path */ static int engine_usable(void) { int kind = atomic_load(&g_kind); @@ -270,8 +252,6 @@ int vkp_framegen_format_ok(VkFormat fmt) { return fge_format_ok(fmt); } -/* The engine could not build its chain in a deeper format: refuse the format for the session and - * restart the engine on the next frame (a failed build is sticky inside both engines). */ static void refuse_format(VkFormat fmt, int kind, int w, int h, const char *what) { if (g_refused_n < (int)(sizeof(g_refused_fmt) / sizeof(g_refused_fmt[0]))) g_refused_fmt[g_refused_n++] = fmt; FGLOG("%s could not %s in format %d (the HDR picture's FP16) at %dx%d: it restarts, and HDR frames go through " @@ -321,7 +301,6 @@ static int ensure_engine(int kind) { return 1; } -/* "x3 (2 interpolated frames per game frame)" or "adaptive to 90 fps (up to 3 ...)". */ static const char *describe_mode(char *buf, size_t n, int mult, int target) { if (target) snprintf(buf, n, "adaptive to %d fps (up to %d interpolated frames per game frame)", target, mult - 1); else snprintf(buf, n, "x%d (%d interpolated frame%s per game frame)", mult, mult - 1, mult == 2 ? "" : "s"); @@ -397,9 +376,6 @@ int vkp_framegen_run(VkCommandBuffer cmd, VkImage scene, VkImageView scene_view, if (n_gen > g_ring_n) n_gen = g_ring_n; g_planned = n_gen; - /* The scene image was just written (blits, and whatever the effects chain did); make that - * visible to the engine's copy (TRANSFER) and shaders (COMPUTE). Fresh ring images get their - * one UNDEFINED -> GENERAL transition here; the others need TRANSFER-read -> COMPUTE-write. */ VkImageMemoryBarrier bars[1 + VKP_FG_MAX_GENERATIONS]; uint32_t nb = 0; bars[nb++] = (VkImageMemoryBarrier){ @@ -425,15 +401,12 @@ int vkp_framegen_run(VkCommandBuffer cmd, VkImage scene, VkImageView scene_view, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, nb, bars); - /* Frame N in; the shared passes (the whole flow pyramid) run only when generating. */ fge_process(cmd, scene, (uint32_t)w, (uint32_t)h, n_gen); for (uint32_t g = 0; g < n_gen; g++) { fge_generate(cmd, g, n_gen, g_ring[g].img, g_ring[g].view, (uint32_t)w, (uint32_t)h); gens[g] = g_ring[g].img; } if (n_gen) { - /* Compute writes -> the screen blits (this command buffer and the later per-present ones; - * a barrier's first scope covers everything submitted earlier on the queue). */ nb = 0; for (uint32_t g = 0; g < n_gen; g++) bars[nb++] = (VkImageMemoryBarrier){ @@ -466,7 +439,7 @@ void vkp_framegen_presented(int generated) { g_total_generated += (uint64_t)generated; } float elapsed = (float)(now - g_window_start_ns) / 1e9f; - if (elapsed < 0.5f) return; /* half-second window, like the X11 renderer */ + if (elapsed < 0.5f) return; float rate = (float)g_present_accum / elapsed, src = (float)g_source_accum / elapsed; g_presented_rate = g_presented_rate > 0.0f ? g_presented_rate + (rate - g_presented_rate) * 0.25f : rate; g_source_rate = g_source_rate > 0.0f ? g_source_rate + (src - g_source_rate) * 0.25f : src; diff --git a/app/src/main/cpp/waylandcomp/src/framegen_bridge.h b/app/src/main/cpp/waylandcomp/src/framegen_bridge.h index b60229cdb..5e27db40d 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_bridge.h +++ b/app/src/main/cpp/waylandcomp/src/framegen_bridge.h @@ -1,27 +1,5 @@ #ifndef FRAMEGEN_BRIDGE_H #define FRAMEGEN_BRIDGE_H -/* - * Frame generation on the Wayland backend (feat/wayland-framegen). - * - * The two NATIVE engines the X11 renderer runs inside Bannerlator's X11 compositor - LSFG Native - * (the user's Lossless.dll chain, lsfg/) and Win-FG Native (our own FSR3-derived optical-flow - * chain, winfg/) - are hosted here inside the Wayland compositor's Turnip device. Both consume - * consecutive presented frames and emit interpolated frames between them; the guest never - * sees any of it. bionic-fg (the guest-side win-fg Vulkan layer) is X11-only and is not - * ported: its frames are born inside the guest and have nothing to attach to here. - * - * Present order per scene frame (vk_present.c): - * scene -> effects -> FRAME GENERATION -> mapping/blit -> swapchain - * The scene is composed 1:1 into an off-screen image, the effects chain runs on it, then - * vkp_framegen_run() (the one hook) feeds that image to the engine and hands back up to three - * generated images. vk_present.c blits the generated frames to the screen FIRST and the real - * frame LAST (interpolation produces frames that belong between N-1 and N), one FIFO present - * each, so the panel shows them on consecutive vblanks - the same pacing the X11 native path - * is device-proven with. No sleeps, no host-side pacer. - * - * Threads: the set_* controls are called from the app (JNI, any thread) and only store - * values; everything that touches Vulkan runs on the compositor thread inside vkp_render(). - */ #include <stdint.h> #ifndef VK_USE_PLATFORM_ANDROID_KHR #define VK_USE_PLATFORM_ANDROID_KHR @@ -29,63 +7,28 @@ #include <vulkan/vulkan.h> enum { VKP_FG_ENGINE_LSFG = 0, VKP_FG_ENGINE_WINFG = 1 }; -/* Hard ceiling on generated frames per source frame (2x..4x -> 1..3), as on X11. */ #define VKP_FG_MAX_GENERATIONS 3 -/* ---- app controls (any thread) ------------------------------------------------------- */ void vkp_framegen_set_engine(int kind); -/* multiplier 2..4, or target_fps > 0 for LSFG's adaptive mode (up to 4x, as the target needs); - * armed = 0 stops generating (the ring and the engine stay for a re-arm). */ void vkp_framegen_set_armed(int armed, int multiplier, int target_fps); -/* LSFG Native: the SPIR-V cache built from the user's Lossless.dll (Lossless.sync); a new path reloads the engine. */ void vkp_framegen_set_lsfg_cache_path(const char *path); -/* Flow scale 0.25..1.0 and the panel's real refresh rate (the pacer never generates above it). */ void vkp_framegen_set_tuning(float flow_scale, float refresh_hz); -/* Win-FG Native: interpolation model (3/4) and performance preset (0..2). */ void vkp_framegen_set_winfg_tuning(int model, int perf_preset); -/* Same codes as VulkanRenderer.getFrameGenProblem(): -1 not known yet (device not up), 0 fine, - * 1 the driver lacks what the selected engine needs (vkp_framegen_caps_reason says what), - * 2 the engine failed to start / build its chain. */ int vkp_framegen_problem(void); const char *vkp_framegen_caps_reason(void); -/* Same shape as VulkanRenderer.getFrameGenStats(): {generations trusted, generations planned, - * real (source) fps, presented fps incl. generated frames, thermal (-1 = none), ms per - * generated frame (-1 = unknown)}. */ void vkp_framegen_stats(float out[6]); -/* Generated frames presented since the last call (compositor.c, the 10 s stats line). */ unsigned vkp_framegen_stats_take(void); -/* ---- vk_present.c: device setup (compositor thread) ----------------------------------- */ -/* Before vkCreateDevice: probe what the engines need on this physical device and return the - * pNext chain to hang on VkDeviceCreateInfo (NULL when the device cannot run the LSFG chain - - * Win-FG needs no feature). Owned by the bridge, valid until the next call. */ const void *vkp_framegen_device_features(VkInstance inst, PFN_vkGetInstanceProcAddr gipa, VkPhysicalDevice pd); -/* After the device exists (features_enabled = the chain above was accepted). */ void vkp_framegen_device_ready(VkDevice dev, VkQueue queue, uint32_t qfam, int features_enabled); -/* VK_ERROR_DEVICE_LOST: never touch the device again. */ void vkp_framegen_device_lost(void); -/* ---- vk_present.c: present path (compositor thread) ------------------------------------ */ -/* 1 when this frame must go through the compositor pass (armed and the engine can run). Safe - * before the device is up (0). */ int vkp_framegen_active(void); -/* Extra swapchain images wanted so every present of one scene frame is queued without waiting - * for a vblank: multiplier - 1 while armed, 0 otherwise. A change means "rebuild the swapchain". */ int vkp_framegen_extra_images(void); -/* THE hook. Called once per composed scene frame, after the effects hook, with the scene image - * (GENERAL, w x h, fmt, TRANSFER_SRC|SAMPLED). Records the engine's chain into `cmd` and - * returns how many generated frames (0..VKP_FG_MAX_GENERATIONS) it produced, their images in - * gens[] (GENERAL, w x h, already made visible to TRANSFER reads). 0 = present the scene alone. */ int vkp_framegen_run(VkCommandBuffer cmd, VkImage scene, VkImageView scene_view, int w, int h, VkFormat fmt, VkImage gens[VKP_FG_MAX_GENERATIONS]); -/* After the presents of one scene frame: how many generated frames actually reached the - * swapchain (rate tracking + the stats counters). */ void vkp_framegen_presented(int generated); -/* 1 when vkp_framegen_run can be fed `fmt` (the 8-bit scene always; FP16 for HDR frames where the - * device can generate in it and the engine has not refused it this session). A chain that fails to - * build in a deeper format is not an engine failure: the engine restarts and the format is refused, - * so the caller goes back to 8 bits. */ int vkp_framegen_format_ok(VkFormat fmt); #endif diff --git a/app/src/main/cpp/waylandcomp/src/framegen_engine.cpp b/app/src/main/cpp/waylandcomp/src/framegen_engine.cpp index 12674e533..43edb1240 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_engine.cpp +++ b/app/src/main/cpp/waylandcomp/src/framegen_engine.cpp @@ -1,9 +1,3 @@ -// C++ side of the Wayland frame-generation bridge - see framegen_engine.h. -// -// Hosts the SAME engine objects the X11 renderer drives (lsfg::Engine, winfg::Engine), fed from -// the compositor's own Turnip entry points. Nothing in framegen/lsfg or framegen/winfg is -// modified: the engines take a VkTable (VulkanRendererContext.h) for their dispatch, and one is -// filled here by name from the compositor's vkGetInstanceProcAddr / vkGetDeviceProcAddr. #include "framegen_engine.h" #include <cstdio> @@ -14,7 +8,7 @@ #include <android/log.h> -#include "VulkanRendererContext.h" // VkTable +#include "VulkanRendererContext.h" #include "lsfg/lsfg_engine.h" #include "lsfg/lsfg_probe.h" #include "lsfg/lsfg_vkd.h" @@ -40,13 +34,10 @@ std::unique_ptr<winfg::Engine> g_winfg; int g_kind = -1; char g_build[96] = ""; -// Instance-level entry points the engines and the probe use. #define FGE_INSTANCE_FUNCS(X) \ X(GetPhysicalDeviceProperties) X(GetPhysicalDeviceMemoryProperties) \ X(GetPhysicalDeviceFeatures2) X(GetPhysicalDeviceFormatProperties) \ X(GetPhysicalDeviceQueueFamilyProperties) X(GetDeviceProcAddr) -// Device-level: the LSFG chain's 34 (lsfg_vkd.h), win-fg's extras (winfg_vkd.cpp) and the -// generic set either may reach for. Anything left null in VkTable is never called. #define FGE_DEVICE_FUNCS(X) \ X(AllocateDescriptorSets) X(AllocateMemory) X(BindBufferMemory) X(BindImageMemory) \ X(CmdBindDescriptorSets) X(CmdBindPipeline) X(CmdCopyImage) X(CmdDispatch) \ @@ -78,7 +69,7 @@ void fill_device(VkDevice dev) { #undef X } -} // namespace +} extern "C" { @@ -96,9 +87,9 @@ const void *fge_probe(PFN_vkGetInstanceProcAddr gipa, VkInstance inst, VkPhysica if (n) enumExts(pd, nullptr, &n, exts.data()); } g_caps = lsfg::Caps{}; - // No Vulkan 1.1 compat here: the Wayland compositor only ever runs on Turnip (adrenotools), - // which reports 1.3+; the stock-driver case the compat flag exists for cannot reach it. - g_caps.features = lsfg::queryFeatures(g_table, pd, exts, /*allowVk11=*/false); + + + g_caps.features = lsfg::queryFeatures(g_table, pd, exts, false); g_caps.probedFormat = ring_fmt; g_caps.storageOnSwapchainFormat = g_table.GetPhysicalDeviceFormatProperties ? lsfg::probeStorageFormat(g_table, pd, ring_fmt) : false; @@ -110,7 +101,6 @@ const void *fge_probe(PFN_vkGetInstanceProcAddr gipa, VkInstance inst, VkPhysica ELOG("device gates for the LSFG chain fail: %s", g_caps.reason); return nullptr; } - // Same chain VulkanRendererContext::createLogicalDevice hangs on its device. g_v12 = VkPhysicalDeviceVulkan12Features{}; g_v12.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES; g_v12.vulkanMemoryModel = VK_TRUE; @@ -207,8 +197,6 @@ void fge_configure(uint32_t multiplier, uint32_t target_fps, float flow_scale, f int fge_prepare(uint32_t w, uint32_t h, VkFormat fmt) { if (g_kind == 1) return g_winfg && g_winfg->prepare(w, h, fmt) ? 1 : 0; if (!g_lsfg) return 0; - // The Wayland scene is the game's own size when it runs fullscreen, so the flow pyramid is - // scaled against that (X11 passes the X screen size here for the same reason). g_lsfg->setGuestExtent(w, h); return g_lsfg->prepare(w, h, fmt) ? 1 : 0; } @@ -247,4 +235,4 @@ void fge_telemetry(float *accepted, float *source_rate, float *thermal) { const char *fge_engine_name(int kind) { return kind == 1 ? "Win-FG Native" : "LSFG Native"; } const char *fge_build_info(void) { return g_build; } -} // extern "C" +} diff --git a/app/src/main/cpp/waylandcomp/src/framegen_engine.h b/app/src/main/cpp/waylandcomp/src/framegen_engine.h index b0deec70b..4257a7592 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_engine.h +++ b/app/src/main/cpp/waylandcomp/src/framegen_engine.h @@ -1,16 +1,5 @@ #ifndef FRAMEGEN_ENGINE_H #define FRAMEGEN_ENGINE_H -/* - * C face of the C++ frame-generation engines for framegen_bridge.c. - * - * framegen_engine.cpp wraps lsfg::Engine (framegen/lsfg) and winfg::Engine (framegen/winfg) - * unmodified - the same objects the X11 renderer (VulkanRendererContext) drives - and fills - * their dispatch tables from the compositor's own Turnip entry points. The standalone - * compositor build (no framegen tree) compiles framegen_engine_stub.c instead, which reports - * "engines not built in" and generates nothing. - * - * Every function runs on the compositor thread. - */ #include <stdint.h> #ifndef VK_USE_PLATFORM_ANDROID_KHR #define VK_USE_PLATFORM_ANDROID_KHR @@ -21,49 +10,27 @@ extern "C" { #endif -/* Probe the physical device and build the VkDeviceCreateInfo pNext chain the LSFG chain needs - * (vulkanMemoryModel + storage-image features; nothing for Win-FG). Returns the chain, or NULL - * when the device fails a gate. `ring_fmt` is the format the engines generate into; it must be - * storage-capable for either engine. */ const void *fge_probe(PFN_vkGetInstanceProcAddr gipa, VkInstance inst, VkPhysicalDevice pd, VkFormat ring_fmt); -/* The device exists; resolve device-level entry points. features_enabled = fge_probe's chain - * was accepted by vkCreateDevice. */ void fge_device_ready(PFN_vkGetInstanceProcAddr gipa, VkInstance inst, VkPhysicalDevice pd, VkDevice dev, VkQueue queue, uint32_t qfam, int features_enabled); -/* 1 when engine `kind` can run on this device (its capability gates pass). */ int fge_caps_ok(int kind); -/* Why the LSFG gates fail ("supported" when they pass); static string. */ const char *fge_caps_reason(void); -/* 1 when the engines can generate in `fmt` on this device (storage + linear sampling), beyond the - * ring format probed at fge_probe - the FP16 scene of HDR frames (vk_present.c). */ int fge_format_ok(VkFormat fmt); -/* Create engine `kind` (LSFG needs the cache path; Win-FG ignores it). 0 = ready, -1 = failed. - * Only one engine holds GPU resources at a time; a different kind replaces it. */ int fge_start(int kind, const char *cache_path); void fge_stop(void); -/* The engine came up but later found it cannot generate (chain build failed). */ int fge_unavailable(void); -/* target_fps > 0: LSFG picks 0..3 generations per frame to reach it (Win-FG ignores it). */ void fge_configure(uint32_t multiplier, uint32_t target_fps, float flow_scale, float refresh_hz, int model, int perf_preset); -/* Build/rebuild the chain for this extent; the guest extent for the flow pyramid is the same - * (the Wayland scene is the game's own size in fullscreen). 1 = chain valid. */ int fge_prepare(uint32_t w, uint32_t h, VkFormat fmt); -/* How many frames to generate for this source frame (0 until the history is valid). */ uint32_t fge_plan(uint32_t capacity, uint64_t source_frames, float presented_rate); -/* Take the scene image (GENERAL) as frame N and run the shared part of the chain. */ void fge_process(VkCommandBuffer cmd, VkImage source, uint32_t w, uint32_t h, uint32_t gens); -/* Synthesise generated frame g of `count` into a storage image (GENERAL). */ void fge_generate(VkCommandBuffer cmd, uint32_t g, uint32_t count, VkImage img, VkImageView view, uint32_t w, uint32_t h); -/* The generation targets were recreated: drop cached descriptors. */ void fge_forget_targets(void); -/* Engine-side telemetry: accepted generations, source rate (0 = engine has none), thermal. */ void fge_telemetry(float *accepted, float *source_rate, float *thermal); const char *fge_engine_name(int kind); -/* "lsfg-native"/"winfg-native" build provenance for the start log line. */ const char *fge_build_info(void); #ifdef __cplusplus diff --git a/app/src/main/cpp/waylandcomp/src/framegen_engine_stub.c b/app/src/main/cpp/waylandcomp/src/framegen_engine_stub.c index dd4144d3e..b9c4dff36 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_engine_stub.c +++ b/app/src/main/cpp/waylandcomp/src/framegen_engine_stub.c @@ -1,5 +1,3 @@ -/* Stand-in for framegen_engine.cpp when the compositor is built on its own (standalone/, no - * framegen tree, no dxbc). Every engine is reported unavailable; nothing is generated. */ #include "framegen_engine.h" const void *fge_probe(PFN_vkGetInstanceProcAddr gipa, VkInstance inst, VkPhysicalDevice pd, diff --git a/app/src/main/cpp/waylandcomp/src/framegen_jni.c b/app/src/main/cpp/waylandcomp/src/framegen_jni.c index 8d5bad571..ea5ce0396 100644 --- a/app/src/main/cpp/waylandcomp/src/framegen_jni.c +++ b/app/src/main/cpp/waylandcomp/src/framegen_jni.c @@ -1,8 +1,3 @@ -/* JNI for frame generation on the Wayland backend (WaylandCompositor.nativeFrameGen* / - * nativeSet*FrameGen* / nativeSetLsfgCachePath / nativeSetWinFgTuning). Every call only stores - * a value in framegen_bridge.c; the compositor thread picks it up on its next frame, so these - * are safe from any thread and before the compositor is up (the app arms the engine from its - * launch code while the SurfaceView is still coming). */ #include <jni.h> #include "framegen_bridge.h" diff --git a/app/src/main/cpp/waylandcomp/src/hdr_compose.c b/app/src/main/cpp/waylandcomp/src/hdr_compose.c index 6f54d4e0c..01d8be6f6 100644 --- a/app/src/main/cpp/waylandcomp/src/hdr_compose.c +++ b/app/src/main/cpp/waylandcomp/src/hdr_compose.c @@ -1,16 +1,10 @@ -/* HDR composition: the encode pass over the mixed scene - see hdr_compose.h. Compositor thread. */ #define _POSIX_C_SOURCE 200809L #include "hdr_compose.h" #include "vk_loader.h" -#include "vk_present.h" /* droiddeck_log, vkp_gpu_name */ +#include "vk_present.h" #include <stdio.h> #include <string.h> -/* SPIR-V, pre-compiled (glslangValidator -V hdr_encode.frag --vn hdr_encode_code) and committed next - * to the GLSL, like every other shader of the compositor: the NDK build compiles no shaders. The quad - * is the effect chain's upscale.vert (same bytes, from app/src/main/cpp/framegen). */ -/* The headers define plain (external) arrays and effects_chain.c includes upscale_vert.h too: this - * translation unit gets its own copy under another name, so the link sees no duplicate symbol. */ #define upscale_vert_code hdrc_upscale_vert_code #include "upscale_vert.h" #undef upscale_vert_code @@ -18,24 +12,20 @@ #define MIXED_FORMAT VK_FORMAT_A2B10G10R10_UNORM_PACK32 -struct pc_encode { float ndc[4]; float params[4]; float rects[HDRC_MAX_RECTS][4]; }; /* 128 bytes */ +struct pc_encode { float ndc[4]; float params[4]; float rects[HDRC_MAX_RECTS][4]; }; static VkDevice g_dev; -static int g_ready; /* 0 = not built, 1 = ok, -1 = unavailable (said once) */ +static int g_ready; static VkSampler g_sampler; static VkDescriptorSetLayout g_dsl; static VkPipelineLayout g_pl; static VkDescriptorPool g_dpool; static VkShaderModule g_vert, g_frag; -/* One render pass + pipeline per output format: the HDR picture is 10-bit, frame generation's scene - * 8-bit - or FP16 where the engine takes it (vk_present.c FG_HDR_FMT). */ #define NFMT 3 static struct { VkFormat fmt; VkRenderPass rp; VkPipeline pipe; } g_out[NFMT] = { {VK_FORMAT_A2B10G10R10_UNORM_PACK32}, {VK_FORMAT_R8G8B8A8_UNORM}, {VK_FORMAT_R16G16B16A16_SFLOAT}}; -/* The mixed image's view + descriptor, and each output image's view + framebuffer: kept while the - * image handle (and size) stays the same, rebuilt when vk_present re-creates the image. */ static struct { VkImage img; int w, h; VkImageView view; VkDescriptorSet ds; } g_src; #define NOUT 3 static struct { VkImage img; VkFormat fmt; int w, h; VkImageView view; VkFramebuffer fb; } g_dst[NOUT]; @@ -92,9 +82,6 @@ static VkShaderModule shader(const uint32_t *code, size_t size) { return m; } -/* The render pass: one colour target written whole (DONT_CARE), left in TRANSFER_DST_OPTIMAL for the - * effects chain / the copies that follow. The dependencies order it after whoever read or wrote the - * target last (last frame's blit or chain) and before whoever samples, blits or copies it next. */ static VkRenderPass make_rp(VkFormat fmt) { VkAttachmentDescription att = {.format = fmt, .samples = VK_SAMPLE_COUNT_1_BIT, .loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, .storeOp = VK_ATTACHMENT_STORE_OP_STORE, @@ -261,7 +248,6 @@ int hdrc_encode(VkCommandBuffer cmd, VkImage mixed, VkImage out, VkFormat out_fm VkFramebuffer fb = dst_ensure(out, out_fmt, w, h, g_out[slot].rp); if (!fb) return -1; - /* The composite blits wrote the mixed image; the pass samples it. */ VkImageMemoryBarrier b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, diff --git a/app/src/main/cpp/waylandcomp/src/hdr_compose.h b/app/src/main/cpp/waylandcomp/src/hdr_compose.h index cfeef7497..5a37d42ad 100644 --- a/app/src/main/cpp/waylandcomp/src/hdr_compose.h +++ b/app/src/main/cpp/waylandcomp/src/hdr_compose.h @@ -1,22 +1,5 @@ #ifndef HDR_COMPOSE_H #define HDR_COMPOSE_H -/* - * HDR composition (round 2 of HDR_RECON.md's Phase B): when an HDR game cannot be alone on its own - * display layer - screen effects on, frame generation on, a window above it on a display that cannot - * compose a second layer, a windowed game, a frame without a gralloc buffer - the whole scene is put - * into ONE encoding instead of being blitted into an 8-bit sRGB target as it stands (which shows the - * game's PQ frames washed out). - * - * vk_present.c blits every draw 1:1 into a 10-bit "mixed" image (each pixel still in its own - * encoding) and hdrc_encode() runs hdr_encode.frag over it into the output image: - * HDRC_OUT_PQ - PQ BT.2020 (SDR pixels placed at the SDR reference white): goes onto the game's - * display layer tagged BT2020_PQ (sc_layer.c), or into an HDR10 swapchain; - * HDRC_OUT_SDR - sRGB BT.709 with the HDR pixels tone-mapped: for a present that cannot carry HDR. - * Which encoding a pixel is in comes from the draws' rectangles (the topmost draw decides). - * - * Compositor thread. Objects are built lazily on the first use; if the driver refuses them, the - * encode is unavailable for the session (logged once) and the caller falls back. - */ #define VK_USE_PLATFORM_ANDROID_KHR #include <vulkan/vulkan.h> @@ -26,26 +9,17 @@ enum hdrc_mode { HDRC_OUT_PQ = 0, HDRC_OUT_SDR = 1 }; struct hdrc_params { enum hdrc_mode mode; - float sdr_white_nits; /* SDR reference white inside the HDR picture (203) */ - float peak_nits; /* HDR content peak for the tone-map (max CLL / mastering max, else 1000) */ - int count; /* rects, top draw first */ - unsigned hdr_mask; /* bit i: rects[i] is an HDR draw */ - float rects[HDRC_MAX_RECTS][4]; /* scene pixels x0, y0, x1, y1 */ + float sdr_white_nits; + float peak_nits; + int count; + unsigned hdr_mask; + float rects[HDRC_MAX_RECTS][4]; }; -/* Called from dev_init (device up). */ void hdrc_bind_device(VkDevice dev, const VkPhysicalDeviceMemoryProperties *memprops); -/* Device teardown. */ void hdrc_destroy(void); -/* vk_present is about to destroy `img` (a resize of the mixed / scene / picture image): drop the view, - * descriptor and framebuffer cached for it, so a later image that happens to reuse the handle value is - * never drawn through a view of the destroyed one. */ void hdrc_forget_image(VkImage img); -/* Record the encode: `mixed` (w x h, VK_FORMAT_A2B10G10R10_UNORM_PACK32, in TRANSFER_DST_OPTIMAL after - * the composite blits) -> `out` (w x h, out_fmt, created with COLOR_ATTACHMENT + SAMPLED usage), which - * is left in TRANSFER_DST_OPTIMAL - the layout vkp_effects_run() takes its scene in. 0 = recorded, - * -1 = unavailable (nothing recorded; `mixed` untouched). */ int hdrc_encode(VkCommandBuffer cmd, VkImage mixed, VkImage out, VkFormat out_fmt, int w, int h, const struct hdrc_params *p); diff --git a/app/src/main/cpp/waylandcomp/src/hdr_encode.frag b/app/src/main/cpp/waylandcomp/src/hdr_encode.frag index 4b75ed586..db3ea8bca 100644 --- a/app/src/main/cpp/waylandcomp/src/hdr_encode.frag +++ b/app/src/main/cpp/waylandcomp/src/hdr_encode.frag @@ -1,38 +1,12 @@ #version 450 -// ============================================================================= -// HDR composition: one encoding for the whole scene (waylandcomp/src/hdr_compose.c). -// -// The compositor composes with blits, which copy values and know nothing about what they mean. -// When an HDR game (PQ-encoded BT.2020 frames) shares the scene with SDR content (sRGB windows, the -// desktop, a window above the game), every draw is first blitted 1:1 into a 10-bit "mixed" image - -// each pixel still in its own encoding - and this pass turns the mixed image into ONE encoding: -// -// mode 0 (HDR out, PQ BT.2020): HDR pixels pass through; SDR pixels are decoded (sRGB), -// converted BT.709 -> BT.2020 and placed at the SDR reference white (params.x nits, -// 203 = Report ITU-R BT.2408), then PQ-encoded. -// mode 1 (SDR out, sRGB): SDR pixels pass through; HDR pixels are PQ-decoded, converted -// BT.2020 -> BT.709, tone-mapped relative to the SDR reference white (linear below a -// knee, a smooth roll-off above it that puts the content's peak, params.y nits, at 98 %) -// and sRGB-encoded - what a display that cannot take HDR gets instead of washed-out PQ. -// -// Which encoding a pixel is in: the topmost draw covering it decides. params.w packs the number of -// rects (count, 0..6) and a bitmask of which of them are HDR (count + 16 * mask); rects are scene -// pixels (x0, y0, x1, y1), TOP draw first, listed down to the lowest HDR draw (everything below -// that is SDR, and so is any pixel no listed rect covers). -// -// Conventions match upscale.vert / the effect passes: combined-image-sampler at binding 0, the -// push-constant block leads with vec4 ndc (offset 0). The target is exactly scene-sized, so -// gl_FragCoord is the scene pixel and texelFetch reads the mixed image 1:1 (no filtering). -// Float-only mask arithmetic (no bitwise ops). -// ============================================================================= layout(binding = 0) uniform sampler2D Mixed; layout(push_constant) uniform PC { - vec4 ndc; // upscale.vert's quad - vec4 params; // x = SDR white nits, y = HDR content peak nits (mode 1), z = mode, w = count + 16 * mask - vec4 rects[6]; // top-first scene rects + vec4 ndc; + vec4 params; + vec4 rects[6]; } pc; layout(location = 0) in vec2 fragTexCoord; @@ -44,7 +18,6 @@ const float PQ_C1 = 0.8359375; const float PQ_C2 = 18.8515625; const float PQ_C3 = 18.6875; -// Column-major: each group of three is one COLUMN (the coefficients of R, then of G, then of B). const mat3 BT709_TO_BT2020 = mat3(0.6274, 0.0691, 0.0164, 0.3293, 0.9195, 0.0880, 0.0433, 0.0114, 0.8956); @@ -64,25 +37,21 @@ vec3 linearToSrgb(vec3 c) { return mix(lo, hi, step(vec3(0.0031308), c)); } -// PQ code (0..1) -> luminance / 10000 cd/m2 (SMPTE ST 2084 EOTF). vec3 pqToLinear(vec3 e) { vec3 p = pow(clamp(e, 0.0, 1.0), vec3(1.0 / PQ_M2)); return pow(max(p - PQ_C1, 0.0) / max(PQ_C2 - PQ_C3 * p, 1e-6), vec3(1.0 / PQ_M1)); } -// luminance / 10000 cd/m2 -> PQ code (inverse EOTF). vec3 linearToPq(vec3 y) { vec3 p = pow(clamp(y, 0.0, 1.0), vec3(PQ_M1)); return pow((PQ_C1 + PQ_C2 * p) / (1.0 + PQ_C3 * p), vec3(PQ_M2)); } -// Tone curve in units of SDR white: identity up to the knee, then an exponential roll-off towards 1 -// whose rate puts `peak` (also in SDR-white units) at 98 % of the range. float toneCurve(float x, float peak) { const float knee = 0.8; if (x <= knee) return x; float span = max(peak - knee, 0.05); - float rate = max(span / 3.912, 1.0 - knee); // ln(50) = 3.912; never steeper than the identity + float rate = max(span / 3.912, 1.0 - knee); return knee + (1.0 - knee) * (1.0 - exp(-(x - knee) / rate)); } @@ -107,7 +76,6 @@ void main() { float sdrWhite = max(pc.params.x, 1.0); vec3 outc; if (pc.params.z < 0.5) { - // HDR out: PQ BT.2020. if (hdr) { outc = c; } else { @@ -115,7 +83,6 @@ void main() { outc = linearToPq(nits / 10000.0); } } else { - // SDR out: sRGB BT.709, HDR tone-mapped. if (!hdr) { outc = c; } else { diff --git a/app/src/main/cpp/waylandcomp/src/hdr_encode_frag.h b/app/src/main/cpp/waylandcomp/src/hdr_encode_frag.h index eb1536f1f..ba52146fc 100644 --- a/app/src/main/cpp/waylandcomp/src/hdr_encode_frag.h +++ b/app/src/main/cpp/waylandcomp/src/hdr_encode_frag.h @@ -1,4 +1,3 @@ - // 1116.5.0 #pragma once const uint32_t hdr_encode_code[] = { 0x07230203,0x00010000,0x0008000b,0x0000018e,0x00000000,0x00020011,0x00000001,0x0006000b, diff --git a/app/src/main/cpp/waylandcomp/src/headpose_shm.h b/app/src/main/cpp/waylandcomp/src/headpose_shm.h index 13c71a7f8..1bed89e79 100644 --- a/app/src/main/cpp/waylandcomp/src/headpose_shm.h +++ b/app/src/main/cpp/waylandcomp/src/headpose_shm.h @@ -1,27 +1,14 @@ #ifndef HEADPOSE_SHM_H #define HEADPOSE_SHM_H -/* - * The head pose Cardboard mode tracks, shared with the guest: a 128-byte file the compositor maps - * at <files>/.wayland-rt/droiddeck-headpose, which the guest sees as /run/droiddeck/droiddeck-headpose. - * Written by the app (cardboard.c) at the sensor's rate; read by guest VR code (the SteamVR driver in - * tools/vr) without any call into the app. - * - * Seqlock: the writer makes seq odd, writes the rest, then makes it even again. A reader copies the - * struct and keeps the copy only if seq was even and unchanged across the copy. - * - * Axes are OpenVR's (and the cardboard shader's): x right, y up, z back (-z is straight ahead). - * rot maps head directions to the recentred world, row-major. time_ns is CLOCK_MONOTONIC, the same - * clock inside the guest (one kernel). - */ #include <stdint.h> -#define DROIDDECK_HEADPOSE_MAGIC 0x50484444u /* "DDHP" */ +#define DROIDDECK_HEADPOSE_MAGIC 0x50484444u #define DROIDDECK_HEADPOSE_VERSION 1u #define DROIDDECK_HEADPOSE_FILE "droiddeck-headpose" enum { - DROIDDECK_HEADPOSE_LIVE = 1u << 0, /* a rotation sensor is feeding it (else identity) */ - DROIDDECK_HEADPOSE_ENABLED = 1u << 1, /* Cardboard mode is on */ + DROIDDECK_HEADPOSE_LIVE = 1u << 0, + DROIDDECK_HEADPOSE_ENABLED = 1u << 1, }; struct droiddeck_headpose { @@ -32,11 +19,9 @@ struct droiddeck_headpose { float rot[9]; float ipd_m; int64_t time_ns; - /* What the lens pass expects of a side-by-side source, so a VR renderer can match it: one eye's - * half of the frame covers tan_half_fov up and down and eye_aspect times that left and right. */ float tan_half_fov; float eye_aspect; - uint32_t eye_w, eye_h; /* one eye's viewport on the panel, pixels (0 = not drawn yet) */ + uint32_t eye_w, eye_h; uint32_t reserved[12]; }; diff --git a/app/src/main/cpp/waylandcomp/src/sc_layer.c b/app/src/main/cpp/waylandcomp/src/sc_layer.c index 3f44a6be8..c7a00eac9 100644 --- a/app/src/main/cpp/waylandcomp/src/sc_layer.c +++ b/app/src/main/cpp/waylandcomp/src/sc_layer.c @@ -1,7 +1,3 @@ -/* Layer mode: the scene on a small set of ASurfaceControl layers - see sc_layer.h and - * ZERO_COPY_SPIKE.md. The ASurfaceControl/ASurfaceTransaction API is dlsym'd from libandroid.so - * (API 29+), the same way the X11 renderers' scanout code does it, so the library still loads on - * older devices and the prototype stays off every other path. */ #define _GNU_SOURCE #include "sc_layer.h" #include "ahb_swapchain.h" @@ -25,25 +21,21 @@ #define FOURCC(a, b, c, d) \ ((uint32_t)(a) | ((uint32_t)(b) << 8) | ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24)) -#define DRM_ABGR8888 FOURCC('A', 'B', '2', '4') /* R,G,B,A in memory = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM */ -#define DRM_ABGR2101010 FOURCC('A', 'B', '3', '0') /* = AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM (VK A2B10G10R10) */ +#define DRM_ABGR8888 FOURCC('A', 'B', '2', '4') +#define DRM_ABGR2101010 FOURCC('A', 'B', '3', '0') #define AHB_RGBA8 AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM #define AHB_RGB10A2 AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM #define MOD_LINEAR 0ULL -#define MOD_QCOM_COMPRESSED 0x0500000000000001ULL /* DRM_FORMAT_MOD_QCOM_COMPRESSED (UBWC) */ +#define MOD_QCOM_COMPRESSED 0x0500000000000001ULL -/* The gralloc buffer handle behind an AHardwareBuffer (AOSP cutils/native_handle.h, which the NDK - * does not ship; the layout is a stable ABI: fds first, then ints). */ struct droiddeck_native_handle { int version; int numFds; int numInts; int data[]; }; -/* ---- libandroid SurfaceControl API (dlsym) ---------------------------------------------------- */ typedef struct ASurfaceControl ASurfaceControl; typedef struct ASurfaceTransaction ASurfaceTransaction; typedef struct ASurfaceTransactionStats ASurfaceTransactionStats; typedef void (*sc_complete_fn)(void *context, ASurfaceTransactionStats *stats); -/* <android/hdr_metadata.h> (NDK, API 29), declared here like the rest of this table: stable ABI. */ struct droiddeck_color_xy { float x, y; }; struct droiddeck_hdr_smpte2086 { struct droiddeck_color_xy displayPrimaryRed, displayPrimaryGreen, displayPrimaryBlue, whitePoint; @@ -65,22 +57,14 @@ static struct { void (*reparent)(ASurfaceTransaction *, ASurfaceControl *, ASurfaceControl *); void (*setOnComplete)(ASurfaceTransaction *, void *, sc_complete_fn); int (*prevReleaseFence)(ASurfaceTransactionStats *, ASurfaceControl *); - /* Display frame-rate vote for the layer (API 30 / 31). Optional: absent on older Android, where - * the layer simply carries no vote and the app's surface vote is all there is. */ void (*setFrameRate)(ASurfaceTransaction *, ASurfaceControl *, float, int8_t); void (*setFrameRateStrategy)(ASurfaceTransaction *, ASurfaceControl *, float, int8_t, int8_t); - /* Colour (API 29), optional: only an HDR session ever calls them (droiddeck_color.h's gate needs - * setBufferDataSpace; the metadata calls are sent when present). */ void (*setBufferDataSpace)(ASurfaceTransaction *, ASurfaceControl *, int32_t); void (*setHdrMetadata_smpte2086)(ASurfaceTransaction *, ASurfaceControl *, const struct droiddeck_hdr_smpte2086 *); void (*setHdrMetadata_cta861_3)(ASurfaceTransaction *, ASurfaceControl *, const struct droiddeck_hdr_cta861_3 *); - /* Not in the public NDK headers (vndk/hardware_buffer.h) but exported by libnativewindow.so on - * every device; Mesa's Android WSI calls it for every gralloc buffer it imports. */ const void *(*getNativeHandle)(const AHardwareBuffer *); - /* API 35: the HDR headroom this layer asks the display for (0 = no preference). Round 2e asks - * explicitly for every PQ layer - some phones only boost HDR when a layer asks. */ void (*setDesiredHdrHeadroom)(ASurfaceTransaction *, ASurfaceControl *, float); - int state; /* 0 = untried, 1 = loaded, -1 = unavailable */ + int state; } api; #define ASC_VISIBILITY_HIDE 0 @@ -132,83 +116,74 @@ static int load_api(void) { return 0; } -/* ---- display frame-rate vote (VRR / refresh-rate matching) ------------------------------------ */ -/* The app votes a panel cadence with Surface.setFrameRate on the compositor's SurfaceView, but a - * zero-copy game's frames never touch that surface - they go straight onto the GAME layer - so - * SurfaceFlinger needs the same vote there or the layer's cadence is invisible to it. The vote - * belongs to the layer the game is actually presenting on and to no other: the overlay layer - * carries a window that updates on its own (slow) schedule and is explicitly voted 0, so it never - * drags the panel. The rate is set from the app thread and applied on the compositor thread with - * the next transaction, so no transaction is ever created off-thread. 0 = no vote (panel free). */ -#define ASC_FRAME_RATE_COMPAT_DEFAULT 0 /* == ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT */ -#define ASC_CHANGE_FRAME_RATE_ALWAYS 1 /* == ANATIVEWINDOW_CHANGE_FRAME_RATE_ALWAYS */ -static _Atomic float g_fps_want; /* what the app asked for (the game's cadence) */ + + + + + + + +#define ASC_FRAME_RATE_COMPAT_DEFAULT 0 +#define ASC_CHANGE_FRAME_RATE_ALWAYS 1 + +static _Atomic float g_fps_want; void sc_layer_set_frame_rate(float fps) { atomic_store(&g_fps_want, fps > 0.0f ? fps : 0.0f); } -/* ---- the layers and their buffer pools -------------------------------------------------------- */ -/* Five for the game layer: one on screen, one queued, and the ones SurfaceFlinger has not reported - * back yet (its OnComplete for a transaction arrives only after the NEXT frame is presented), with - * room to spare, so a free buffer is nearly always one the display already let go of. With three, - * a late callback left none free ("no free layer buffer … frame dropped"). */ #define POOL_MAX 5 struct slot { AHardwareBuffer *ahb; - struct vkp_image *img; /* the AHB imported into the compositor's Turnip as a blit target */ + struct vkp_image *img; int w, h; - uint32_t fmt; /* AHARDWAREBUFFER_FORMAT_*: RGBA8888, or RGBA1010102 for an HDR picture */ - int release_fd; /* SurfaceFlinger's release fence: signals when it stopped reading; -1 = none */ - int busy; /* set on the layer, or still referenced by SurfaceFlinger */ + uint32_t fmt; + int release_fd; + int busy; }; -/* One Android display layer: its SurfaceControl, its own buffer pool and its own lifetime. The - * z-order is fixed by the id (game below, overlay above); nothing is ever re-ordered at runtime. */ struct layer { const char *name; int32_t z; - int pool_n; /* buffers in this layer's pool (the game cycles more) */ + int pool_n; struct slot slots[POOL_MAX]; ASurfaceControl *sc; - ANativeWindow *win; /* the output window sc was created on */ + ANativeWindow *win; int shown; - int cur_slot; /* pool slot of the buffer on the layer, -1 = none / not ours */ - void *cur_token; /* zero-copy: the game's buffer on the layer (NULL = a pool slot) */ + int cur_slot; + void *cur_token; ARect geo_src, geo_dst; int geo_valid; - int alloc_tier; /* how pool buffers are asked of gralloc (TIER_*), lowered on failure */ + int alloc_tier; uint64_t pool_modifier; int first_logged; int64_t drop_logged_ns; - unsigned drops_unlogged; /* frames dropped since the last "no free layer buffer" line */ - /* The last pool layout written to the log, so a line is written per change, not per buffer. */ + unsigned drops_unlogged; + int logged_w, logged_h, logged_tier; uint32_t logged_fmt; uint64_t logged_mod; - int votes_rate; /* 1: this layer carries the game's cadence (the game layer) */ - float fps_applied; /* the vote the live SurfaceControl already carries (-1 = none yet) */ - int recreate_pending; /* composition recovery: swap this layer's SurfaceControl for a fresh - * one inside the next frame's transaction (see swap_sc_begin) */ - int32_t ds_applied; /* dataspace the live SurfaceControl carries; -1 = never set on it - * (untouched, today's UNKNOWN), see apply_colour */ - uint32_t md_applied; /* identity of the image description whose metadata it carries, 0 = none */ - float hr_applied; /* desired HDR headroom the live SurfaceControl carries; < 0 = never set */ + int votes_rate; + float fps_applied; + int recreate_pending; + + int32_t ds_applied; + + uint32_t md_applied; + float hr_applied; }; static struct layer g_layers[SC_LAYER_COUNT]; static void apply_frame_rate(ASurfaceTransaction *tx, struct layer *l); static void coverage_text(const struct layer *l, char *out, size_t n); static int g_layers_ready; -static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; /* pools + callback state */ -static int g_pending_cb; /* OnComplete callbacks not yet delivered */ -static unsigned g_stat_layer_frames; /* frames put on a layer through our own buffers */ +static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; +static int g_pending_cb; +static unsigned g_stat_layer_frames; static int g_two_logged; -/* A small buffer set on a layer when it is hidden or retired, so SurfaceFlinger replaces (and - * releases, with a fence) the game's buffer instead of holding it while the layer is invisible. */ static AHardwareBuffer *g_blank; static void layers_init(void) { @@ -231,10 +206,10 @@ static struct layer *layer_of(int id) { struct complete_ctx { ASurfaceControl *sc; - int layer; /* which layer's pool prev_slot belongs to */ - int prev_slot; /* pool buffer this transaction replaced: free once its release fence is known */ - void *prev_token; /* or the game's buffer it replaced: ahb_swapchain gets its release fence */ - int retire; /* release sc after this transaction (hide + reparent) */ + int layer; + int prev_slot; + void *prev_token; + int retire; }; static int64_t now_ns(void) { @@ -242,7 +217,6 @@ static int64_t now_ns(void) { return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec; } -/* Binder thread. */ static void on_complete(void *context, ASurfaceTransactionStats *stats) { struct complete_ctx *ctx = context; pthread_mutex_lock(&g_lock); @@ -257,15 +231,12 @@ static void on_complete(void *context, ASurfaceTransactionStats *stats) { pthread_mutex_unlock(&g_lock); if (ctx->prev_token) { int fd = (stats && ctx->sc) ? api.prevReleaseFence(stats, ctx->sc) : -1; - ahb_swapchain_layer_released(ctx->prev_token, fd); /* takes the fd */ + ahb_swapchain_layer_released(ctx->prev_token, fd); } if (ctx->retire && ctx->sc) api.release(ctx->sc); free(ctx); } -/* `sc` is the SurfaceControl the REPLACED buffer sits on, which is not always l->sc: during a - * composition-recovery swap the outgoing buffer belongs to the old SurfaceControl, and both its - * release fence and its release must be taken from that one. */ static int add_complete_on(ASurfaceTransaction *tx, struct layer *l, ASurfaceControl *sc, int prev_slot, void *prev_token, int retire) { struct complete_ctx *ctx = calloc(1, sizeof(*ctx)); @@ -283,7 +254,6 @@ static int add_complete(ASurfaceTransaction *tx, struct layer *l, int prev_slot, return add_complete_on(tx, l, l->sc, prev_slot, prev_token, retire); } -/* The stand-in buffer for a hidden/retired layer (allocated once, black, shared by both layers). */ static AHardwareBuffer *blank_buffer(void) { if (g_blank) return g_blank; AHardwareBuffer_Desc d = { @@ -300,15 +270,11 @@ static AHardwareBuffer *blank_buffer(void) { return g_blank; } -/* Take the current buffer (pool slot or the game's) off the layer by putting the blank one on it, - * so SurfaceFlinger releases it with a fence through this transaction's callback. */ static void replace_with_blank(ASurfaceTransaction *tx, struct layer *l) { AHardwareBuffer *b = blank_buffer(); if (b) api.setBuffer(tx, l->sc, b, -1); } -/* Everything a SurfaceControl needs on its way out, as transaction ops: give the buffer back (so - * SurfaceFlinger releases the game's, with a fence), hide it, unparent it. */ static void retire_ops(ASurfaceTransaction *tx, ASurfaceControl *sc) { AHardwareBuffer *b = blank_buffer(); if (b) api.setBuffer(tx, sc, b, -1); @@ -316,8 +282,6 @@ static void retire_ops(ASurfaceTransaction *tx, ASurfaceControl *sc) { api.reparent(tx, sc, NULL); } -/* Hide + detach one layer; the SurfaceControl is released from the transaction's callback (the - * buffer on it stays referenced by SurfaceFlinger until then). */ static void retire_sc(struct layer *l) { if (!l->sc) return; ASurfaceTransaction *tx = api.txCreate(); @@ -333,13 +297,11 @@ static void retire_sc(struct layer *l) { droiddeck_log("layer", "%s: SurfaceControl retired (window %p)", l->name, (void *)l->win); l->sc = NULL; l->win = NULL; l->shown = 0; l->cur_slot = -1; l->cur_token = NULL; l->geo_valid = 0; - l->fps_applied = -1.0f; /* the next SurfaceControl carries no vote until it is re-applied */ - l->recreate_pending = 0; /* a fresh SurfaceControl is coming anyway */ - l->ds_applied = -1; l->md_applied = 0; l->hr_applied = -1.0f; /* ...and no colour tag either */ + l->fps_applied = -1.0f; + l->recreate_pending = 0; + l->ds_applied = -1; l->md_applied = 0; l->hr_applied = -1.0f; } -/* Wait (bounded) for SurfaceFlinger to finish with every pool buffer of every layer, then free - * the pools. */ static void drain_and_free_pools(void) { int64_t deadline = now_ns() + 300000000LL; for (;;) { @@ -360,14 +322,11 @@ static void drain_and_free_pools(void) { s->release_fd = -1; } g_layers[i].first_logged = 0; - g_layers[i].logged_w = g_layers[i].logged_h = 0; /* the next pool's layout is logged again */ + g_layers[i].logged_w = g_layers[i].logged_h = 0; } pthread_mutex_unlock(&g_lock); } -/* Native-handle sniff, the same one Mesa's u_gralloc fallback uses (u_gralloc_fallback.c): a QTI - * gralloc private_handle_t carries the magic 'gmsm' as its first int and the UBWC flag - * (PRIV_FLAGS_UBWC_ALIGNED, 0x08000000) in the next one. Returns 0 when the layout is unknown. */ static int sniff_modifier(const struct droiddeck_native_handle *h, uint64_t *mod) { const uint32_t gmsm = ('g' << 24) | ('m' << 16) | ('s' << 8) | 'm'; if (!h || h->numFds < 1 || h->numInts < 2) return 0; @@ -376,18 +335,10 @@ static int sniff_modifier(const struct droiddeck_native_handle *h, uint64_t *mod return 1; } -static int g_alloc_failed; /* set by alloc_slot when gralloc or the import refused a buffer */ +static int g_alloc_failed; -/* How a layer's pool buffers are asked of gralloc, best first; a layer only ever moves down. - * TIER_UBWC GPU render target + sampled + composer overlay + AHARDWAREBUFFER_USAGE_VENDOR_0 (bit 28): - * QTI gralloc's GRALLOC_USAGE_PRIVATE_ALLOC_UBWC. Without it QTI gralloc allocates LINEAR even - * for a GPU render target (gr_allocator.cpp IsUBwcEnabled: an explicit UBWC format, the - * private UBWC bit or CLIENT_TARGET, and no CPU bit). Whether gralloc really compressed the - * buffer is read back from its handle (sniff_modifier), never assumed. - * TIER_PLAIN the same without the vendor bit (a gralloc that refuses unknown bits; what we asked before). - * TIER_LINEAR plus a CPU bit, so the layout is known to be linear even when the handle is unreadable. */ enum { TIER_UBWC, TIER_PLAIN, TIER_LINEAR }; -#define DROIDDECK_AHB_USAGE_VENDOR_UBWC (1ULL << 28) /* AHARDWAREBUFFER_USAGE_VENDOR_0 */ +#define DROIDDECK_AHB_USAGE_VENDOR_UBWC (1ULL << 28) static const char *tier_name(int tier) { return tier == TIER_UBWC ? "asked for UBWC" : tier == TIER_PLAIN ? "no UBWC request" : "linear by request (CPU bit)"; @@ -400,8 +351,6 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm AHardwareBuffer_Desc d = { .width = (uint32_t)w, .height = (uint32_t)h, .layers = 1, .format = fmt, - /* GPU render target (the blit writes it) + sampled (SurfaceFlinger's GPU fallback reads it) + - * composer overlay (the display scans it out). See the tiers above for the rest. */ .usage = AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT | AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_COMPOSER_OVERLAY | (tier == TIER_UBWC ? DROIDDECK_AHB_USAGE_VENDOR_UBWC : 0) | @@ -422,8 +371,6 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm const struct droiddeck_native_handle *nh = api.getNativeHandle(ahb); uint64_t mod = MOD_LINEAR; if (!sniff_modifier(nh, &mod)) { - /* Not a handle we can read: the only layout we can assume is linear, and only if the buffer - * was asked for with a CPU bit (gralloc must not have compressed it). */ if (tier < TIER_LINEAR) { AHardwareBuffer_release(ahb); l->alloc_tier = TIER_LINEAR; @@ -434,8 +381,6 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm mod = MOD_LINEAR; } int fd = (nh && nh->numFds > 0) ? nh->data[0] : -1; - /* The test create: vkp_image_import_dmabuf creates the image with gralloc's explicit layout, so a - * layout the compositor's Turnip refuses falls back a tier instead of drawing garbage. */ struct vkp_image *img = fd >= 0 ? vkp_image_import_dmabuf(fd, fmt == AHB_RGB10A2 ? DRM_ABGR2101010 : DRM_ABGR8888, mod, w, h, got.stride * 4, 0, 1) : NULL; if (!img) { @@ -462,7 +407,6 @@ static int alloc_slot(struct layer *l, struct slot *s, int w, int h, uint32_t fm return -1; } -/* 1 when a sync_file has signalled (the display has stopped reading that buffer). */ static int fence_signalled(int fd) { struct pollfd p = {.fd = fd, .events = POLLIN}; int r; @@ -470,12 +414,6 @@ static int fence_signalled(int fd) { return r > 0; } -/* A slot SurfaceFlinger has handed back, holding a w x h buffer of AHB format fmt. -1 = none free (or - * the buffer could not be made). A free slot whose release fence has already signalled is preferred; - * otherwise the fence of the first free one is handed back in *wait_fd for the copy to wait on ON THE - * GPU (vkp_blit_image / vkp_pass_copy_to consume it), so the compositor thread no longer blocks here. - * Only a driver without VK_KHR_external_semaphore_fd still waits on the CPU, bounded, as before. - * *wait_fd is -1 whenever nothing needs waiting for (and on every failure). */ static int take_free_slot(struct layer *l, int w, int h, uint32_t fmt, int *wait_fd) { int idx = -1, pending = -1, fd = -1; *wait_fd = -1; @@ -493,8 +431,6 @@ static int take_free_slot(struct layer *l, int w, int h, uint32_t fmt, int *wait if (fd >= 0 && fence_signalled(fd)) { close(fd); fd = -1; } struct slot *s = &l->slots[idx]; if (s->ahb && (s->w != w || s->h != h || s->fmt != fmt)) { - /* Size or format change: this slot is free, so it can be replaced at once (the display keeps its - * own reference to the old buffer for as long as it reads it; the new one needs no wait). */ if (fd >= 0) { close(fd); fd = -1; } vkp_image_destroy(s->img); AHardwareBuffer_release(s->ahb); memset(s, 0, sizeof(*s)); s->release_fd = -1; @@ -504,7 +440,6 @@ static int take_free_slot(struct layer *l, int w, int h, uint32_t fmt, int *wait int r; do { r = poll(&p, 1, 100); } while (r < 0 && errno == EINTR); if (r == 0) { - /* Still read by the display: keep its fence with it (the next use waits again), drop this frame. */ pthread_mutex_lock(&g_lock); if (s->release_fd < 0) s->release_fd = fd; else close(fd); pthread_mutex_unlock(&g_lock); @@ -521,7 +456,6 @@ static int take_free_slot(struct layer *l, int w, int h, uint32_t fmt, int *wait return idx; } -/* ---- public --------------------------------------------------------------------------------- */ int sc_layer_available(void) { return load_api() == 0; } @@ -529,8 +463,8 @@ void sc_layer_probe_dmabuf_fd(int fd) { static int done; if (done || fd < 0) return; done = 1; - struct { uint32_t flags; int32_t fd; } exp = {.flags = 1u /* DMA_BUF_SYNC_READ */, .fd = -1}; - /* DMA_BUF_IOCTL_EXPORT_SYNC_FILE = _IOWR('b', 2, struct dma_buf_export_sync_file) */ + struct { uint32_t flags; int32_t fd; } exp = {.flags = 1u , .fd = -1}; + if (ioctl(fd, _IOWR('b', 2, exp), &exp) == 0) { droiddeck_log("layer", "kernel exports sync_file fences from the game's dma-buf: zero-copy acquire fences can come from the buffer itself"); if (exp.fd >= 0) close(exp.fd); @@ -540,8 +474,6 @@ void sc_layer_probe_dmabuf_fd(int fd) { } } -/* One layer's SurfaceControl on the current output window (created on first use, re-created after - * a window change). -1 = no window / no API. */ static int ensure_sc(struct layer *l) { if (load_api() != 0) return -1; ANativeWindow *win = vkp_window(); @@ -564,16 +496,11 @@ static int ensure_sc(struct layer *l) { return 0; } -/* Geometry of a w x h buffer covering the scene, through the same mapping the blit path and the - * app's touch mapping use. 1 = r filled, 0 = nothing of it is on screen, -1 = no mapping. */ static int layer_geometry(int w, int h, int scene_w, int scene_h, int r[8]) { if (vkp_update_map(scene_w, scene_h) != 0) return -1; return vkp_map_rect(w, h, scene_w, scene_h, r) ? 1 : 0; } -/* The GAME layer's last placement, kept across SurfaceControl retires and swaps (l->geo_valid is - * per-SurfaceControl and is cleared by both). This is the src -> dst the display actually works - * with, and it is what decides whether a second layer is affordable - see overlay_affordable(). */ static ARect g_game_src, g_game_dst; static int g_game_geo_known; @@ -581,7 +508,7 @@ static void apply_geometry(ASurfaceTransaction *tx, struct layer *l, const int r ARect srcR = {r[0], r[1], r[2], r[3]}, dstR = {r[4], r[5], r[6], r[7]}; if (l == &g_layers[SC_LAYER_GAME]) { g_game_src = srcR; g_game_dst = dstR; g_game_geo_known = 1; } if (!l->geo_valid || memcmp(&srcR, &l->geo_src, sizeof(srcR)) || memcmp(&dstR, &l->geo_dst, sizeof(dstR))) { - api.setGeometry(tx, l->sc, &srcR, &dstR, 0 /* no transform: the DPU scales, never rotates */); + api.setGeometry(tx, l->sc, &srcR, &dstR, 0 ); l->geo_src = srcR; l->geo_dst = dstR; l->geo_valid = 1; char cov[96] = ""; if (l->ds_applied > 0) coverage_text(l, cov, sizeof(cov)); @@ -590,16 +517,11 @@ static void apply_geometry(ASurfaceTransaction *tx, struct layer *l, const int r } } -/* Add the vote to tx when it differs from what this layer carries (compositor thread). Only the - * layer the game presents on carries the game's rate; every other layer is voted 0, so a slow - * overlay window can never hold the panel at the game's cadence (or the game's at the overlay's). */ static void apply_frame_rate(ASurfaceTransaction *tx, struct layer *l) { float want = l->votes_rate ? atomic_load(&g_fps_want) : 0.0f; if (want == l->fps_applied) return; if (!api.setFrameRateStrategy && !api.setFrameRate) return; if (api.setFrameRateStrategy) { - /* ALWAYS, like the app's own surface vote: the seamless-only default is ignored by a panel - * sitting at its peak rate, which is exactly the case we need to move. */ api.setFrameRateStrategy(tx, l->sc, want, ASC_FRAME_RATE_COMPAT_DEFAULT, ASC_CHANGE_FRAME_RATE_ALWAYS); } else { api.setFrameRate(tx, l->sc, want, ASC_FRAME_RATE_COMPAT_DEFAULT); @@ -610,13 +532,6 @@ static void apply_frame_rate(ASurfaceTransaction *tx, struct layer *l) { else if (!first) droiddeck_log("layer", "display frame-rate vote on %s cleared (panel runs free)", l->name); } -/* ---- colour (HDR, droiddeck_color.h) ------------------------------------------------------------- - * The layer is told what its buffer's pixels MEAN - the dataspace (BT2020_PQ for an HDR10 frame) and - * the game's mastering / content-light metadata - in the same transaction as the buffer, so the - * display never shows an HDR frame decoded as sRGB or the other way round. A layer that has never been - * tagged is never touched: sessions without an HDR description make no colour call at all. */ -/* How much of the screen the layer's picture covers ("covers 80% of the screen (1920x1080 of 2400x1080)"): - * some phones only switch to HDR for an HDR layer above a minimum area. Empty when unknown. */ static void coverage_text(const struct layer *l, char *out, size_t n) { int ow = 0, oh = 0; vkp_output_size(&ow, &oh); @@ -628,8 +543,6 @@ static void coverage_text(const struct layer *l, char *out, size_t n) { snprintf(out, n, "covers %.0f%% of the screen (%dx%d of %dx%d)", pct > 100.0 ? 100.0 : pct, w, h, ow, oh); } -/* API 35: ask the display for HDR headroom explicitly while the layer carries an HDR frame, and clear the - * request (0 = no preference) when it stops. Only on change; a layer never tagged is never touched. */ static void apply_headroom(ASurfaceTransaction *tx, struct layer *l, const struct droiddeck_color *c) { static int said_missing; if (!api.setDesiredHdrHeadroom) { @@ -642,7 +555,7 @@ static void apply_headroom(ASurfaceTransaction *tx, struct layer *l, const struc return; } const float want = (c && c->dataspace) ? droiddeck_color_desired_headroom(c, NULL, 0) : 0.0f; - if (l->hr_applied < 0.0f && want == 0.0f && !(c && c->dataspace)) return; /* never asked: leave it be */ + if (l->hr_applied < 0.0f && want == 0.0f && !(c && c->dataspace)) return; if (l->hr_applied >= 0.0f && fabsf(want - l->hr_applied) < 0.005f) return; char why[200] = ""; if (c && c->dataspace) droiddeck_color_desired_headroom(c, why, sizeof(why)); @@ -662,7 +575,7 @@ static void apply_colour(ASurfaceTransaction *tx, struct layer *l, const struct int32_t want = c ? c->dataspace : DROIDDECK_ADATASPACE_UNKNOWN; uint32_t md = (c && c->dataspace) ? c->identity : 0; if (!api.setBufferDataSpace) return; - if (l->ds_applied < 0 && want == DROIDDECK_ADATASPACE_UNKNOWN) return; /* never tagged: leave it be */ + if (l->ds_applied < 0 && want == DROIDDECK_ADATASPACE_UNKNOWN) return; apply_headroom(tx, l, want ? c : NULL); if (want == l->ds_applied && md == l->md_applied) return; api.setBufferDataSpace(tx, l->sc, want); @@ -676,7 +589,6 @@ static void apply_colour(ASurfaceTransaction *tx, struct layer *l, const struct .maxLuminance = c->max_lum, .minLuminance = c->min_lum}; } if (cta_on) cta = (struct droiddeck_hdr_cta861_3){.maxContentLightLevel = c->max_cll, .maxFrameAverageLightLevel = c->max_fall}; - /* NULL clears what a previous description set: metadata never outlives the frame it belongs to. */ if (api.setHdrMetadata_smpte2086) api.setHdrMetadata_smpte2086(tx, l->sc, st_on ? &st : NULL); if (api.setHdrMetadata_cta861_3) api.setHdrMetadata_cta861_3(tx, l->sc, cta_on ? &cta : NULL); if (want) { @@ -706,8 +618,6 @@ void sc_layer_hdr_symbols(char *out, size_t size) { api.setDesiredHdrHeadroom ? "yes (asked for per HDR frame: content peak / SDR white)" : "no (Android < 15)"); } -/* Once, when a second layer first goes up: HWC only composes a few layers before SurfaceFlinger - * falls back to GPU client composition, so the count is deliberately capped and said out loud. */ static void log_layer_count(void) { if (g_two_logged) return; g_two_logged = 1; @@ -718,34 +628,12 @@ static void log_layer_count(void) { g_layers[SC_LAYER_OVERLAY].name, (int)g_layers[SC_LAYER_OVERLAY].z, SC_LAYER_COUNT); } -/* ---- composition recovery --------------------------------------------------------------------- - * When the overlay is retired and the scene is one fullscreen window again, the game layer is - * marked for a swap and the swap rides the NEXT frame: a fresh SurfaceControl is created here, the - * frame is put on it, and the old one is hidden and unparented IN THE SAME TRANSACTION. Because - * SurfaceFlinger applies a transaction atomically there is never a composited frame with neither - * layer on it - no black frame, and no dropped frame beyond the one layer creation. The outgoing - * buffer is released through the OLD SurfaceControl's callback, which also releases it. - * - * MEASURED 2026-09-14, and the news is bad: on the Pocket FIT this does NOT bring hardware - * composition back. The swap fires 6 ms after the overlay goes, SurfaceFlinger really does hand out - * a new layer (its id changes), the game never drops a frame - and the composer still reports - * `DEVICE/CLIENT` 24 s later. Neither does dropping the layer path entirely and re-creating the - * SurfaceControl after a gap. The ONE thing that clears it is HOME + resume, which re-creates the - * app's whole window and SurfaceView (`VRI[XServerDisplayActivity]#0` becomes `#4`) - so the sticky - * client-composition state belongs to the PARENT surface (or the display), not to this child layer. - * The phase-4 note that "only re-creating the GAME layer's SurfaceControl clears it" was inferred - * from HOME + resume and is wrong; see sc_layer.h. The swap is kept because it is free and correct - * and the mechanism may differ on hardware that does not rotate every layer - but do not claim it - * restores DEVICE composition, and do not log as if it did. - * - * Returns the old SurfaceControl (the caller must add retire_ops for it to the same transaction and - * name it in add_complete_on), or NULL when no swap is due. Compositor thread. */ static ASurfaceControl *swap_sc_begin(struct layer *l) { if (!l->recreate_pending) return NULL; l->recreate_pending = 0; - if (!l->sc) return NULL; /* nothing to swap; ensure_sc made a fresh one already */ + if (!l->sc) return NULL; ANativeWindow *win = vkp_window(); - if (!win || win != l->win) return NULL; /* the window changed: ensure_sc re-creates it anyway */ + if (!win || win != l->win) return NULL; ASurfaceControl *fresh = api.createFromWindow(win, l->name); if (!fresh) { droiddeck_log("error", "layer: %s: composition recovery could not create a new SurfaceControl", l->name); @@ -753,9 +641,6 @@ static ASurfaceControl *swap_sc_begin(struct layer *l) { } ASurfaceControl *old = l->sc; l->sc = fresh; - /* These describe the SurfaceControl, not the layer: the new one carries none of them yet. - * cur_slot / cur_token are deliberately NOT cleared - they name the buffer still on the OLD - * SurfaceControl, which is what the caller passes to add_complete_on as the one being replaced. */ l->shown = 0; l->geo_valid = 0; l->fps_applied = -1.0f; @@ -765,16 +650,13 @@ static ASurfaceControl *swap_sc_begin(struct layer *l) { return old; } -/* The transaction that puts pool slot `idx` of layer `l` on screen at `r`, as `color` (NULL = no - * description). 0 = applied. */ static int present_slot(struct layer *l, int idx, const int r[8], const struct droiddeck_color *color, int translucent) { struct slot *s = &l->slots[idx]; ASurfaceTransaction *tx = api.txCreate(); if (!tx) return -1; - ASurfaceControl *old = swap_sc_begin(l); /* this frame carries the recovery swap, if one is due */ + ASurfaceControl *old = swap_sc_begin(l); if (old) api.setZOrder(tx, l->sc, l->z); - /* The blit was waited for on the CPU, so no acquire fence is needed (-1). */ api.setBuffer(tx, l->sc, s->ahb, -1); api.setBufferTransparency(tx, l->sc, translucent ? ASC_TRANSPARENCY_TRANSLUCENT : ASC_TRANSPARENCY_OPAQUE); @@ -784,7 +666,7 @@ static int present_slot(struct layer *l, int idx, const int r[8], const struct d if (!l->shown) api.setVisibility(tx, l->sc, ASC_VISIBILITY_SHOW); if (old) retire_ops(tx, old); if (add_complete_on(tx, l, old ? old : l->sc, l->cur_slot, l->cur_token, old ? 1 : 0) != 0 && old) - api.release(old); /* no callback to retire it from (out of memory): let it go here */ + api.release(old); pthread_mutex_lock(&g_lock); s->busy = 1; pthread_mutex_unlock(&g_lock); @@ -797,8 +679,6 @@ static int present_slot(struct layer *l, int idx, const int r[8], const struct d return 0; } -/* No free buffer this frame: the display still holds all of them. Counted for the 10 s perf line, and - * logged at most every 30 s with the number of frames dropped since the last line. */ static unsigned g_pool_drops; static void log_drop(struct layer *l) { g_pool_drops++; @@ -828,8 +708,6 @@ int sc_layer_present_ahb(AHardwareBuffer *ahb, int w, int h, int acquire_fd, voi if (g < 0) goto unavailable; if (g == 0) { if (acquire_fd >= 0) close(acquire_fd); sc_layer_hide(); return 1; } if (token == l->cur_token && l->shown && !l->recreate_pending) { - /* The same frame again (the scene was redrawn for another reason): the display already - * has it; only the placement may have changed (or, rarely, its description). */ if (acquire_fd >= 0) close(acquire_fd); ASurfaceTransaction *tx = api.txCreate(); if (tx) { @@ -841,22 +719,20 @@ int sc_layer_present_ahb(AHardwareBuffer *ahb, int w, int h, int acquire_fd, voi } ASurfaceTransaction *tx = api.txCreate(); if (!tx) goto unavailable; - ASurfaceControl *old = swap_sc_begin(l); /* this frame carries the recovery swap, if one is due */ + ASurfaceControl *old = swap_sc_begin(l); if (old) api.setZOrder(tx, l->sc, l->z); - api.setBuffer(tx, l->sc, ahb, acquire_fd); /* the transaction owns the fence */ + api.setBuffer(tx, l->sc, ahb, acquire_fd); api.setBufferTransparency(tx, l->sc, ASC_TRANSPARENCY_OPAQUE); apply_geometry(tx, l, r); apply_frame_rate(tx, l); apply_colour(tx, l, color); if (!l->shown) api.setVisibility(tx, l->sc, ASC_VISIBILITY_SHOW); if (old) retire_ops(tx, old); - /* Same buffer, new SurfaceControl: it is NOT free, so no release is reported for it - the swap - * moved it rather than taking it off screen. */ if (add_complete_on(tx, l, old ? old : l->sc, l->cur_slot, l->cur_token == token ? NULL : l->cur_token, old ? 1 : 0) != 0) { - api.txDelete(tx); /* the fence went with the transaction */ + api.txDelete(tx); if (old) api.release(old); - l->ds_applied = -1; l->md_applied = 0; l->hr_applied = -1.0f; /* the colour tag went with it too: re-send it next time */ + l->ds_applied = -1; l->md_applied = 0; l->hr_applied = -1.0f; return -1; } api.txApply(tx); @@ -864,8 +740,6 @@ int sc_layer_present_ahb(AHardwareBuffer *ahb, int w, int h, int acquire_fd, voi l->cur_slot = -1; l->cur_token = token; if (!l->shown) { l->shown = 1; droiddeck_log("layer", "%s: layer shown", l->name); } - /* The notification is re-armed for each session; the layer and its logging flag can - * outlive it. Let the notification's own one-shot decide whether to fire. */ if (!l->first_logged) l->first_logged = 1; vkp_signal_first_frame(); if (color && color->dataspace) droiddeck_color_frame_shown(color, DROIDDECK_HDR_ZERO_COPY, ahb_format); @@ -882,7 +756,7 @@ int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const stru int r[8]; int g = layer_geometry(sw, sh, scene_w, scene_h, r); if (g < 0) return -1; - if (g == 0) { sc_layer_hide(); return 0; } /* nothing of it is on screen */ + if (g == 0) { sc_layer_hide(); return 0; } int wait_fd; int idx = take_free_slot(l, sw, sh, AHB_RGBA8, &wait_fd); @@ -905,15 +779,13 @@ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int int rw = 0, rh = 0, r[8]; if (!draws || n <= 0 || ensure_sc(l) != 0) return -1; if (vkp_update_map(scene_w, scene_h) != 0) return -1; - /* Compose + run the effects chain first: the chain's result size (a scaling mode resizes to - * the scene's mapped output size) decides how big the layer buffer has to be. */ if (vkp_pass_begin(scene_w, scene_h, draws, n, &rw, &rh) != 0) return -1; if (!vkp_map_rect(rw, rh, scene_w, scene_h, r)) { vkp_pass_abort(); sc_layer_hide(); return 0; } int wait_fd; int idx = take_free_slot(l, rw, rh, AHB_RGBA8, &wait_fd); if (idx < 0) { vkp_pass_abort(); log_drop(l); return 0; } if (vkp_pass_copy_to(l->slots[idx].img, wait_fd) != 0) return -1; - if (present_slot(l, idx, r, NULL, 0) != 0) return -1; /* the effects chain's result is 8-bit sRGB */ + if (present_slot(l, idx, r, NULL, 0) != 0) return -1; if (!l->first_logged) { l->first_logged = 1; droiddeck_log("layer", "presenting %dx%d frames on their own SurfaceControl layer (%s pool, %d buffers); " @@ -924,14 +796,6 @@ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int return 0; } -/* ---- the HDR picture (hdr_compose.h) ------------------------------------------------------------ - * An HDR game that cannot be alone on its layer: the WHOLE scene, composed into one 10-bit PQ BT.2020 - * picture with the effects applied, goes onto the game layer - one layer, tagged BT2020_PQ with the - * game's metadata, so the display composes it as HDR (and a window above the game never needs the - * second layer that costs this panel its hardware composition). 10-bit buffers; if gralloc or the - * import refuses those, 8-bit ones carry the same tagged picture (colours right, precision less). - * With the drawer's HDR output switch off (hf->tonemap) the same picture is composed tone-mapped to - * sRGB instead and goes on the ordinary 8-bit buffers, UNtagged - an SDR frame like any other. */ static uint32_t g_hdr_pool_fmt = AHB_RGB10A2; int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, @@ -955,7 +819,7 @@ int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct } if (idx < 0) { vkp_pass_abort(); log_drop(l); return 0; } if (vkp_pass_copy_to(l->slots[idx].img, wait_fd) != 0) return -1; - if (present_slot(l, idx, r, tm ? NULL : color, 0) != 0) return -1; /* tone-mapped = plain sRGB: no tag */ + if (present_slot(l, idx, r, tm ? NULL : color, 0) != 0) return -1; if (color && color->dataspace) droiddeck_color_frame_shown(color, tm ? DROIDDECK_HDR_TONEMAPPED : DROIDDECK_HDR_COMPOSED, tm ? AHB_RGBA8 : g_hdr_pool_fmt); static int said = -1; @@ -980,40 +844,17 @@ int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct return 0; } -/* ---- is a SECOND display layer affordable on this display? ----------------------------------- - * MEASURED on the Pocket FIT (2026-09-14, three times): while a second layer is up, this hardware - * composer hands the WHOLE frame back to the GPU (`DEVICE/CLIENT`), and nothing short of re-creating - * the app's window brings it back - so on this display the overlay layer costs more than it saves, - * every time, for the rest of the session. Where it costs nothing it is still the better path (the - * game keeps copy-free frames with a window on top), so this is a GATE, not a removal. - * - * The gate is read from what the layer path actually knows, never from a device or panel list: - * - rotation: vkp_surface_rotation_degrees(), i.e. VkSurfaceCapabilitiesKHR::currentTransform - - * what the presentation engine says it does to every layer we hand it; - * - scale: the GAME layer's own src -> dst rectangles, the ones passed to setGeometry. - * Rotated AND scaled is the combination that was measured to cost composition (the DPU's rotator - * has to take a scaled source); either alone, or neither, is left as it was. - * - * Why not just measure the composition type after raising the layer and back out if it comes back - * CLIENT - which would beat predicting? Because no app can read it. The composition type lives in - * the Composer HAL; ASurfaceTransactionStats exposes latch time and fences and nothing else, and - * the only place the value is published is `dumpsys android.hardware.graphics.composer3.IComposer`, - * which needs android.permission.DUMP and string-parsing. It would also arrive at least a frame - * late, so backing out would itself be the visible change we are avoiding. Hence the rule. */ static int overlay_affordable(int *deg, int sw[2], int dw[2]) { *deg = vkp_surface_rotation_degrees(); sw[0] = sw[1] = dw[0] = dw[1] = 0; - if (!g_game_geo_known) return 1; /* the game has never been placed: nothing to weigh against */ - if (*deg <= 0) return 1; /* unknown (-1) or upright: leave today's behaviour alone */ + if (!g_game_geo_known) return 1; + if (*deg <= 0) return 1; sw[0] = g_game_src.right - g_game_src.left; sw[1] = g_game_src.bottom - g_game_src.top; dw[0] = g_game_dst.right - g_game_dst.left; dw[1] = g_game_dst.bottom - g_game_dst.top; if (sw[0] <= 0 || sw[1] <= 0) return 1; - return !(dw[0] != sw[0] || dw[1] != sw[1]); /* rotated AND scaled -> not affordable */ + return !(dw[0] != sw[0] || dw[1] != sw[1]); } -/* -1 = not decided yet, 0 = raising the overlay, 1 = declining it. Logged on every change, so a - * tester's log says why they are seeing the copy path instead of two layers - and says it again if - * the placement changes the answer (e.g. a fullscreen mode that stops scaling the game). */ static int g_overlay_declined = -1; int sc_layer_overlay_affordable(void) { @@ -1040,8 +881,6 @@ int sc_layer_overlay_affordable(void) { int sc_layer_present_overlay(struct vkp_image *src, const int geo[8], int translucent) { struct layer *l = layer_of(SC_LAYER_OVERLAY); if (!src || !geo) return -1; - /* The caller decides with sc_layer_overlay_affordable() BEFORE it commits the game to a layer; - * this is only a guard so the layer can never go up behind that decision's back. */ if (!sc_layer_overlay_affordable()) return -1; if (ensure_sc(l) != 0) return -1; int sw = vkp_image_width(src), sh = vkp_image_height(src); @@ -1049,8 +888,6 @@ int sc_layer_present_overlay(struct vkp_image *src, const int geo[8], int transl int wait_fd; int idx = take_free_slot(l, sw, sh, AHB_RGBA8, &wait_fd); if (idx < 0) { log_drop(l); return 0; } - /* The window is copied into the layer buffer 1:1; `geo` crops it and places it, so the layer - * is exactly the window's rectangle on screen and nothing else is blended anywhere. */ if (vkp_blit_image(src, l->slots[idx].img, wait_fd) != 0) return -1; if (present_slot(l, idx, geo, NULL, translucent) != 0) return -1; if (!l->first_logged) { @@ -1060,8 +897,6 @@ int sc_layer_present_overlay(struct vkp_image *src, const int geo[8], int transl return 0; } -/* Hide one layer: the buffer comes off with it (a hidden layer would keep the game's buffer - or - * the pool slot - referenced, and the game needs it back to keep presenting the other way). */ static void hide_layer(struct layer *l) { if (!l->sc || !l->shown || api.state != 1) return; ASurfaceTransaction *tx = api.txCreate(); @@ -1076,10 +911,6 @@ static void hide_layer(struct layer *l) { void sc_layer_hide(void) { layers_init(); int said = g_layers[SC_LAYER_GAME].shown || g_layers[SC_LAYER_OVERLAY].sc; - /* Top down, so nothing of the scene is ever uncovered for a frame. The overlay layer is let go - * of entirely rather than hidden (same reason as sc_layer_hide_overlay: a live second - * SurfaceControl keeps SurfaceFlinger composing on the GPU); the game layer keeps its - * SurfaceControl, since it goes up and down with every effects/frame-generation toggle. */ if (g_layers[SC_LAYER_OVERLAY].sc) sc_layer_hide_overlay(); if (g_layers[SC_LAYER_GAME].shown) hide_layer(&g_layers[SC_LAYER_GAME]); if (said) droiddeck_log("layer", "layers hidden (scene is not a single fullscreen window)"); @@ -1089,18 +920,9 @@ void sc_layer_hide_overlay(void) { layers_init(); struct layer *l = &g_layers[SC_LAYER_OVERLAY]; if (!l->sc) return; - /* RETIRED, not just hidden, so the overlay layer lives exactly as long as the window above the - * game: a live second SurfaceControl is what puts SurfaceFlinger into GPU client composition - * on this hardware (see sc_layer.h), and a hidden one is still live. Measured caveat: letting - * it go does NOT by itself bring hardware composition back - on the Pocket FIT the fallback - * outlives the overlay and only clears when the GAME layer's SurfaceControl is re-created - * (HOME + resume). Retiring is still the right thing; it just is not the whole cure. The pool - * buffers stay allocated for the next window. */ if (l->shown) hide_layer(l); retire_sc(l); droiddeck_log("layer", "%s: gone (nothing is above the game any more)", l->name); - /* ...and that is the half the measurement said is not enough: arm the game layer's - * SurfaceControl swap, which the next frame performs (swap_sc_begin). */ if (g_layers[SC_LAYER_GAME].sc) g_layers[SC_LAYER_GAME].recreate_pending = 1; } diff --git a/app/src/main/cpp/waylandcomp/src/sc_layer.h b/app/src/main/cpp/waylandcomp/src/sc_layer.h index 1d8e6a11e..9df4cd80e 100644 --- a/app/src/main/cpp/waylandcomp/src/sc_layer.h +++ b/app/src/main/cpp/waylandcomp/src/sc_layer.h @@ -1,70 +1,5 @@ #ifndef SC_LAYER_H #define SC_LAYER_H -/* - * Layer mode (DROIDDECK_WAYLAND_ZERO_COPY=1): the scene is handed to SurfaceFlinger as a small, - * deliberately ordered SET of Android display layers - ASurfaceControl children of the - * compositor's SurfaceView - instead of being blitted into the compositor's own swapchain. - * - * app window ......... Compose UI, the in-game drawer, the HUD, the on-screen controls and - * the pointer arrow: ordinary Android views, ALWAYS above everything - * below (the SurfaceView is not Z-on-top, so its whole subtree is under - * the window's own content). None of the layers below take input: - * an ASurfaceControl has no input channel, so touch and mouse keep - * reaching the SurfaceView exactly as they did. - * +-- SurfaceView ..... the compositor's Vulkan swapchain; black while layer mode is up - * +-- z=1 "droiddeck_wayland_game" ...... the one fullscreen game window - * +-- z=2 "droiddeck_wayland_overlay" ... at most one window drawn ABOVE the game - * - * TWO layers is the hard cap (SC_LAYER_COUNT), and deliberately so: HWC composes only a few - * layers before SurfaceFlinger falls back to GPU client composition, which would throw away the - * whole benefit. The count is logged when the second layer first goes up. - * - * MEASURED on the Pocket FIT (Adreno 750, portrait panel + landscape session, so every layer is - * ROT_90 + scaled), `dumpsys android.hardware.graphics.composer3.IComposer/default`: one layer is - * `composition: DEVICE/DEVICE`, effects on the layer keep it DEVICE/DEVICE - but a SECOND layer - * flips the whole frame to `DEVICE/CLIENT`. It does not come back when the overlay goes away. - * The overlay layer is retired rather than hidden for that reason, but the fallback outlives it. - * The likely mechanism is the DPU's rotator budget (one rotated+scaled layer), not the layer count - * as such, so a device or orientation needing no rotation may well take both on the DPU. Even in - * client composition the overlay layer is not a loss - SurfaceFlinger does the one blit the - * compositor would have done - but the hardware-composition win is only real for one layer. - * - * COMPOSITION RECOVERY (sc_layer.c, swap_sc_begin): retiring the overlay arms the game layer, and - * the NEXT frame is presented on a brand-new SurfaceControl while the old one is hidden and - * unparented in the SAME transaction. SurfaceFlinger applies a transaction atomically, so no - * composited frame is ever missing the game - no black frame, no dropped frame beyond the layer - * creation itself. One `layer` line is written when it happens. - * - * It does NOT restore hardware composition on this panel, and that was measured, not assumed - * (2026-09-14, Wizardry on the game layer + Wine's Task Manager on the overlay, reproduced twice): - * - two layers -> DEVICE/CLIENT - * - overlay retired + fresh game SurfaceControl -> still DEVICE/CLIENT, 24 s later too - * - layer path dropped entirely and re-created -> still DEVICE/CLIENT - * - drawer opened and closed, no second window -> DEVICE/DEVICE (control: the drawer is not the cause) - * - HOME + resume -> DEVICE/DEVICE - * HOME + resume re-creates the app's whole window and SurfaceView (`VRI[XServerDisplayActivity]#0` - * becomes `#4`), not just this child layer - so the sticky state belongs to the parent surface or - * the display. Re-creating the parent would cost a real black frame and a swapchain rebuild, which - * is worse than the few percent of GPU that client composition costs. The cure that actually keeps - * the win is PREVENTION, and it IS implemented: sc_layer_overlay_affordable() declines the second - * layer when the game layer is both rotated and scaled, and the window above the game goes down the - * copy path instead (what the pre-phase-4 code did) - one blit SurfaceFlinger would have done - * anyway, and the session keeps DEVICE composition throughout. Where a second layer costs nothing, - * nothing changes. - * - * What can be on the game layer, cheapest first: - * - the game's own gralloc buffer (ahb_swapchain.c, true zero-copy: no copy anywhere); - * - one blit of the game's frame into a compositor-allocated AHardwareBuffer (sc_layer_present); - * - with screen effects on, the compositor pass's result blitted into such a buffer - * (sc_layer_present_pass) - the game STAYS on its layer while a Look is applied, and the - * scene -> output mapping is done by the display (setGeometry) instead of a second GPU blit. - * The compositor never alpha-blends (it composes with blits, which overwrite), so every layer is - * marked OPAQUE and the overlay layer is cropped to the window it carries: the picture is the - * same as the copy path's, pixel for pixel. - * - * Compositor thread only, except the SurfaceFlinger OnComplete callbacks (binder threads), which - * only touch the pools under g_lock. sc_layer_hide*() is safe at any time. - */ #include <stddef.h> #include <stdint.h> #include <android/hardware_buffer.h> @@ -73,103 +8,39 @@ struct vkp_image; struct vkp_draw; struct droiddeck_color; -/* The layers, bottom first. The z-order is the array order (z = id + 1). */ enum sc_layer_id { SC_LAYER_GAME = 0, SC_LAYER_OVERLAY = 1, SC_LAYER_COUNT = 2 }; -/* 1 when this device has the SurfaceControl API the layers need (Android 10+, libnativewindow's - * getNativeHandle); the reason is logged (tag `layer`) the first time it is missing. */ int sc_layer_available(void); -/* One-shot: does the kernel export a sync_file from the game's dma-buf (DMA_BUF_IOCTL_EXPORT_SYNC_FILE)? - * Decides where a zero-copy acquire fence can come from; result goes to the session log. */ void sc_layer_probe_dmabuf_fd(int fd); -/* HDR (droiddeck_color.h): can a display layer be told its buffer's colour encoding here - * (ASurfaceTransaction_setBufferDataSpace, Android 10+)? Part of the HDR gate. */ int sc_layer_can_tag_hdr(void); -/* Which of the colour calls this libandroid has, for the gate's log line. */ void sc_layer_hdr_symbols(char *out, size_t size); -/* COLOUR on the game layer: `color` is the image description of the frame being presented (NULL = - * none, i.e. sRGB). A layer that was never tagged is never touched (no setBufferDataSpace call at all: - * a session without an HDR description behaves exactly as before); once tagged, a later untagged frame - * puts the layer back to UNKNOWN. SMPTE 2086 / CTA-861.3 metadata travel with the dataspace. */ -/* GAME layer: show `src` (the fullscreen window's imported frame, scene-sized) on it, placed - * through the current fullscreen mode / alignment mapping. 0 = shown (or deliberately dropped: no - * free buffer), -1 = the layer path is unavailable this frame and the caller must draw the old way. - * The copy into the layer buffer is 8-bit: an HDR frame keeps its tag (its colours stay right), and - * loses precision. */ int sc_layer_present(struct vkp_image *src, int scene_w, int scene_h, const struct droiddeck_color *color); -/* GAME layer, screen effects on: run the compositor pass (composite `draws` + the effects chain, - * vkp_pass_begin) and put its result on the layer. Same return values as sc_layer_present. */ int sc_layer_present_pass(const struct vkp_draw *draws, int n, int scene_w, int scene_h); -/* GAME layer, HDR (hdr_compose.h): the whole scene - `draws`, hf->is_hdr marking the HDR ones - composed - * into one 10-bit PQ BT.2020 picture with the effects applied (vkp_pass_begin_hdr), copied into a - * 10-bit layer buffer (8-bit where gralloc refuses those) and shown tagged with `color` (the topmost HDR - * draw's description). The overlay layer is not used: the picture already holds what is above the game. - * Same return values as sc_layer_present. */ struct vkp_hdr_frame; int sc_layer_present_hdr_scene(const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, int scene_w, int scene_h, const struct droiddeck_color *color); -/* GAME layer, zero-copy (ahb_swapchain.c): show the game's own w x h AHardwareBuffer, gated by - * acquire_fd (a sync_file the layer waits on before reading; owned by the callee, -1 = none). - * token identifies the buffer: once a later transaction replaces it (or the layer is hidden or - * retired), ahb_swapchain_layer_released(token, release_fd) reports SurfaceFlinger's release fence - * for it. Presenting the token already on the layer only updates the placement. 0 = on the layer, - * 1 = nothing of it is on screen (layer hidden, the buffer was not taken), -1 = unavailable this - * frame (the caller draws the old way). `color` / `ahb_format`: the frame's image description (NULL = - * none) and the buffer's AHARDWAREBUFFER_FORMAT_*, for the dataspace and the HDR evidence. */ int sc_layer_present_ahb(AHardwareBuffer *ahb, int w, int h, int acquire_fd, void *token, int scene_w, int scene_h, const struct droiddeck_color *color, uint32_t ahb_format); -/* Vote a panel refresh rate for the layer the game presents on (VRR / refresh-rate matching), the - * same rate and compatibility the app votes on its own surface with Surface.setFrameRate; 0 = no - * vote. Needed because a zero-copy game's frames go onto the GAME layer and never reach the app's - * surface, so the surface vote alone does not describe the game's cadence to SurfaceFlinger. Only - * the game layer ever carries it: the overlay layer is explicitly voted 0, so a window that redraws - * once a second can never hold (or drop) the panel at the game's cadence. Callable from any thread - * at any time (it only stores the rate); the compositor applies it with the layer's next - * transaction, and re-applies it whenever a SurfaceControl is re-created. A no-op on devices whose - * libandroid has no ASurfaceTransaction_setFrameRate. */ void sc_layer_set_frame_rate(float fps); -/* Is a SECOND display layer worth raising on THIS display? Ask before committing the game to its - * layer, not after: on a display where the answer is no, the whole scene must go down the copy path - * from the start, or the game layer would be shown and hidden again on every frame. - * - * The answer is computed from what the layer path knows - the rotation the presentation engine - * applies to everything we hand it (VkSurfaceCapabilitiesKHR::currentTransform) and the game - * layer's own src -> dst rectangles - and never from a device or panel allowlist. ROTATED AND - * SCALED is the combination measured to cost hardware composition (below); either alone is fine. - * The reason is written to the session log once, and again if the answer changes. */ int sc_layer_overlay_affordable(void); -/* OVERLAY layer: show `src` (one window's imported frame) above the game layer, at the placement - * `geo` = {src x0,y0,x1,y1 in image pixels, dst x0,y0,x1,y1 in output pixels} from vkp_map_draw. - * `translucent` preserves a client's alpha over the game for Steam notifications and overlays. - * 0 = shown or dropped, -1 = unavailable (the caller must fall back to the copy path). */ int sc_layer_present_overlay(struct vkp_image *src, const int geo[8], int translucent); -/* The scene is not a single fullscreen window this frame: hide every layer that is up. */ void sc_layer_hide(void); -/* Only the overlay layer: nothing is above the game any more. The game keeps its layer, but its - * SurfaceControl is swapped for a fresh one on the next frame (composition recovery, above - which - * on this panel does not in fact win hardware composition back; see the measurement there). */ void sc_layer_hide_overlay(void); -/* The output window changed or went away (compositor thread, from vkp_apply_window_request). */ void sc_layer_window_gone(void); -/* Frames the compositor put on a layer through one of its own buffers (a blit or the effects - * pass) since the last call - the zero-copy frames ahb_swapchain.c counts are NOT included. - * For the 10 s summary. */ unsigned sc_layer_frames_take(void); -/* Frames dropped since the last call because every layer buffer was still held by the display (the - * 10 s perf line; the "no free layer buffer" log line itself is rate-limited). */ unsigned sc_layer_drops_take(void); #endif diff --git a/app/src/main/cpp/waylandcomp/src/vk_loader.c b/app/src/main/cpp/waylandcomp/src/vk_loader.c index ef939911c..0cda81cd0 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_loader.c +++ b/app/src/main/cpp/waylandcomp/src/vk_loader.c @@ -1,4 +1,3 @@ -/* See vk_loader.h. */ #define _GNU_SOURCE #include "vk_loader.h" #include "../../adrenotools/include/adrenotools/driver.h" diff --git a/app/src/main/cpp/waylandcomp/src/vk_loader.h b/app/src/main/cpp/waylandcomp/src/vk_loader.h index 32a15ae87..e10950d0d 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_loader.h +++ b/app/src/main/cpp/waylandcomp/src/vk_loader.h @@ -1,11 +1,5 @@ #ifndef VK_LOADER_H #define VK_LOADER_H -/* - * Loads Turnip via adrenotools (the same path the app uses for the guest renderer) - * and resolves every Vulkan entry point through its vkGetInstanceProcAddr - NOT the - * process-default system Adreno driver, which lacks VK_EXT_image_drm_format_modifier / - * dmabuf import. All compositor Vulkan calls go through g_vk.* . - */ #define VK_USE_PLATFORM_ANDROID_KHR #include <vulkan/vulkan.h> @@ -29,7 +23,7 @@ X(EndCommandBuffer) X(ResetFences) X(QueueSubmit) X(QueuePresentKHR) X(WaitForFences) \ X(QueueWaitIdle) X(GetMemoryFdPropertiesKHR) \ X(GetImageSubresourceLayout) X(MapMemory) X(UnmapMemory) X(CmdClearColorImage) \ - /* screen-effect chain (effects_chain.c): graphics pipelines over full-screen quads */ \ + \ X(CreateShaderModule) X(DestroyShaderModule) X(CreatePipelineLayout) X(DestroyPipelineLayout) \ X(CreateGraphicsPipelines) X(DestroyPipeline) X(CreateRenderPass) X(DestroyRenderPass) \ X(CreateFramebuffer) X(DestroyFramebuffer) X(CreateImageView) X(DestroyImageView) \ @@ -37,7 +31,7 @@ X(CreateDescriptorPool) X(DestroyDescriptorPool) X(AllocateDescriptorSets) X(FreeDescriptorSets) \ X(UpdateDescriptorSets) X(CmdBeginRenderPass) X(CmdEndRenderPass) X(CmdBindPipeline) \ X(CmdBindDescriptorSets) X(CmdPushConstants) X(CmdDraw) X(CmdSetViewport) X(CmdSetScissor) \ - /* frame generation (framegen_bridge.c / vk_present.c per-present sync slots) */ \ + \ X(DestroySemaphore) struct vk_api { @@ -47,14 +41,10 @@ struct vk_api { }; extern struct vk_api g_vk; -/* Open Turnip via adrenotools (falls back to system libvulkan if any arg is NULL) - * and load global entry points. Returns 0 on success. */ int vk_loader_open(const char *driver_path, const char *library_name, const char *native_lib_dir); void vk_loader_load_instance(VkInstance instance); void vk_loader_load_device(VkDevice device); -/* The driver's vkGetInstanceProcAddr (NULL before vk_loader_open): for modules that resolve their - * own entry points, e.g. the frame-generation engines (framegen_engine.cpp). */ PFN_vkGetInstanceProcAddr vk_loader_gipa(void); #endif diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.c b/app/src/main/cpp/waylandcomp/src/vk_present.c index 1042635f2..29db4f000 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.c +++ b/app/src/main/cpp/waylandcomp/src/vk_present.c @@ -1,5 +1,3 @@ -/* Android-surface render backend - see vk_present.h. Uses Turnip via vk_loader - * (g_vk.*), not the process-default system Adreno driver. */ #define _POSIX_C_SOURCE 200809L #include "vk_present.h" #include "vk_loader.h" @@ -29,78 +27,54 @@ struct vkp_image { VkImage image; VkDeviceMemory mem; int w, h; - int dmabuf; /* imported from a client; owned by the foreign queue family */ - int blit_dst; /* layer mode: a pool buffer this backend blits into (never a source) */ - void *map; /* shm images: persistently mapped linear memory */ + int dmabuf; + int blit_dst; + void *map; VkDeviceSize offset, row_pitch; - int in_general; /* shm images: moved from PREINITIALIZED to GENERAL */ + int in_general; VkFormat fmt; - int sampled; /* created with SAMPLED usage: the alpha pass can read it */ + int sampled; }; -static ANativeWindow *g_window; /* the window frames go to; compositor thread only */ +static ANativeWindow *g_window; static char *g_driver_path, *g_library_name, *g_native_lib_dir; -static int g_dev_state; /* 0 = not yet, 1 = ok, -1 = failed, -2 = lost (VK_ERROR_DEVICE_LOST) */ +static int g_dev_state; static VkInstance g_inst; static VkPhysicalDevice g_pd; static VkDevice g_dev; static VkQueue g_queue; static uint32_t g_qfam; static VkCommandPool g_pool; -/* One command buffer + acquire/render-done semaphore pair per PRESENT. A scene frame is one - * present, or up to 1 + VKP_FG_MAX_GENERATIONS with frame generation (the generated frames go - * out first, the real frame last), each on its own slot so no semaphore ever carries two - * pending signals. Slot 0 is also the layer-mode blit's command buffer. */ #define MAX_PRESENTS (1 + VKP_FG_MAX_GENERATIONS) static VkCommandBuffer g_cmds[MAX_PRESENTS]; static VkSemaphore g_acqs[MAX_PRESENTS], g_rnds[MAX_PRESENTS]; static VkFence g_fence; -/* Extra swapchain images this swapchain was built with (frame generation: one per generated - * frame, so all presents of a scene frame queue without waiting for a vblank). */ static int g_swap_extra; -/* Format of the compositor pass's images (the scene image below, the effects chain's targets and - * the frame-generation ring): a blit converts every client format into it, the chains read RGBA. */ #define SCENE_FMT VK_FORMAT_R8G8B8A8_UNORM static VkPhysicalDeviceMemoryProperties g_memprops; static VkSurfaceKHR g_surface; static VkSwapchainKHR g_swapchain; -/* The display's own rotation of everything we present (VkSurfaceCapabilitiesKHR::currentTransform), - * captured when the swapchain is built. Not a panel or device allowlist: it is what the presentation - * engine says it will do to our layers. */ static VkSurfaceTransformFlagBitsKHR g_surface_transform; static int g_surface_transform_known; static VkImage *g_images; static uint32_t g_nimg; static VkExtent2D g_extent; -static int g_first_frame_done; /* one-shot: fire droiddeck_on_first_frame() on first present */ +static int g_first_frame_done; -/* The base surface under the display layers (vkp_base_black): 1 once a plain black frame has been - * presented on the CURRENT swapchain and nothing else since. Every other present clears it, and so does - * every swapchain teardown, so the next layer frame puts one black frame back first. While it is set, - * a frame that lives entirely on the display layers draws and presents nothing here. */ static int g_base_black; -/* The layer path presents nothing on the base surface while the game keeps its layer, so the swapchain's - * OUT_OF_DATE (a resized surface, a new transform) would never be seen there: check_surface_changed() - * asks the surface directly instead, at most every 100 ms. caps.currentExtent at the last build. */ static VkExtent2D g_caps_extent; static int64_t g_surface_checked_ns; -/* Letterbox bars: vkCmdClearColorImage clears whole images only, so the bars around the picture are - * blitted from this small black image instead of clearing the whole output under a picture that covers - * most of it. Created and cleared once with the device. 1 = ready, 0/-1 = not available (whole clear). */ #define BLACK_DIM 16 static VkImage g_black_img; static VkDeviceMemory g_black_mem; static int g_black_state; -/* The display's release fence of a layer buffer, as a GPU wait (VK_KHR_external_semaphore_fd): the - * pool's next copy into that buffer waits for it on the GPU instead of the compositor thread polling it. */ static PFN_vkImportSemaphoreFdKHR g_import_sem_fd; static VkSemaphore g_wait_sem; -/* The 10 s perf line's counters (vkp_perf_take). Compositor thread only. */ static struct vkp_perf g_perf; static int64_t perf_now(void) { struct timespec ts; @@ -113,7 +87,6 @@ static void perf_add(int64_t *sum, int64_t *max, unsigned *count, int64_t d) { if (d > *max) *max = d; (*count)++; } -/* vkWaitForFences on the compositor's own work, timed for the perf line. */ static VkResult timed_wait(VkFence fence, uint64_t timeout) { int64_t t0 = perf_now(); VkResult r = g_vk.WaitForFences(g_dev, 1, &fence, VK_TRUE, timeout); @@ -121,93 +94,66 @@ static VkResult timed_wait(VkFence fence, uint64_t timeout) { return r; } -/* The scene image (effects path only): every draw composited 1:1 at scene size, the input of the - * screen-effect chain (effects_chain.c) - and of frame generation once that lands. Recreated on a - * scene size change; frames are fenced, so never while the GPU reads it. */ static struct { VkImage img; VkDeviceMemory mem; int w, h; } g_scene; -/* HDR composition (hdr_compose.h): the 10-bit image every draw is blitted into 1:1 in its own encoding - * ("mixed"), and the 10-bit HDR picture the encode pass makes of it for the game's display layer. Only - * ever created while an HDR game shares the scene with something the display layer cannot show alone. */ struct vkp_img_slot { VkImage img; VkDeviceMemory mem; int w, h; VkFormat fmt; }; static struct vkp_img_slot g_mixed, g_hdrscene, g_fgscene; #define HDR_FMT VK_FORMAT_A2B10G10R10_UNORM_PACK32 -/* Frame generation with HDR frames through an HDR10 swapchain: the scene, the effects and the engine's - * frames in FP16 - the format lsfg-vk itself uses for HDR - where the engine can build its chain in it, - * so the PQ picture keeps more than 8 bits through interpolation (framegen_bridge.h). */ #define FG_HDR_FMT VK_FORMAT_R16G16B16A16_SFLOAT -/* HDR10 swapchain (frame generation with an HDR game): VK_EXT_swapchain_colorspace is enabled on the - * instance only when the session asked for HDR; the swapchain is built as A2B10G10R10 + HDR10_ST2084 - * while such frames are being presented and the Android surface lists that pair (its WSI then sets the - * surface dataspace to BT2020_PQ). */ -static int g_colorspace_ext; /* the instance has VK_EXT_swapchain_colorspace */ -static int g_swap_hdr_want; /* the next swapchain should be HDR10 */ -static int g_swap_is_hdr; /* the live swapchain is HDR10 */ -static int g_swap_hdr_unavailable; /* this surface lists no HDR10 format: tone-map instead (said once) */ -static VkFormat g_swap_fmt; /* the live swapchain's format */ -/* VK_EXT_hdr_metadata (HDR sessions, where the device offers it): the HDR10 swapchain carries the game's - * mastering metadata, sent once per swapchain and image description. */ + + + + +static int g_colorspace_ext; +static int g_swap_hdr_want; +static int g_swap_is_hdr; +static int g_swap_hdr_unavailable; +static VkFormat g_swap_fmt; + + static PFN_vkSetHdrMetadataEXT g_set_hdr_metadata; static uint32_t g_swap_md_identity; static int g_surface_formats_said; static const char *vk_result_name(VkResult r); -/* The Android surface is created/destroyed on the app's UI thread while the compositor thread - * may be inside a WSI call (acquire and present can block for a refresh or more). The UI thread - * therefore never touches the swapchain: it leaves the new window here and returns at once; the - * compositor thread picks it up before its next frame (vkp_apply_window_request). g_req_lock is - * only ever held for these few assignments, never across a Vulkan call. */ static pthread_mutex_t g_req_lock = PTHREAD_MUTEX_INITIALIZER; static ANativeWindow *g_req_window; static int g_req_pending; -/* A detach was asked for since the compositor thread last looked. A rebind (null, then the same - * window) to rebuild the swapchain at a new size can land in full before the thread gets to it, - * and would otherwise read as the same window handed over again - nothing rebuilt, and the old - * size's buffers scaled onto the rotated or refolded panel. */ static int g_req_detached; -/* Scale mode + alignment (app values, see vk_present.h); written from any thread, read per frame. */ static volatile int g_mode = VKP_MODE_STRETCH, g_align = VKP_ALIGN_CENTER; -/* The scene -> output mapping of the last frame (compositor thread): scene pixel (x,y) lands at - * output (off_x + x * kx, off_y + y * ky), clipped to the region rectangle. */ static struct { int scene_w, scene_h, out_w, out_h, mode, align; float kx, ky, off_x, off_y; - int rx, ry, rw, rh; /* region the picture may occupy */ + int rx, ry, rw, rh; int valid; } g_map; -/* A failed swapchain creation is retried no sooner than this (the window may be mid-teardown), - * and the failure is logged once per streak instead of every frame. */ static int64_t g_swap_retry_at_ns; static int g_swap_fail_logged; -/* Implemented in waylandcomp_jni.c - notifies Java (dismiss launch overlay). */ extern void droiddeck_on_first_frame(void); static char g_gpu_name[VK_MAX_PHYSICAL_DEVICE_NAME_SIZE]; const char *vkp_gpu_name(void) { return g_gpu_name; } -/* DRM fourccs name the channels of a little-endian 32-bit word: XRGB8888 is B,G,R,X in memory - * (= VK B8G8R8A8) and XBGR8888 is R,G,B,X (= VK R8G8B8A8). Turnip's Wayland WSI sends XB24 for - * R8G8B8A8 swapchains, so reading everything as BGRA swaps red and blue. */ static VkFormat drm_to_vk(uint32_t drm) { switch (drm) { - case 0x34324241: /* AB24 */ - case 0x34324258: /* XB24 */ + case 0x34324241: + case 0x34324258: return VK_FORMAT_R8G8B8A8_UNORM; - case 0x30334241: /* AB30 */ - case 0x30334258: /* XB30 */ + case 0x30334241: + case 0x30334258: return VK_FORMAT_A2B10G10R10_UNORM_PACK32; - case 0x30335241: /* AR30 */ - case 0x30335258: /* XR30 */ + case 0x30335241: + case 0x30335258: return VK_FORMAT_A2R10G10B10_UNORM_PACK32; - case 0x48344241: /* AB4H */ - case 0x48344258: /* XB4H */ + case 0x48344241: + case 0x48344258: return VK_FORMAT_R16G16B16A16_SFLOAT; - default: /* AR24 / XR24 */ + default: return VK_FORMAT_B8G8R8A8_UNORM; } } @@ -220,10 +166,6 @@ void vk_present_set_driver(const char *driver_path, const char *library_name, g_native_lib_dir = native_lib_dir ? strdup(native_lib_dir) : NULL; } -/* Destroy and recreate every acquire/render-done semaphore. Done with the swapchain (after the - * device went idle): a frame that acquired an image and then bailed out leaves its acquire - * semaphore signalled with nothing to wait on it, and reusing it in the next acquire is invalid - * (Adreno answers OUT_OF_DATE, which rebuilds again - a loop). Fresh objects cannot carry that. */ static void reset_sync(void) { VkSemaphoreCreateInfo semci = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; for (int k = 0; k < MAX_PRESENTS; k++) { @@ -235,14 +177,14 @@ static void reset_sync(void) { } static void destroy_swapchain(void) { - g_base_black = 0; /* a new swapchain starts with nothing on it */ + g_base_black = 0; if (g_dev_state != 1) return; g_vk.DeviceWaitIdle(g_dev); reset_sync(); for (uint32_t i = 0; i < g_nimg; i++) blendp_forget_image(g_images[i]); if (g_swapchain) g_vk.DestroySwapchainKHR(g_dev, g_swapchain, NULL); g_swapchain = VK_NULL_HANDLE; - g_swap_md_identity = 0; /* a new swapchain gets the metadata again */ + g_swap_md_identity = 0; if (g_surface) g_vk.DestroySurfaceKHR(g_inst, g_surface, NULL); g_surface = VK_NULL_HANDLE; free(g_images); @@ -253,8 +195,6 @@ static void destroy_swapchain(void) { void vk_present_set_window(ANativeWindow *window) { ANativeWindow *superseded = NULL; pthread_mutex_lock(&g_req_lock); - /* Two requests before the compositor thread got to the first: the first window was never - * used, so its reference is dropped here. */ if (g_req_pending && g_req_window && g_req_window != window) superseded = g_req_window; g_req_window = window; g_req_pending = 1; @@ -274,18 +214,16 @@ int vkp_apply_window_request(void) { g_req_detached = 0; pthread_mutex_unlock(&g_req_lock); if (w == g_window) { - /* The same window handed over again (ANativeWindow_fromSurface adds a reference each time). */ if (w) ANativeWindow_release(w); if (!w || !detached) return 0; - /* ... after a detach: a rebind for a resized surface. Rebuilt at its size on the next frame. */ sc_layer_window_gone(); destroy_swapchain(); droiddeck_log("gpu", "screen surface rebound: rebuilding the swapchain at its new size"); return 1; } - sc_layer_window_gone(); /* the layer (if any) belongs to the old window */ - destroy_swapchain(); /* recreated against the new window on the next frame */ - g_swap_hdr_unavailable = 0; /* a new surface (another display, say) may offer HDR10 */ + sc_layer_window_gone(); + destroy_swapchain(); + g_swap_hdr_unavailable = 0; if (g_window) ANativeWindow_release(g_window); g_window = w; g_swap_retry_at_ns = 0; @@ -329,11 +267,6 @@ static const char *align_name(int a) { } } -/* Mirror of ViewTransformation.update(outer = output, inner = scene, mode, alignment) - keep the - * arithmetic identical: the app maps touch input through that class with the same inputs, so any - * difference here puts the pointer beside what it is pointing at. OFF and FIT are both an - * aspect-preserving letterbox (OFF only differs in the app's fullscreen gates); TOP/BOTTOM confine - * the picture to the top/bottom half of the output (the app's handheld split, #413). */ static void update_map(int scene_w, int scene_h) { int mode = g_mode, align = g_align; int W = (int)g_extent.width, H = (int)g_extent.height; @@ -358,11 +291,10 @@ static void update_map(int scene_w, int scene_h) { if (mode == VKP_MODE_FILL) aspect = sx > sy ? sx : sy; else if (mode == VKP_MODE_INTEGER) { float m = sx < sy ? sx : sy; - aspect = (float)(int)m; /* floor for m >= 1 */ + aspect = (float)(int)m; if (aspect < 1.0f) aspect = 1.0f; } else aspect = sx < sy ? sx : sy; g_map.kx = g_map.ky = aspect; - /* Same integer truncation as ViewTransformation's viewOffsetX/Y. */ g_map.off_x = (float)(g_map.rx + (int)((g_map.rw - scene_w * aspect) * 0.5f)); g_map.off_y = (float)(g_map.ry + (int)((g_map.rh - scene_h * aspect) * 0.5f)); } @@ -393,11 +325,9 @@ static int has_ext(VkExtensionProperties *e, uint32_t n, const char *name) { return 0; } -/* Instance + device + command objects. Doesn't need the window. */ static int dev_init(void) { if (g_dev_state != 0) return g_dev_state == 1 ? 0 : -1; - /* Load Turnip (adrenotools) and its entry points - NOT the system driver. */ if (vk_loader_open(g_driver_path, g_library_name, g_native_lib_dir) != 0) { LOGE("present: vk_loader_open failed"); g_dev_state = -1; return -1; } @@ -405,9 +335,6 @@ static int dev_init(void) { const char *inst_exts[3] = {VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME, NULL}; uint32_t n_inst_exts = 2; - /* HDR sessions only (droiddeck_color.h): the colour-space extension lets the swapchain carry HDR10 for - * frame generation. Asked for only when the loader has it, and only when HDR was asked for, so - * every other session creates exactly the instance it always did. */ if (droiddeck_color_requested() && g_vk.EnumerateInstanceExtensionProperties) { uint32_t ne = 0; g_vk.EnumerateInstanceExtensionProperties(NULL, &ne, NULL); @@ -425,8 +352,6 @@ static int dev_init(void) { "generation %s)", g_colorspace_ext ? "enables" : "has no", g_colorspace_ext ? "is possible where the screen surface offers one" : "is not possible: tone-mapped instead"); } - /* 1.3 like the X11 renderer's instance: the frame-generation probe (framegen_engine.cpp) - * queries VkPhysicalDeviceVulkan12Features, and a 1.1 instance may be answered as 1.1. */ VkApplicationInfo app = {.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName = "droiddeck-wayland-present", .apiVersion = VK_API_VERSION_1_3}; @@ -464,7 +389,6 @@ static int dev_init(void) { } g_vk.GetPhysicalDeviceMemoryProperties(g_pd, &g_memprops); - /* Verify the dmabuf-import extensions are present, and log any that are missing. */ const char *dev_exts[7] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME, "VK_KHR_external_memory_fd", "VK_EXT_external_memory_dma_buf", "VK_EXT_image_drm_format_modifier", "VK_KHR_image_format_list", NULL, NULL}; @@ -476,14 +400,11 @@ static int dev_init(void) { for (unsigned i = 0; i < 5; i++) if (!has_ext(exts, ne, dev_exts[i])) LOGE("present: driver MISSING %s (dmabuf import will fail)", dev_exts[i]); - /* HDR sessions only: the HDR10 swapchain (frame generation) can carry the game's metadata. */ int want_hdr_md = 0; if (droiddeck_color_requested()) { want_hdr_md = has_ext(exts, ne, VK_EXT_HDR_METADATA_EXTENSION_NAME); if (want_hdr_md) dev_exts[n_dev_exts++] = VK_EXT_HDR_METADATA_EXTENSION_NAME; } - /* The layer pool's release fences as GPU waits (sync_file -> semaphore): optional, the pool falls - * back to waiting for them on the CPU without it. */ const int want_sem_fd = has_ext(exts, ne, VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME); if (want_sem_fd) dev_exts[n_dev_exts++] = VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME; free(exts); @@ -491,10 +412,6 @@ static int dev_init(void) { float prio = 1.0f; VkDeviceQueueCreateInfo qci = {.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, .queueFamilyIndex = g_qfam, .queueCount = 1, .pQueuePriorities = &prio}; - /* Frame generation (framegen_bridge.c): the LSFG chain needs the memory-model and - * storage-image features enabled at device creation; the probe hands back the pNext chain - * for them, or NULL on a device that cannot run it (then Win-FG Native alone is offered). A - * driver that rejects the chain costs nothing: retried once without it, exactly as before. */ const void *fg_features = vkp_framegen_device_features(g_inst, vk_loader_gipa(), g_pd); VkDeviceCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, .pNext = fg_features, .queueCreateInfoCount = 1, .pQueueCreateInfos = &qci, @@ -579,7 +496,6 @@ static int swap_init(void) { #define SWLOGE(...) do { if (!g_swap_fail_logged) LOGE(__VA_ARGS__); } while (0) -/* Short names for the session log (the formats and colour spaces an Android surface lists). */ static void vk_format_short(VkFormat f, char *out, size_t n) { const char *s = NULL; switch (f) { @@ -622,8 +538,6 @@ static const char *vk_colorspace_short(VkColorSpaceKHR c) { default: return NULL; } } -/* One line: the total, how many colour spaces each format comes with, and every HDR-capable pair - * (HDR10 / HLG / extended sRGB, any format) - never "the first N pairs". */ static int hdr_capable_space(VkColorSpaceKHR cs) { int c = (int)cs; return c == VK_COLOR_SPACE_HDR10_ST2084_EXT || c == VK_COLOR_SPACE_HDR10_HLG_EXT || @@ -644,7 +558,6 @@ static void log_surface_formats(const VkSurfaceFormatKHR *f, uint32_t n) { pp += (size_t)snprintf(per + pp, sizeof(per) - pp, "%s%s x%u", pp ? ", " : "", nm, cnt); } } - /* The deep formats (10-bit, FP16) first, so a long list never pushes them out of the line. */ for (int pass = 0; pass < 2; pass++) for (uint32_t i = 0; i < n; i++) { if (!hdr_capable_space(f[i].colorSpace) || (vk_format_bits(f[i].format) > 8) != (pass == 0)) continue; @@ -673,17 +586,10 @@ static int swap_init_locked(void) { VkSurfaceCapabilitiesKHR caps; g_vk.GetPhysicalDeviceSurfaceCapabilitiesKHR(g_pd, g_surface, &caps); - /* Remembered for the layer path: currentTransform is the rotation the presentation engine (and - * therefore SurfaceFlinger and the DPU) applies to everything we put on this display. Anything - * but IDENTITY means every display layer we hand over is a ROTATED layer. sc_layer.c needs that - * to decide whether a second layer is affordable - see sc_layer_present_overlay(). */ g_surface_transform = caps.currentTransform; g_surface_transform_known = 1; - g_caps_extent = caps.currentExtent; /* check_surface_changed() compares against this */ + g_caps_extent = caps.currentExtent; g_surface_checked_ns = perf_now(); - /* Every format/colour-space pair the surface lists. With VK_EXT_swapchain_colorspace the Android - * WSI lists each format once per colour space (the Fold: 11 per format), so a fixed-size array - * silently drops the 10-bit and FP16 rows - which is where HDR10 lives. */ uint32_t nfmt = 0; g_vk.GetPhysicalDeviceSurfaceFormatsKHR(g_pd, g_surface, &nfmt, NULL); VkSurfaceFormatKHR *fmts = nfmt ? calloc(nfmt, sizeof(*fmts)) : NULL; @@ -697,10 +603,6 @@ static int swap_init_locked(void) { g_surface_formats_said = 1; log_surface_formats(fmts, nfmt); } - /* HDR10 for frame generation with an HDR game (render_impl sets g_swap_hdr_want): the best format - * the surface lists with VK_COLOR_SPACE_HDR10_ST2084_EXT - 10-bit, then FP16, then 8-bit (still a - * PQ BT.2020 picture, with less precision). None listed -> the frames are tone-mapped into the - * ordinary swapchain, and that is said once. */ g_swap_is_hdr = 0; if (g_swap_hdr_want) { static const VkFormat pref[] = {VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_FORMAT_A2R10G10B10_UNORM_PACK32, @@ -736,17 +638,10 @@ static int swap_init_locked(void) { g_extent.width = ANativeWindow_getWidth(g_window); g_extent.height = ANativeWindow_getHeight(g_window); } - /* + one image per generated frame while frame generation is armed: every present of a scene - * frame can then be queued at once and FIFO spaces them onto consecutive vblanks. */ g_swap_extra = vkp_framegen_extra_images(); uint32_t want = caps.minImageCount + 1 + (uint32_t)g_swap_extra; if (caps.maxImageCount && want > caps.maxImageCount) want = caps.maxImageCount; - /* Use IDENTITY preTransform when the surface supports it. Setting preTransform = - * currentTransform tells the presentation engine our content is ALREADY pre-rotated by - * that amount - but our blit doesn't rotate, so on a device whose surface reports a 90° - * currentTransform the display then rotates our upright frame 90° (game shows sideways). - * IDENTITY = "don't rotate what I present", which is what we want. */ VkSurfaceTransformFlagBitsKHR pretrans = (caps.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) ? VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR : caps.currentTransform; @@ -761,8 +656,6 @@ static int swap_init_locked(void) { .presentMode = VK_PRESENT_MODE_FIFO_KHR, .clipped = VK_TRUE}; VkResult cr = g_vk.CreateSwapchainKHR(g_dev, &sci, NULL, &g_swapchain); if (cr != VK_SUCCESS && g_swap_is_hdr) { - /* The surface listed the HDR10 pair but refused the swapchain: say so, never ask again this - * surface, and build the ordinary one now (frames tone-mapped) - never a black screen. */ droiddeck_log("color", "the driver refused the HDR10 swapchain (%s): frames with frame generation are " "tone-mapped to SDR instead", vk_result_name(cr)); g_swap_is_hdr = 0; @@ -794,9 +687,6 @@ static int memory_type(uint32_t bits, VkMemoryPropertyFlags want) { return -1; } -/* The small black image the letterbox bars are blitted from (see g_black_img): created, cleared and left - * in TRANSFER_SRC_OPTIMAL once, with a one-time submit, right after the device comes up. Any failure only - * means the bars are cleared the old way (the whole output). */ static void init_black_image(void) { g_black_state = -1; VkImageCreateInfo ici = { @@ -851,10 +741,6 @@ static void init_black_image(void) { droiddeck_log("gpu", "letterbox black image unavailable: the whole output is cleared every copy-path frame"); } -/* Blit the black image over everything of swapchain image `img` (in TRANSFER_DST_OPTIMAL) OUTSIDE the - * destination rectangle of `covered` - the letterbox bars around a picture that fills that rectangle - * completely. Up to four rectangles, one blit command. 0 = not possible (no black image): the caller - * clears the whole image instead. */ static int record_bars_clear(VkCommandBuffer cmd, VkImage img, const VkImageBlit *covered) { if (g_black_state != 1 || !covered) return 0; const int W = (int)g_extent.width, H = (int)g_extent.height; @@ -868,11 +754,11 @@ static int record_bars_clear(VkCommandBuffer cmd, VkImage img, const VkImageBlit if (y1 > H) y1 = H; if (x1 <= x0 || y1 <= y0) return 0; int rects[4][4], n = 0; - if (y0 > 0) { rects[n][0] = 0; rects[n][1] = 0; rects[n][2] = W; rects[n][3] = y0; n++; } /* top */ - if (y1 < H) { rects[n][0] = 0; rects[n][1] = y1; rects[n][2] = W; rects[n][3] = H; n++; } /* bottom */ - if (x0 > 0) { rects[n][0] = 0; rects[n][1] = y0; rects[n][2] = x0; rects[n][3] = y1; n++; } /* left */ - if (x1 < W) { rects[n][0] = x1; rects[n][1] = y0; rects[n][2] = W; rects[n][3] = y1; n++; } /* right */ - if (!n) return 1; /* the picture covers the whole output: nothing to clear */ + if (y0 > 0) { rects[n][0] = 0; rects[n][1] = 0; rects[n][2] = W; rects[n][3] = y0; n++; } + if (y1 < H) { rects[n][0] = 0; rects[n][1] = y1; rects[n][2] = W; rects[n][3] = H; n++; } + if (x0 > 0) { rects[n][0] = 0; rects[n][1] = y0; rects[n][2] = x0; rects[n][3] = y1; n++; } + if (x1 < W) { rects[n][0] = x1; rects[n][1] = y0; rects[n][2] = W; rects[n][3] = y1; n++; } + if (!n) return 1; VkImageBlit blits[4]; for (int i = 0; i < n; i++) blits[i] = (VkImageBlit){.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, @@ -893,9 +779,8 @@ const char *vkp_modifier_name(uint64_t modifier) { return other; } -/* Can the driver create and import this format/modifier with the requested image usage? */ static int modifier_importable(VkFormat fmt, uint64_t modifier, VkImageUsageFlags usage) { - if (!g_vk.GetPhysicalDeviceImageFormatProperties2) return 1; /* can't ask; the create will tell */ + if (!g_vk.GetPhysicalDeviceImageFormatProperties2) return 1; VkPhysicalDeviceExternalImageFormatInfo ext = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO, .handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT}; @@ -1032,7 +917,6 @@ struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t return img; } -/* Linear wl_shm images can use the blend pass only if the driver samples them with a linear filter. */ static int shm_sampled(void) { static int known; if (!known) { @@ -1086,8 +970,6 @@ struct vkp_image *vkp_image_create_shm(int w, int h) { return img; } -/* Renders are synchronous (we wait for the frame's fence), so writing between frames - * never races the GPU. */ void vkp_image_upload_shm(struct vkp_image *img, const void *data, int stride) { if (!img || !img->map || !data) return; size_t rowbytes = (size_t)img->w * 4; @@ -1111,9 +993,6 @@ void vkp_image_destroy(struct vkp_image *img) { free(img); } -/* Map a draw into swapchain pixels through the scale mode, clipping the destination to the - * picture's region (the whole output, or its half on TOP/BOTTOM; FILL's overflow is cut here) - * and trimming the source to match. Returns 0 if nothing is left to draw. */ static int map_draw(const struct vkp_draw *d, float kx, float ky, float off_x, float off_y, float L, float T, float R, float B, VkImageBlit *blit) { float x0 = off_x + d->dx * kx, y0 = off_y + d->dy * ky; @@ -1148,12 +1027,10 @@ static int draw_to_blit(const struct vkp_draw *d, VkImageBlit *blit) { return map_draw(d, g_map.kx, g_map.ky, g_map.off_x, g_map.off_y, (float)g_map.rx, (float)g_map.ry, R, B, blit); } -/* The same draw 1:1 into the scene image (no mapping: scene pixels are the destination). */ static int draw_to_scene_blit(const struct vkp_draw *d, int scene_w, int scene_h, VkImageBlit *blit) { return map_draw(d, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, (float)scene_w, (float)scene_h, blit); } -/* A translucent draw needs source-over blending; every other draw keeps the existing blit path. */ static void record_draw(VkCommandBuffer cmd, const struct vkp_draw *d, const VkImageBlit *blit, VkImage target, VkFormat target_fmt, int target_w, int target_h, VkFilter filter) { const struct vkp_image *im = d->img; @@ -1167,15 +1044,13 @@ static void record_draw(VkCommandBuffer cmd, const struct vkp_draw *d, const VkI static void destroy_scene_image(void) { if (g_scene.img) vkp_effects_forget_image(g_scene.img); - if (g_scene.img) hdrc_forget_image(g_scene.img); /* the HDR pass may have drawn into it */ + if (g_scene.img) hdrc_forget_image(g_scene.img); if (g_scene.img) blendp_forget_image(g_scene.img); if (g_scene.img) g_vk.DestroyImage(g_dev, g_scene.img, NULL); if (g_scene.mem) g_vk.FreeMemory(g_dev, g_scene.mem, NULL); memset(&g_scene, 0, sizeof(g_scene)); } -/* The scene image at this size (R8G8B8A8: a blit converts every client format into it, and the - * chain's shaders read it as RGBA). 0 = ready. */ static int ensure_scene_image(int w, int h) { if (g_scene.img && g_scene.w == w && g_scene.h == h) return 0; destroy_scene_image(); @@ -1184,7 +1059,7 @@ static int ensure_scene_image(int w, int h) { .format = VK_FORMAT_R8G8B8A8_UNORM, .extent = {(uint32_t)w, (uint32_t)h, 1}, .mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL, .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | - VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, /* the HDR encode pass writes it (frame generation) */ + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED}; VkResult r = g_vk.CreateImage(g_dev, &ici, NULL, &g_scene.img); if (r != VK_SUCCESS) { LOGE("effects: scene image %dx%d: vkCreateImage %s", w, h, vk_result_name(r)); g_scene.img = VK_NULL_HANDLE; return -1; } @@ -1202,7 +1077,6 @@ static int ensure_scene_image(int w, int h) { return 0; } -/* One of the HDR composition's device-local images at this size and format. 0 = ready. */ static int ensure_img(struct vkp_img_slot *s, int w, int h, VkFormat fmt, VkImageUsageFlags usage, const char *what) { if (s->img && s->w == w && s->h == h && s->fmt == fmt) return 0; if (s->img) hdrc_forget_image(s->img); @@ -1239,11 +1113,6 @@ static int ensure_img(struct vkp_img_slot *s, int w, int h, VkFormat fmt, VkImag return 0; } -/* The HDR composition (hdr_compose.h): clear the mixed image, blit every draw into it 1:1 in its own - * encoding, then encode it into `out` (left in TRANSFER_DST_OPTIMAL). The caller has already taken the - * draws' images (queue-family acquire / host-write barriers). Which rectangles are HDR comes from - * hf->is_hdr: listed top draw first, down to the lowest HDR draw (everything under that is SDR). - * 0 = done, -1 = the HDR pass is unavailable (nothing was recorded into `out`). */ static int compose_hdr(VkCommandBuffer cmd, const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, int scene_w, int scene_h, VkImage out, VkFormat out_fmt, enum hdrc_mode mode) { if (!hf || !hf->is_hdr || n <= 0) return -1; @@ -1272,7 +1141,6 @@ static int compose_hdr(VkCommandBuffer cmd, const struct vkp_draw *draws, int n, for (int i = 0; i < n; i++) { VkImageBlit blit; if (!draws[i].img || !draw_to_scene_blit(&draws[i], scene_w, scene_h, &blit)) continue; - /* A blit converts UNORM to UNORM by value: 8-bit sRGB and 10-bit PQ both land unchanged. */ record_draw(cmd, &draws[i], &blit, g_mixed.img, HDR_FMT, scene_w, scene_h, VK_FILTER_LINEAR); if (hf->is_hdr[i] && lowest_hdr < 0) lowest_hdr = i; } @@ -1296,9 +1164,6 @@ static int compose_hdr(VkCommandBuffer cmd, const struct vkp_draw *draws, int n, return hdrc_encode(cmd, g_mixed.img, out, out_fmt, scene_w, scene_h, &p); } -/* VK_ERROR_DEVICE_LOST: nothing on this device works any more, and there is no way back short - * of a new session. Say so once and stop touching the swapchain; clients keep being paced by the - * compositor (see pace_without_output) so they don't wedge, they just aren't shown. */ static void device_lost(const char *where) { if (g_dev_state == -2) return; g_dev_state = -2; @@ -1337,17 +1202,10 @@ static const char *vk_result_name(VkResult r) { } } -/* ---------------------------------------------------------------- the compositor pass */ -/* The mapping/blit stage for one present: clear the swapchain image to black (the letterbox), - * blit `src` (src_w x src_h, in GENERAL: the effects chain's result or a generated frame, either - * standing for the whole scene) through the scene -> output mapping, and leave the image ready - * to present. */ static void record_screen_blit(VkCommandBuffer cmd, VkImage src, int src_w, int src_h, int scene_w, int scene_h, uint32_t img, VkFilter filter, int cardboard) { VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - /* Cardboard mode (cardboard.h): the pass turns src into the two eye views over the whole panel, - * and that image is blitted 1:1 instead of src going through the scene mapping. */ int direct = 0; if (cardboard) { VkImageMemoryBarrier rd = { @@ -1371,8 +1229,6 @@ static void record_screen_blit(VkCommandBuffer cmd, VkImage src, int src_w, int .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = g_images[img], .subresourceRange = range, .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT}, - /* Whatever wrote src last (blits, the effects chain, a compute dispatch, in this or an - * earlier submission on this queue) must be visible to the blit's read. */ {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_GENERAL, .newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, @@ -1380,7 +1236,7 @@ static void record_screen_blit(VkCommandBuffer cmd, VkImage src, int src_w, int .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT}}; g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 2, pre); - struct vkp_image tmp = {.w = src_w, .h = src_h}; /* only its size is looked at */ + struct vkp_image tmp = {.w = src_w, .h = src_h}; struct vkp_draw d = {&tmp, 0, 0, (float)src_w, (float)src_h, 0, 0, scene_w, scene_h}; VkImageBlit blit; const int have_blit = direct ? 1 : draw_to_blit(&d, &blit); @@ -1389,7 +1245,6 @@ static void record_screen_blit(VkCommandBuffer cmd, VkImage src, int src_w, int .srcOffsets = {{0, 0, 0}, {src_w, src_h, 1}}, .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{0, 0, 0}, {src_w, src_h, 1}}}; - /* The result fills its whole mapped rectangle, so only the letterbox bars around it need black. */ if (!have_blit || !record_bars_clear(cmd, g_images[img], &blit)) { VkClearColorValue black = {.float32 = {0.0f, 0.0f, 0.0f, 1.0f}}; g_vk.CmdClearColorImage(cmd, g_images[img], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); @@ -1411,16 +1266,9 @@ static void record_screen_blit(VkCommandBuffer cmd, VkImage src, int src_w, int 0, 0, NULL, 0, NULL, 1, &b_present); } -/* Acquire a swapchain image for present slot k. On the frame's FIRST acquire the swapchain is - * (re)created when needed and rebuilt once when the surface changed under us (OUT_OF_DATE after a - * resize/rotation, SURFACE_LOST when the window was torn down) - once, so a frame is not lost to - * a recreate that would have succeeded. Later slots have presents in flight on this swapchain, - * so they never rebuild; they fail and the frame ends early. 0 on success. */ static int acquire_image(int k, int first, uint32_t *img, VkResult *ar_out) { for (int attempt = 0; ; attempt++) { if (!g_swapchain && (!first || swap_init() != 0)) return -1; - /* A bounded wait: a surface that went away without telling us must not park the - * compositor thread forever (clients are paced from this thread). */ const int64_t t_acq = perf_now(); VkResult ar = g_vk.AcquireNextImageKHR(g_dev, g_swapchain, 1000000000ULL, g_acqs[k], VK_NULL_HANDLE, img); perf_add(&g_perf.acquire_ns, &g_perf.acquire_max_ns, &g_perf.acquires, perf_now() - t_acq); @@ -1434,14 +1282,11 @@ static int acquire_image(int k, int first, uint32_t *img, VkResult *ar_out) { } droiddeck_log("error", "present: acquire %d failed (%s %d)", k, vk_result_name(ar), (int)ar); if (ar == VK_TIMEOUT || ar == VK_NOT_READY) return -1; - destroy_swapchain(); /* anything else: start over next frame */ + destroy_swapchain(); return -1; } } -/* vkp_render_plain: the base surface's black frame with the compositor pass skipped (HDR game on its - * layer, compositor.c) - neither the effects chain nor frame generation runs, and so neither paces - * this loop with extra presents. */ static int g_plain_frame; int vkp_render_plain(int scene_w, int scene_h) { g_plain_frame = 1; @@ -1450,10 +1295,6 @@ int vkp_render_plain(int scene_w, int scene_h) { return r; } -/* A surface that changed size or transform under a swapchain nobody presents to (the base surface - * while the game is on its display layer) never reports OUT_OF_DATE, so ask it - at most every - * 100 ms, on the compositor thread - and tear the swapchain down when it no longer matches; the next - * frame rebuilds it (and, in layer mode, puts one black frame on it). */ static void check_surface_changed(void) { if (!g_swapchain || !g_surface || g_dev_state != 1) return; const int64_t now = perf_now(); @@ -1475,7 +1316,7 @@ int vkp_base_black(int scene_w, int scene_h) { check_surface_changed(); if (g_base_black && g_swapchain && g_window && g_dev_state == 1) { g_perf.base_kept++; - return 0; /* the black frame from before is still what the base surface shows */ + return 0; } int r = vkp_render_plain(scene_w, scene_h); if (r == 0) g_perf.base_presents++; @@ -1489,10 +1330,6 @@ void vkp_perf_take(struct vkp_perf *out) { int vkp_can_wait_sync_fd(void) { return g_import_sem_fd != NULL && g_wait_sem != VK_NULL_HANDLE; } -/* Hand a sync_file to the next submit as a GPU wait (temporary import into g_wait_sem; the driver - * owns the fd from then on). Returns 1 when the submit must wait on g_wait_sem, 0 when there is nothing - * to wait on - the fd was -1, or it could not be imported and was waited for on the CPU (bounded) and - * closed instead. -1 = the display is still reading the buffer after 100 ms: do not write it. */ static int take_wait_fd(int fd) { if (fd < 0) return 0; if (vkp_can_wait_sync_fd()) { @@ -1516,8 +1353,6 @@ static int take_wait_fd(int fd) { return r == 0 ? -1 : 0; } -/* The HDR10 swapchain's metadata: the game's SMPTE 2086 / CTA-861.3 values (its image description) via - * VK_EXT_hdr_metadata, once per swapchain and description. A game that gave none gets none. */ static void send_hdr_metadata(const struct droiddeck_color *c) { static int said_none, said_nodesc; if (!c) return; @@ -1552,26 +1387,19 @@ static void send_hdr_metadata(const struct droiddeck_color *c) { c->identity, c->text); } -/* The whole present: vkp_render (hf == NULL, exactly as it always was) and vkp_render_hdr (a scene with - * HDR draws: composed into one encoding, hdr_compose.h). */ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, int *how) { if (how) *how = 0; vkp_apply_window_request(); - check_surface_changed(); /* a rotation or a fold the rebind did not cover: rebuild at the new size */ + check_surface_changed(); if (g_dev_state == -2) return -1; if (dev_init() != 0 || !g_window || scene_w <= 0 || scene_h <= 0) return -1; - /* Frame generation queues several presents per scene frame and needs swapchain images for - * them: rebuild when that changes (armed, disarmed, another multiplier). */ if (g_swapchain && vkp_framegen_extra_images() != g_swap_extra) { droiddeck_log("gpu", "frame generation now wants %d present%s per frame: rebuilding the swapchain", 1 + vkp_framegen_extra_images(), vkp_framegen_extra_images() ? "s" : ""); destroy_swapchain(); } - /* HDR frames presented here (frame generation with an HDR game) want an HDR10 swapchain - unless the - * drawer's HDR output switch is off (tone-mapped) - nothing else does. A change of that rebuilds the - * swapchain (only in HDR sessions: g_colorspace_ext). */ const int want_hdr = hf && !hf->tonemap && g_colorspace_ext && !g_swap_hdr_unavailable; if (g_swapchain && want_hdr != g_swap_is_hdr) { droiddeck_log("color", want_hdr ? "HDR frames with frame generation: rebuilding the screen swapchain as HDR10" @@ -1585,21 +1413,13 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i VkResult ar; if (acquire_image(0, 1, &img, &ar) != 0) return -1; update_map(scene_w, scene_h); - /* A new HDR10 swapchain, or a new image description on it: hand the game's metadata over. */ if (g_swap_is_hdr && hf && hf->color && hf->color->identity != g_swap_md_identity) { g_swap_md_identity = hf->color->identity; send_hdr_metadata(hf->color); } - /* The compositor pass - scene -> effects -> frame generation -> mapping/blit -> swapchain - - * composes the scene off-screen when either stage needs the whole frame: the screen-effect - * chain (effects_chain.c) or frame generation (framegen_bridge.c). With both off the draws are - * blitted straight through the mapping into the swapchain image, as they always were; only the - * blit filter follows the scaling mode. */ const int fx = !g_plain_frame && vkp_effects_active(); const int fg = !g_plain_frame && vkp_framegen_active(); - /* Cardboard mode needs the whole frame too; it works on the 8-bit picture only (an HDR frame - * is shown flat, said once). */ int cb = !g_plain_frame && vkp_cardboard_active(); if (cb && hf) { static int said; @@ -1607,9 +1427,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i cb = 0; } vkp_cardboard_begin_frame(); - /* HDR frames with frame generation through an HDR10 swapchain: the scene, the effects and the - * engine's frames in FP16 (FG_HDR_FMT) where the engine can take that format - more than 8 bits of - * PQ through interpolation - else the 8-bit scene, as before. */ const int deep = hf && n > 0 && fg && g_swap_is_hdr && !hf->tonemap && vkp_framegen_format_ok(FG_HDR_FMT) && ensure_img(&g_fgscene, scene_w, scene_h, FG_HDR_FMT, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | @@ -1629,7 +1446,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i "into the HDR10 swapchain (%s): banding possible in smooth gradients", sw); } } - /* An HDR scene always takes the pass: its draws are composed into one encoding first. */ const int pass = deep || ((fx || fg || cb || (hf && n > 0)) && ensure_scene_image(scene_w, scene_h) == 0); VkImage scene_img = deep ? g_fgscene.img : g_scene.img; const VkFormat scene_fmt = deep ? FG_HDR_FMT : SCENE_FMT; @@ -1643,11 +1459,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i blendp_begin_frame(); VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - /* Source images: take dmabufs from the client's queue family, move shm images to - * GENERAL once (host writes stay visible across frames: memory is coherent and each - * frame is a new submission). One barrier per distinct image. The destination joins them: - * the swapchain image on the direct path, the scene image on the compositor pass (the - * per-present blits transition the swapchain images themselves there). */ VkImageMemoryBarrier *bars = calloc((size_t)n + 1, sizeof(*bars)); int nb = 0; bars[nb++] = (VkImageMemoryBarrier){ @@ -1682,12 +1493,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, (uint32_t)nb, bars); free(bars); - /* Clear the composition target to black (the letterbox on the direct path, the desktop's - * background on the pass) before the draws land on it. An opaque draw that covers the whole scene - * overwrites every pixel of its rectangle, so then only what lies outside it - * needs black: the letterbox bars on the direct path, nothing at all on the pass (the scene image - * is exactly the scene). Anything else - no such draw, the HDR composition - clears the whole - * target as before. */ VkImage target = pass ? scene_img : g_images[img]; int covered = 0; VkImageBlit cover_blit; @@ -1712,10 +1517,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i 0, 1, &mb, 0, NULL, 0, NULL); } - /* 1. Scene: the draws in order, 1:1 into the scene image (pass) or mapped onto the output. An HDR - * scene is composed into ONE encoding instead (hdr_compose.h): PQ BT.2020 for an HDR10 - * swapchain, tone-mapped sRGB for an ordinary one; the scene image comes back in - * TRANSFER_DST_OPTIMAL like the blits leave it. If that pass is unavailable, the blits below. */ int drawn = 0; int hdr_how = 0; if (hf && pass && n > 0 && @@ -1747,21 +1548,13 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, NULL, 0, NULL, 1, &b_present); } else { - /* 2. Effects (the hook): scaling to the scene's mapped size, then the effects, in the X11 - * order. The result stands for the whole scene (rw x rh) and comes back in - * TRANSFER_SRC_OPTIMAL; with nothing on the scene stays as composited. */ VkImageLayout result_layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; if (fx) { int mapped_w = (int)(scene_w * g_map.kx + 0.5f), mapped_h = (int)(scene_h * g_map.ky + 0.5f); - /* This path's scene is 8-bit. Set only where the chain really runs: a plain frame (the black - * base under an HDR picture) must not flip the chain's format back every frame. */ vkp_effects_set_formats(scene_fmt, scene_fmt); result = vkp_effects_run(cmd, scene_img, scene_w, scene_h, mapped_w, mapped_h, &rw, &rh); result_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; } - /* Hand-off: from here the frame lives in GENERAL - the frame-generation engines copy and - * sample it there, and every screen blit reads it there. Whatever wrote it last (the - * composite blits or the chain's last pass) is made visible to both. */ VkImageMemoryBarrier b_general = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = result_layout, .newLayout = VK_IMAGE_LAYOUT_GENERAL, @@ -1773,23 +1566,16 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, &b_general); - /* 3. Frame generation (the hook): the result is frame N; the engine interpolates between - * N-1 and N and hands back the frames that belong in between. Shown first, N last. */ if (fg) { ngen = vkp_framegen_run(cmd, result, VK_NULL_HANDLE, rw, rh, scene_fmt, gens); if (ngen < 0) ngen = 0; if (ngen > VKP_FG_MAX_GENERATIONS) ngen = VKP_FG_MAX_GENERATIONS; } - /* 4. Mapping/blit: the first present carries generated frame 0, or the result itself. */ record_screen_blit(cmd, ngen ? gens[0] : result, rw, rh, scene_w, scene_h, img, blit_filter, cb); } g_vk.EndCommandBuffer(cmd); - /* One submit + present per slot: generated frames first, the real frame last. The frame - * fence rides on the last submit (a fence signal is ordered after everything submitted - * earlier on the queue, so it still covers all of this frame's work). With FIFO the - * presentation engine shows them on consecutive vblanks: that IS the pacing, no sleeps. */ const int presents = 1 + ngen; int fence_submitted = 0, presented_gen = 0; VkResult pr = VK_SUCCESS; @@ -1818,7 +1604,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i if (qr != VK_SUCCESS) { if (qr == VK_ERROR_DEVICE_LOST) device_lost("submit"); else LOGE("present: submit %d/%d failed (%d)", k + 1, presents, (int)qr); - /* The acquired image is never presented; the swapchain would be stuck with it. */ if (g_dev_state != -2) destroy_swapchain(); return -1; } @@ -1833,8 +1618,6 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i if (pr == VK_ERROR_OUT_OF_DATE_KHR || pr == VK_ERROR_SURFACE_LOST_KHR || pr == VK_ERROR_DEVICE_LOST) break; } if (!fence_submitted) { - /* Ended early: an empty submit carrying the fence is ordered after everything queued - * above, so the wait below still means "this frame's work is done". */ VkSubmitInfo si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO}; g_vk.QueueSubmit(g_queue, 1, &si, g_fence); } @@ -1842,14 +1625,10 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i if (fg) vkp_framegen_presented(presented_gen); if (fr == VK_ERROR_DEVICE_LOST || pr == VK_ERROR_DEVICE_LOST) { device_lost("present"); return -1; } if (pr == VK_ERROR_OUT_OF_DATE_KHR || pr == VK_ERROR_SURFACE_LOST_KHR) { - /* The surface changed or went away under this frame: rebuild before the next one. */ droiddeck_log("gpu", "screen surface %s on present: rebuilding the swapchain", vk_result_name(pr)); destroy_swapchain(); return -1; } else if (pr == VK_SUBOPTIMAL_KHR || ar == VK_SUBOPTIMAL_KHR) { - /* Expected and harmless here: the swapchain uses IDENTITY preTransform on purpose (see - * swap_init), so a rotated panel reports SUBOPTIMAL on every frame. Rebuilding would change - * nothing (and a rebuild per frame would be far worse), so present as is; say so once. */ static int said; if (!said) { said = 1; droiddeck_log("gpu", "surface reports SUBOPTIMAL (panel rotation); presenting as is"); } } else if (pr != VK_SUCCESS) { @@ -1858,13 +1637,8 @@ static int render_impl(int scene_w, int scene_h, const struct vkp_draw *draws, i return -1; } - /* What the base surface shows from now on: black only after a plain frame with nothing drawn - * (vkp_base_black keeps presenting nothing while that holds); anything else it presented is - * content, and the next layer frame must put a black frame back under the layers first. */ g_base_black = (g_plain_frame && n == 0 && !hf) ? 1 : 0; - /* Signal the app once, on the first real client frame reaching the screen, so the - * launch/preloader overlay can dismiss (wayland has no XServer window-content hook). */ if (drawn && !g_first_frame_done) { g_first_frame_done = 1; droiddeck_on_first_frame(); @@ -1882,25 +1656,19 @@ int vkp_render_hdr(int scene_w, int scene_h, const struct vkp_draw *draws, int n return render_impl(scene_w, scene_h, draws, n, hf, how); } -/* ---------------------------------------------------------------- layer mode helpers */ -/* Degrees the display rotates our layers by: 0, 90, 180 or 270; -1 before the swapchain exists. - * Read from the surface, so it follows the panel's install orientation and the session's own - * orientation together, on any device. */ int vkp_surface_rotation_degrees(void) { if (!g_surface_transform_known) return -1; switch (g_surface_transform) { case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR: return 90; case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR: return 180; case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR: return 270; - default: return 0; /* IDENTITY, and the mirrored forms we never see here */ + default: return 0; } } ANativeWindow *vkp_window(void) { return g_window; } -/* A new session under a compositor that has already presented: arm the one-shot again so the - * next drawn frame reports, or the app would show its loading panel over the new client for good. */ void vkp_reset_first_frame(void) { g_first_frame_done = 0; } @@ -1913,14 +1681,14 @@ void vkp_signal_first_frame(void) { int vkp_update_map(int scene_w, int scene_h) { if (g_dev_state == -2 || dev_init() != 0 || !g_window || scene_w <= 0 || scene_h <= 0) return -1; - check_surface_changed(); /* layer mode may present nothing on the base surface for a long time */ + check_surface_changed(); if (!g_swapchain && swap_init() != 0) return -1; update_map(scene_w, scene_h); return g_map.valid ? 0 : -1; } int vkp_map_rect(int img_w, int img_h, int scene_w, int scene_h, int out[8]) { - struct vkp_image tmp = {.w = img_w, .h = img_h}; /* only its size is looked at */ + struct vkp_image tmp = {.w = img_w, .h = img_h}; struct vkp_draw d = {&tmp, 0, 0, (float)img_w, (float)img_h, 0, 0, scene_w, scene_h}; return vkp_map_draw(&d, out); } @@ -1935,15 +1703,12 @@ int vkp_map_draw(const struct vkp_draw *d, int out[8]) { return 1; } -/* Copy src (a client frame) into dst (a layer pool buffer) 1:1 and wait for it. Both images are - * owned by the "foreign" queue family (the game's driver / the display) between our uses, so each - * use acquires them and the destination is released back for the display to read. */ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd) { if (!src || !dst || !dst->blit_dst || g_dev_state == -2 || dev_init() != 0) { if (wait_fd >= 0) close(wait_fd); return -1; } - const int gpu_wait = take_wait_fd(wait_fd); /* the display's release of dst, before the copy writes it */ + const int gpu_wait = take_wait_fd(wait_fd); if (gpu_wait < 0) return -1; VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; @@ -1962,7 +1727,7 @@ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd) { .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT, .dstQueueFamilyIndex = g_qfam, .image = dst->image, .subresourceRange = range, .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT}}; - if (!src->dmabuf) { /* shm image: host-written, GENERAL */ + if (!src->dmabuf) { acq[0].oldLayout = src->in_general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_PREINITIALIZED; acq[0].newLayout = VK_IMAGE_LAYOUT_GENERAL; acq[0].srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; @@ -2004,12 +1769,12 @@ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd) { return 0; } -/* A client's GPU cursor read back to the CPU, for the app's pointer overlay (compositor.c, - * wl_pointer.set_cursor). wlroots hands a nested compositor its cursor as a dma-buf from the parent's - * formats - on Adreno usually UBWC, which the CPU cannot read - so it is blitted into a small linear, - * host-visible image first. Rare (a cursor changes shape, it is not redrawn per frame) and waited - * for on the CPU. out: w*h pixels, B8G8R8A8 in memory = 0xAARRGGBB ints. 0 = ok. */ -static struct vkp_image *g_rb; /* the readback image, kept for the next shape */ + + + + + +static struct vkp_image *g_rb; int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px) { if (!src || !out || src->w <= 0 || src->h <= 0 || src->w * src->h > max_px || @@ -2076,7 +1841,6 @@ int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px) { .newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = g_rb->image, .subresourceRange = range, .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, .dstAccessMask = VK_ACCESS_HOST_READ_BIT}, - /* the client's buffer goes back to it */ {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, .newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = src->dmabuf ? g_qfam : VK_QUEUE_FAMILY_IGNORED, @@ -2102,32 +1866,10 @@ int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px) { return 0; } -/* ---------------------------------------------------------------- the compositor pass, off-screen - * - * Layer mode with screen effects on: the game's frame still has to go through the compositor pass - * (scene image -> effects chain), but the RESULT does not have to end up in the app's swapchain - - * it is copied into a gralloc buffer and put on the game's own Android layer, so hardware - * composition stays alive while a Look is applied and the scene -> output mapping is done by the - * display (setGeometry) instead of a second full-screen GPU blit. - * - * The chain runs in its OWN submit, with no swapchain image acquired. That is deliberate and was - * measured: folding it into the submit that owns the acquired image (one command buffer, one - * present, which looks cheaper on paper) puts the whole 13-pass chain behind the acquire - * semaphore, i.e. behind a vblank - 72 fps instead of 111 fps on the Pocket FIT with Retro CRT at - * 1920x1080. The base surface's black frame is presented by the caller AFTER the layer - * transaction, so the layer never waits for a vblank either. - * - * Three steps because the chain's result size is only known once the chain has run, and the layer - * buffer is allocated from it: - * vkp_pass_begin() -> records composite + effects into the frame's command buffer - * vkp_pass_copy_to()-> appends the copy into the layer buffer, submits, waits - * vkp_pass_abort() -> drops it unsubmitted (no free layer buffer this frame) - * Frame generation is NOT run here: its extra frames need a present each, on consecutive vblanks, - * which one buffer per layer transaction cannot pace (see WAYLAND_RUNTIME.md). */ static struct { int active; - VkImage result; /* in TRANSFER_SRC_OPTIMAL */ + VkImage result; int rw, rh; } g_pass; @@ -2144,15 +1886,12 @@ int vkp_pass_begin_hdr(int scene_w, int scene_h, const struct vkp_draw *draws, i return pass_begin_impl(scene_w, scene_h, draws, n, hf, rw, rh); } -/* The off-screen pass for a layer: the SDR one (hf == NULL: composite into the 8-bit scene image, the - * effects in 8-bit - unchanged) or the HDR one (the draws composed into one 10-bit PQ BT.2020 picture, - * the effects run on it in 10-bit). */ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, int *rw, int *rh) { if (g_pass.active) vkp_pass_abort(); if (g_dev_state == -2 || dev_init() != 0 || !g_window || scene_w <= 0 || scene_h <= 0) return -1; - check_surface_changed(); /* as in render_impl: the effects pass maps onto the output's size */ - if (!g_swapchain && swap_init() != 0) return -1; /* the mapping is in output pixels */ + check_surface_changed(); + if (!g_swapchain && swap_init() != 0) return -1; update_map(scene_w, scene_h); if (!g_map.valid) return -1; if (hf) { @@ -2171,8 +1910,6 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw blendp_begin_frame(); VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - /* Same acquisition as vkp_render's compositor pass: the sources come from the client's queue - * family (dmabuf) or from a host write (shm), the scene image is the transfer destination. */ VkImageMemoryBarrier *bars = calloc((size_t)n + 1, sizeof(*bars)); if (!bars) { g_vk.EndCommandBuffer(cmd); return -1; } int nb = 0; @@ -2209,11 +1946,9 @@ static int pass_begin_impl(int scene_w, int scene_h, const struct vkp_draw *draw free(bars); if (hf) { - /* HDR: every draw into the mixed image, then ONE encoding into the 10-bit picture: PQ BT.2020, or - * tone-mapped sRGB while the drawer's HDR output switch is off. */ if (compose_hdr(cmd, draws, n, hf, scene_w, scene_h, g_hdrscene.img, HDR_FMT, hf->tonemap ? HDRC_OUT_SDR : HDRC_OUT_PQ) != 0) { - g_vk.EndCommandBuffer(cmd); /* never submitted; reset before its next use */ + g_vk.EndCommandBuffer(cmd); return -1; } if (vkp_effects_active()) vkp_effects_set_formats(HDR_FMT, HDR_FMT); @@ -2253,8 +1988,8 @@ int vkp_pass_copy_to(struct vkp_image *dst, int wait_fd) { g_vk.EndCommandBuffer(cmd); return -1; } - const int gpu_wait = take_wait_fd(wait_fd); /* the display's release of dst, before the copy writes it */ - if (gpu_wait < 0) { g_vk.EndCommandBuffer(cmd); return -1; } /* never submitted; reset before its next use */ + const int gpu_wait = take_wait_fd(wait_fd); + if (gpu_wait < 0) { g_vk.EndCommandBuffer(cmd); return -1; } VkImageSubresourceRange range = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; VkImageMemoryBarrier acq = { @@ -2264,9 +1999,6 @@ int vkp_pass_copy_to(struct vkp_image *dst, int wait_fd) { .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT}; g_vk.CmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 1, &acq); - /* The result stands for the whole scene, and so does the layer buffer (the DPU maps it onto - * the output), so this is 1:1 whenever the buffer was allocated at the size begin() reported; - * a stale-sized buffer is resampled rather than shown wrong. */ VkImageBlit blit = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .srcOffsets = {{0, 0, 0}, {g_pass.rw, g_pass.rh, 1}}, .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, @@ -2303,5 +2035,5 @@ int vkp_pass_copy_to(struct vkp_image *dst, int wait_fd) { void vkp_pass_abort(void) { if (!g_pass.active) return; g_pass.active = 0; - g_vk.EndCommandBuffer(g_cmds[0]); /* never submitted; the buffer is reset before its next use */ + g_vk.EndCommandBuffer(g_cmds[0]); } diff --git a/app/src/main/cpp/waylandcomp/src/vk_present.h b/app/src/main/cpp/waylandcomp/src/vk_present.h index 2a752c343..5b050d479 100644 --- a/app/src/main/cpp/waylandcomp/src/vk_present.h +++ b/app/src/main/cpp/waylandcomp/src/vk_present.h @@ -2,70 +2,34 @@ #define VK_PRESENT_H #include <stdint.h> #include <android/native_window.h> -/* - * Android-surface render backend for the embedded Wayland compositor. - * Owns a Turnip VkDevice + a swapchain on the SurfaceView's ANativeWindow. Each - * client buffer becomes an image (a dmabuf from winewayland's Vulkan WSI is - * imported zero-copy; a wl_shm buffer is copied into a host-visible image), and - * every frame blits the whole scene - desktop, windows, subsurfaces - in order. - */ - -// Set the Turnip driver to load (adrenotools). Call before the first frame. -// NULL args -> the backend falls back to the system libvulkan (dmabuf import will -// likely fail - Adreno lacks drm_format_modifier). driver_path ends with '/'. void vk_present_set_driver(const char *driver_path, const char *library_name, const char *native_lib_dir); -// Set/replace the output window (from Surface via ANativeWindow_fromSurface; the backend -// owns the reference from then on). NULL = the surface is gone. Callable from any thread -// and never blocks: the request is applied by the compositor thread (vkp_render / -// vkp_apply_window_request), which tears the old swapchain down and releases the old window. void vk_present_set_window(ANativeWindow *window); -/* Compositor thread: apply a pending window change now. Returns 1 if the window changed. */ int vkp_apply_window_request(void); -/* How the scene is mapped onto the output (the app's Container.FULLSCREEN_* / ALIGN_* values, - * mirrored 1:1 from ViewTransformation.java so touch input, which is mapped by the app with - * that class, lands on the same pixels). Callable from any thread, applies on the next frame. */ enum vkp_scale_mode { VKP_MODE_OFF = 0, VKP_MODE_FIT = 1, VKP_MODE_STRETCH = 2, VKP_MODE_FILL = 3, VKP_MODE_INTEGER = 4 }; enum vkp_align { VKP_ALIGN_CENTER = 0, VKP_ALIGN_TOP = 1, VKP_ALIGN_BOTTOM = 2 }; void vk_present_set_scale_mode(int mode, int alignment); -/* Output pixel (0..output size) -> scene pixel through the current mapping (compositor thread). - * Returns 0 before the first frame has established a mapping (sx/sy untouched). */ int vkp_output_to_scene(double ox, double oy, double *sx, double *sy); -/* Output size in pixels (0x0 before the first swapchain). */ void vkp_output_size(int *w, int *h); struct vkp_image; -/* DRM format modifiers this backend knows the memory layout of (both single-plane on Adreno): - * LINEAR, and QCOM_COMPRESSED = UBWC (drm_fourcc.h: fourcc_mod_code(QCOM = 0x05, 1)). */ #define VKP_MOD_LINEAR 0x0000000000000000ULL #define VKP_MOD_QCOM_COMPRESSED 0x0500000000000001ULL #define VKP_MOD_INVALID 0x00ffffffffffffffULL -/* "linear", "qcom_compressed", or "modifier 0x…" for anything else (static buffer). */ const char *vkp_modifier_name(uint64_t modifier); -/* The modifiers a dma-buf of this DRM fourcc can be imported with as a blit source, asked of the - * renderer's own driver (VkDrmFormatModifierPropertiesListEXT, each confirmed for a dma-buf-backed - * TRANSFER_SRC image with vkGetPhysicalDeviceImageFormatProperties2). Only LINEAR and - * QCOM_COMPRESSED are ever returned (the ones this file can describe a plane layout for); other - * modifiers the driver reports are logged once. Returns the count, 0 when the device is not up - * (LINEAR is then the only safe assumption). Compositor thread. */ int vkp_dmabuf_modifiers(uint32_t drm_format, uint64_t *out, int max); -// Import a dmabuf (single plane). NULL on failure. The image aliases the buffer, so -// later client frames rendered into the same buffer show up without re-importing. struct vkp_image *vkp_image_from_dmabuf(int fd, uint32_t drm_format, uint64_t modifier, int w, int h, uint32_t stride, uint32_t offset); -// Same, choosing the role: as_blit_dst = 0 imports a client frame (blit source), 1 imports a -// buffer this backend blits INTO (layer mode's AHardwareBuffer pool, see sc_layer.h). struct vkp_image *vkp_image_import_dmabuf(int fd, uint32_t drm_format, uint64_t modifier, int w, int h, uint32_t stride, uint32_t offset, int as_blit_dst); int vkp_image_is_dmabuf(const struct vkp_image *img); -// Create a host-visible image and copy BGRA/XRGB8888 pixels into it. NULL on failure. struct vkp_image *vkp_image_create_shm(int w, int h); void vkp_image_upload_shm(struct vkp_image *img, const void *data, int stride); @@ -73,117 +37,61 @@ int vkp_image_width(const struct vkp_image *img); int vkp_image_height(const struct vkp_image *img); void vkp_image_destroy(struct vkp_image *img); -// One scene draw: the src rectangle of an image (image pixels) scaled into the dst -// rectangle (scene pixels). The scene is mapped onto the output by the scale mode. struct vkp_draw { struct vkp_image *img; float sx, sy, sw, sh; int dx, dy, dw, dh; - int blend; /* translucent: composed over what is under it (blend_pass.h) */ + int blend; }; -// The GPU the renderer runs on ("Adreno (TM) 750"), empty before the device is up. const char *vkp_gpu_name(void); -// 0 if the renderer can create images (device up), -1 otherwise. int vkp_ready(void); -/* Whether an output window is attached (or requested); without one vkp_render() draws nothing. */ int vkp_has_window(void); -/* 1 once the Vulkan device was lost: nothing is presented any more (the session must restart). */ int vkp_device_lost(void); -// Clear to black, blit the draws in order (first = bottom) and present. -// Returns 0 on success, -1 if nothing could be presented (no window yet, etc.). int vkp_render(int scene_w, int scene_h, const struct vkp_draw *draws, int n); -/* A black frame with no compositor pass (no effects, no frame generation), whatever is armed: the - * base surface under an HDR game that keeps its display layer (compositor.c). */ int vkp_render_plain(int scene_w, int scene_h); -/* Layer mode's base surface: make sure it shows a plain black frame under the display layers. A frame - * is presented only when the base is not already black on the current swapchain (entering layer mode, - * after a copy-path / frame-generation frame, after a swapchain rebuild); otherwise nothing is drawn - * or presented at all. 0 = the base is black, -1 = no output. Compositor thread. */ int vkp_base_black(int scene_w, int scene_h); -/* ---- perf counters (the 10 s `perf` line, compositor.c) ---- - * Everything the compositor thread spends in the driver, since the last vkp_perf_take(). Times in ns. */ struct vkp_perf { unsigned acquires, presents, waits; - int64_t acquire_ns, acquire_max_ns; /* vkAcquireNextImageKHR */ - int64_t present_ns, present_max_ns; /* vkQueuePresentKHR */ - int64_t wait_ns, wait_max_ns; /* vkWaitForFences on the compositor's own work */ - unsigned base_presents; /* black frames presented under the display layers */ - unsigned base_kept; /* layer frames that kept the black frame already there */ - unsigned gpu_release_waits; /* layer-buffer release fences waited for on the GPU */ + int64_t acquire_ns, acquire_max_ns; + int64_t present_ns, present_max_ns; + int64_t wait_ns, wait_max_ns; + unsigned base_presents; + unsigned base_kept; + unsigned gpu_release_waits; }; void vkp_perf_take(struct vkp_perf *out); -/* 1 when a release fence (sync_file) can be handed to the GPU as a wait (VK_KHR_external_semaphore_fd): - * the layer pool then reuses a buffer the display is still reading without blocking on the CPU. */ int vkp_can_wait_sync_fd(void); -/* ---- HDR composition (hdr_compose.h) ---- - * An HDR game that cannot be alone on its display layer: the scene is composed into ONE encoding. */ struct droiddeck_color; struct vkp_hdr_frame { - const unsigned char *is_hdr; /* per draw: 1 = its image holds PQ BT.2020 (the surface's description) */ - float peak_nits; /* the HDR content's peak, for a tone-map (max CLL / mastering max; 0 = 1000) */ - int tonemap; /* 1 = the drawer's HDR output switch is off: tone-map to SDR, never PQ */ - const struct droiddeck_color *color; /* the topmost HDR draw's description: an HDR10 swapchain's metadata */ + const unsigned char *is_hdr; + float peak_nits; + int tonemap; + const struct droiddeck_color *color; }; -/* vkp_render for a scene with HDR draws (frame generation): composed into PQ and presented through an - * HDR10 swapchain where the surface offers one (and hf->tonemap is 0), else tone-mapped into the ordinary - * one. *how = 1 HDR10, 2 tone-mapped, 0 the HDR pass was unavailable (shown the old way). Same return as - * vkp_render. */ int vkp_render_hdr(int scene_w, int scene_h, const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, int *how); -/* vkp_pass_begin for a scene with HDR draws: composed into a 10-bit picture - PQ BT.2020, or sRGB - * tone-mapped when hf->tonemap - the effects run on it in 10-bit, the result (rw x rh, A2B10G10R10) is - * left for vkp_pass_copy_to() into a layer buffer (a blit: 10-bit or 8-bit). -1 = not possible (nothing - * recorded). */ int vkp_pass_begin_hdr(int scene_w, int scene_h, const struct vkp_draw *draws, int n, const struct vkp_hdr_frame *hf, int *rw, int *rh); -/* ---- layer mode helpers (sc_layer.c; compositor thread) ---- */ -/* Whole-image copy of src into dst (a blit-destination dmabuf image), waited for on the CPU. - * wait_fd: a sync_file the copy must wait for before writing dst (the display's release fence of that - * buffer), -1 = none. Always consumed (handed to the GPU, or waited for and closed). */ int vkp_blit_image(struct vkp_image *src, struct vkp_image *dst, int wait_fd); -/* A client's GPU cursor (an imported dma-buf, often UBWC) read back as w*h 0xAARRGGBB pixels through a - * linear host-visible copy; waited for on the CPU. Compositor thread, between frames. 0 = ok. */ int vkp_image_readback(struct vkp_image *src, uint32_t *out, int max_px); -/* Refresh the scene -> output mapping for this scene size without presenting (creates the - * swapchain if needed, since the mapping is in output pixels). 0 = mapping valid. */ int vkp_update_map(int scene_w, int scene_h); -/* Map a draw through the current mapping: out = {src x0,y0,x1,y1 (image px), dst x0,y0,x1,y1 - * (output px)}, clipped like the blit path. 0 = nothing of it is visible. */ int vkp_map_draw(const struct vkp_draw *d, int out[8]); -/* The same for a bare w x h buffer shown over the whole scene (no vkp_image: zero-copy layers). */ int vkp_map_rect(int img_w, int img_h, int scene_w, int scene_h, int out[8]); -/* The output window frames go to (NULL = none); compositor thread. */ -/* Degrees the display rotates every layer we present by (0/90/180/270), -1 before the swapchain - * exists. From VkSurfaceCapabilitiesKHR::currentTransform, i.e. what the presentation engine says - * it does - never a device allowlist. The layer path uses it to decide whether a SECOND display - * layer is affordable on this display (sc_layer_present_overlay). */ int vkp_surface_rotation_degrees(void); ANativeWindow *vkp_window(void); -/* Fire the one-shot first-frame notification (layer mode presents outside vkp_render). */ void vkp_signal_first_frame(void); -/* ---- the compositor pass into a layer buffer (sc_layer.c; compositor thread) ---- - * Screen effects on the game's own Android layer: compose `draws` into the scene image and run the - * effects chain WITHOUT presenting, then copy the result into a gralloc layer buffer. Three steps - * because the chain's result size (a scaling mode resizes to the scene's mapped output size) is - * only known once the chain has run, and the layer buffer is allocated from it. - * begin: 0 = a pass is in progress, its result is *rw x *rh; -1 = not possible (draw the old way). - * Exactly one of copy_to (blit into dst, submit, wait; 0 = done) / abort (drop it unsubmitted) - * must follow a successful begin. The chain deliberately runs with NO swapchain image acquired - - * see the comment in vk_present.c. Frame generation is not run here (see WAYLAND_RUNTIME.md). */ int vkp_pass_begin(int scene_w, int scene_h, const struct vkp_draw *draws, int n, int *rw, int *rh); -/* wait_fd: as for vkp_blit_image (consumed in every case). */ int vkp_pass_copy_to(struct vkp_image *dst, int wait_fd); void vkp_pass_abort(void); -// Session log (compositor.c): one line to Download/Wayland-logs and logcat. void droiddeck_log(const char *tag, const char *fmt, ...) __attribute__((format(printf, 2, 3))); #endif diff --git a/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c b/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c index 4a9dd4d23..b1125ce38 100644 --- a/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c +++ b/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c @@ -1,9 +1,3 @@ -/* - * JNI entry for the embedded Wayland compositor (experimental parallel runtime). - * Starts the compositor on a dedicated thread (it blocks in the wl event loop). - * The render-to-Surface backend + input are added in the M4 phase; this brings up - * the server so a Wayland client (eventually winewayland.drv) can connect. - */ #include <jni.h> #include <errno.h> #include <stdbool.h> @@ -44,15 +38,15 @@ extern volatile int g_output_w, g_output_h; #define TAG "DroidDeckWayland" static JavaVM *g_jvm; -static volatile pid_t g_comp_tid; /* the compositor thread, for the app's ADPF hint session */ -static jclass g_compositor_cls; /* global ref */ -static jmethodID g_on_first_frame; /* static void onFirstFramePresented() */ -static jmethodID g_on_game_surface; /* static void onGameSurface(String, String) */ -static jmethodID g_on_game_frame; /* static void onGameFrame() */ -static jmethodID g_on_game_program; /* static void onGameProgram(int, String) */ -static jmethodID g_on_pointer_lock; /* static void onPointerLock(boolean, int, int) */ -static jmethodID g_on_clipboard; /* static void onClipboardText(byte[]) */ -static jmethodID g_on_text_input; /* static void onTextInput(boolean, String, int, int, int, int) */ +static volatile pid_t g_comp_tid; +static jclass g_compositor_cls; +static jmethodID g_on_first_frame; +static jmethodID g_on_game_surface; +static jmethodID g_on_game_frame; +static jmethodID g_on_game_program; +static jmethodID g_on_pointer_lock; +static jmethodID g_on_clipboard; +static jmethodID g_on_text_input; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { (void)reserved; @@ -79,9 +73,6 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { return JNI_VERSION_1_6; } -/* Called from vk_present.c on the compositor thread when the first client frame is - * presented. Attaches to the JVM (this thread is a bare pthread) and calls back into - * Java so the launch overlay can dismiss. Fires exactly once. */ void droiddeck_on_first_frame(void) { if (!g_jvm || !g_compositor_cls || !g_on_first_frame) return; JNIEnv *env = NULL; @@ -96,8 +87,6 @@ void droiddeck_on_first_frame(void) { __android_log_print(ANDROID_LOG_INFO, TAG, "first client frame presented -> notified app"); } -/* The compositor thread stays attached once it first calls into Java: the HUD gets an upcall - * for every game frame. */ static __thread JNIEnv *t_env; static __thread int t_attached; @@ -112,7 +101,6 @@ static JNIEnv *thread_env(void) { return t_env = env; } -/* A window started presenting GPU frames (window = its description), or NULL when it closed. */ void droiddeck_on_game_surface(const char *window, const char *gpu) { JNIEnv *env; if (!g_compositor_cls || !g_on_game_surface || !(env = thread_env())) return; @@ -124,8 +112,6 @@ void droiddeck_on_game_surface(const char *window, const char *gpu) { if (jg) (*env)->DeleteLocalRef(env, jg); } -/* The program behind the game window that just started presenting: its Linux pid and executable name - * ("" = unknown). The app arms its launch-time CPU affinity on it. Compositor thread. */ void droiddeck_on_game_program(int pid, const char *program) { JNIEnv *env; if (!g_compositor_cls || !g_on_game_program || !(env = thread_env())) return; @@ -135,7 +121,6 @@ void droiddeck_on_game_program(int pid, const char *program) { if (jp) (*env)->DeleteLocalRef(env, jp); } -/* One GPU frame from that window. */ void droiddeck_on_game_frame(void) { JNIEnv *env; if (!g_compositor_cls || !g_on_game_frame || !(env = thread_env())) return; @@ -143,9 +128,6 @@ void droiddeck_on_game_frame(void) { if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); } -/* A program locked the pointer (locked = 1; the app's input path switches to deltas) or the lock - * ended (locked = 0; x,y = where the pointer is now, in scene coordinates, for the app to re-sync - * its own pointer to). Compositor thread. */ void droiddeck_on_pointer_lock(int locked, int x, int y) { JNIEnv *env; if (!g_compositor_cls || !g_on_pointer_lock || !(env = thread_env())) return; @@ -153,8 +135,6 @@ void droiddeck_on_pointer_lock(int locked, int x, int y) { if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env); } -/* A program copied text (UTF-8, not NUL-terminated for the app's sake - a byte[] so emoji and - * NULs survive JNI). Compositor thread. */ void droiddeck_on_clipboard_text(const char *utf8, int len) { JNIEnv *env; if (!g_compositor_cls || !g_on_clipboard || !(env = thread_env())) return; @@ -166,8 +146,6 @@ void droiddeck_on_clipboard_text(const char *utf8, int len) { (*env)->DeleteLocalRef(env, arr); } -/* A program started (enabled) or stopped accepting IME text; x,y,w,h = its caret rectangle in - * scene pixels (all 0 = unknown). Compositor thread. */ void droiddeck_on_text_input(int enabled, const char *program, int x, int y, int w, int h) { JNIEnv *env; if (!g_compositor_cls || !g_on_text_input || !(env = thread_env())) return; @@ -178,16 +156,11 @@ void droiddeck_on_text_input(int enabled, const char *program, int x, int y, int if (jp) (*env)->DeleteLocalRef(env, jp); } -/* android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY: the level the hwui RenderThread runs at. */ #define COMPOSITOR_NICE (-8) static void *comp_thread(void *arg) { (void)arg; __android_log_print(ANDROID_LOG_INFO, TAG, "compositor thread starting"); - /* Every buffer release, frame callback and layer transaction of the session goes through this one - * thread. It is named, so `ps -T`, the logs and the drawer's Thread Priority Boost (PerfPriority - * matches thread names) can find it, and it runs at display priority instead of whatever the - * thread that started it had. Never lowered: a thread that already runs hotter keeps its value. */ pthread_setname_np(pthread_self(), "wl-compositor"); const id_t tid = (id_t)gettid(); g_comp_tid = (pid_t)tid; @@ -225,15 +198,12 @@ static void start_thread(void) { __android_log_print(ANDROID_LOG_ERROR, TAG, "pthread_create failed"); } -/* The compositor thread's tid, or 0 before it starts. The app opens a PerformanceHintManager session - * on it and reports each presented frame's interval, so the governor sees a frame that ran long. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCompositorTid(JNIEnv *env, jclass clazz) { (void)env; (void)clazz; return (jint)g_comp_tid; } -/* Headless start (no output window) - used for bring-up tests. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStart(JNIEnv *env, jclass clazz, jstring xdgRuntimeDir) { @@ -249,8 +219,6 @@ static char *dup_jstr(JNIEnv *env, jstring s) { return out; } -/* Start with a real output Surface + the container's Turnip driver (adrenotools). - * Frames committed by clients are composited to this Surface via Turnip. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStartWithSurface( JNIEnv *env, jclass clazz, jobject surface, jstring xdgRuntimeDir, @@ -263,14 +231,12 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeStartWithSurface( free(dp); free(ln); free(nl); if (surface) { ANativeWindow *win = ANativeWindow_fromSurface(env, surface); - vk_present_set_window(win); /* backend acquires; released on nativeSetSurface(null) */ + vk_present_set_window(win); __android_log_print(ANDROID_LOG_INFO, TAG, "output window bound (%p)", (void *)win); } start_thread(); } -/* Inject a pointer event from the Android SurfaceView touch listener (UI thread). - * action: 0=down 1=move 2=up; x/y in output space (0..1919, 0..1079). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendPointer( JNIEnv *env, jclass clazz, jint action, jint x, jint y) { @@ -283,9 +249,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendTouch( droiddeck_wayland_send_touch(action, pointer_id, x, y); } -/* The client's pointer image and whether it wants one at all (wl_pointer.set_cursor). - * out = [serial, hidden, w, h, hotspotX, hotspotY, ARGB pixels...]; returns ints written, 0 if the - * array is too small. The app polls the serial and only rebuilds its overlay when it changes. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSnapshot( JNIEnv *env, jclass clazz, jintArray out) { @@ -298,98 +261,75 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSnapshot( return n; } -/* The serial nativeCursorSnapshot would return, without copying anything. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeCursorSerial(JNIEnv *env, jclass clazz) { (void)env; (void)clazz; return droiddeck_cursor_serial(); } -/* Inject a key event. evdev = Linux input keycode (KEY_A=30…); state 1=down 0=up. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendKey( JNIEnv *env, jclass clazz, jint evdev, jint state) { droiddeck_wayland_send_key(evdev, state); } -/* App X-server input in scene (virtual desktop) coordinates; see droiddeck_wayland_send_scene_input. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSendSceneInput( JNIEnv *env, jclass clazz, jint type, jint a, jint b) { droiddeck_wayland_send_scene_input(type, a, b); } -/* One screen refresh (Choreographer frame callback, UI thread): the compositor draws once. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeVsync(JNIEnv *env, jclass clazz, jlong frameTimeNanos) { droiddeck_wayland_vsync((int64_t)frameTimeNanos); } -/* Shortcut launches: don't draw explorer's windows (desktop, taskbar), like X11's unviewable classes. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHideShell(JNIEnv *env, jclass clazz, jboolean hide) { g_hide_shell = hide ? 1 : 0; } -/* Zero-copy layer mode (DROIDDECK_WAYLAND_ZERO_COPY=1 at launch, the drawer's switch live): one fullscreen - * window on its own Android layer instead of the swapchain blit (sc_layer.c / ahb_swapchain.c). Any - * thread, any time: before the compositor starts it is the launch default; afterwards the compositor - * thread flips the state, tells the bound games to rebuild their swapchains for it and redraws. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetZeroCopy(JNIEnv *env, jclass clazz, jboolean on) { int live = droiddeck_get_display() != NULL; - if (!live) g_zero_copy = on ? 1 : 0; /* read by ahb_swapchain_init before the queue drains */ + if (!live) g_zero_copy = on ? 1 : 0; droiddeck_host_zero_copy(on ? 1 : 0, live); __android_log_print(ANDROID_LOG_INFO, TAG, "zero-copy layer mode %s%s", on ? "on" : "off", live ? " (live)" : ""); } -/* Milliseconds since the compositor last put a game frame on the layer without a copy; -1 = never - * this session. The drawer's status line ("switching..." until the frames arrive / stop). */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeZeroCopyLastFrameAgeMs(JNIEnv *env, jclass clazz) { return (jint)ahb_swapchain_last_frame_age_ms(); } -/* Zero-copy frames in the last completed 10 s stats window (on_stats_timer), for the drawer's live line. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeZeroCopyFrames(JNIEnv *env, jclass clazz) { return (jint)g_zero_copy_last; } -/* Compressed (UBWC) game buffers: advertise DRM_FORMAT_MOD_QCOM_COMPRESSED on zwp_linux_dmabuf_v1 when - * the renderer's driver imports it (default on; DROIDDECK_WAYLAND_UBWC=0 = off). Set before the compositor - * starts. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetUbwc(JNIEnv *env, jclass clazz, jboolean on) { g_ubwc = on ? 1 : 0; __android_log_print(ANDROID_LOG_INFO, TAG, "compressed (UBWC) game buffers %s", on ? "on" : "off"); } -/* Debug: name no DRM device in the dma-buf feedback (DROIDDECK_WAYLAND_NO_RENDER_NODE=1), the way a - * phone that exposes no /dev/dri node to apps does. Set before the compositor starts. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetNoRenderNode(JNIEnv *env, jclass clazz, jboolean on) { g_no_render_node = on ? 1 : 0; if (on) __android_log_print(ANDROID_LOG_INFO, TAG, "debug: advertising no DRM device (main device 0:0)"); } -/* VRR / refresh-rate matching: the rate the app is voting for the panel, mirrored onto the game's - * own SurfaceControl layer. Under zero-copy the game's frames bypass the app's surface entirely, so - * without this SurfaceFlinger never sees the game's cadence. 0 = clear the vote. Any thread, any - * time; the compositor applies it with its next layer transaction and logs the change. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetLayerFrameRate(JNIEnv *env, jclass clazz, jfloat hz) { (void)env; (void)clazz; sc_layer_set_frame_rate((float)hz); } -/* The panel's refresh rate (Hz) for the advertised wl_output mode. Set before the compositor starts. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetOutputRefreshRate(JNIEnv *env, jclass clazz, jfloat hz) { g_output_refresh_mhz = hz > 1.0f ? (int)(hz * 1000.0f + 0.5f) : 0; } -/* The container's screen size for the advertised wl_output mode. Set before the compositor starts. */ extern void vkp_reset_first_frame(void); JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeResetFirstFrame(JNIEnv *env, jclass clazz) { @@ -403,8 +343,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetOutputSize(JNIEnv g_output_h = h > 0 ? h : 0; } -/* The output's size changed while the compositor runs (a foldable opening or closing with the - * session following the screen): store it and have the compositor thread tell the clients. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeResizeOutput(JNIEnv *env, jclass clazz, jint w, jint h) { (void)env; (void)clazz; @@ -414,57 +352,44 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeResizeOutput(JNIEnv droiddeck_wayland_output_resized(); } -/* Fullscreen mode + screen alignment (Container.FULLSCREEN_* / ALIGN_* values): how the scene is - * fitted onto the output. Any thread, any time; the next frame uses it. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetScaleMode(JNIEnv *env, jclass clazz, jint mode, jint alignment) { vk_present_set_scale_mode(mode, alignment); __android_log_print(ANDROID_LOG_INFO, TAG, "scale mode %d alignment %d", mode, alignment); } -/* ---- screen effects (effects_chain.c): the X11 Vulkan renderer's post chain in the compositor. - * Any thread, any time; the compositor thread applies them on its next frame and logs one - * `effects` line per change. Values are 1:1 with VulkanRenderer's setters. */ -/* Scaling mode: 0=None 1=Linear 2=Nearest 3=SGSR 4=FSR 5=FSR Fit 6=Sharpen 7=NIS 8=SGSR HQ. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetUpscaler(JNIEnv *env, jclass clazz, jint mode) { vkp_effects_set_scaling(mode); } -/* The upscaler's sharpness slider 0..100 (RCAS lobe / SGSR edge / NIS sharpness). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetUpscaleSharpness(JNIEnv *env, jclass clazz, jint sharpness) { vkp_effects_set_upscale_sharpness(sharpness); } -/* AMD CAS sharpen toggle + level 0..100 (0 = pass off). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCas(JNIEnv *env, jclass clazz, jboolean enabled, jint sharpness) { vkp_effects_set_cas(enabled ? 1 : 0, sharpness); } -/* Fake-HDR toggle. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdr(JNIEnv *env, jclass clazz, jboolean enabled) { vkp_effects_set_hdr(enabled ? 1 : 0); } -/* Terminal debanding toggle + strength 0..200 (100 = 1 LSB). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetDeband(JNIEnv *env, jclass clazz, jboolean enabled, jint strength) { vkp_effects_set_deband(enabled ? 1 : 0, strength); } -/* Colour grade (slider units: brightness/contrast -100..100, gamma 0.5..3, saturation 0..200 %) and the - * FXAA / Toon / CRT / NTSC toggles - VulkanRenderer.setScreenEffects. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetScreenEffects(JNIEnv *env, jclass clazz, jfloat brightness, jfloat contrast, jfloat gamma, jfloat saturation, jboolean fxaa, jboolean toon, jboolean crt, jboolean ntsc) { vkp_effects_set_screen(brightness, contrast, gamma, saturation, fxaa ? 1 : 0, toon ? 1 : 0, crt ? 1 : 0, ntsc ? 1 : 0); } -/* Cardboard mode - WaylandCompositor.nativeSetCardboard. */ 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) { @@ -474,7 +399,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboard(JNIEnv vkp_cardboard_set(&p); } -/* The head pose (row-major 3x3, head -> world), from the sensor thread. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboardPose(JNIEnv *env, jclass clazz, jfloatArray rot, jboolean live) { @@ -484,7 +408,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboardPose(JNI vkp_cardboard_set_pose(m, live ? 1 : 0); } -/* Where the head pose is shared with the guest - WaylandCompositor.nativeSetCardboardPoseFile. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboardPoseFile(JNIEnv *env, jclass clazz, jstring path) { if (!path) return; @@ -494,9 +417,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetCardboardPoseFile (*env)->ReleaseStringUTFChars(env, path, p); } -/* One "display" line in the session log, written from Java: facts that live in the Android - * framework (the panel's HDR capability) rather than in the compositor. droiddeck_log() mirrors to - * logcat and appends to the session file when one is open, so this is safe at any point. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogDisplay(JNIEnv *env, jclass clazz, jstring message) { char *s = dup_jstr(env, message); @@ -504,8 +424,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogDisplay(JNIEnv *e free(s); } -/* The same under the "perf" area: facts the app knows about the session's performance setup (the CPU - * cores the game is pinned to, say). Same safety as nativeLogDisplay. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogPerf(JNIEnv *env, jclass clazz, jstring message) { char *s = dup_jstr(env, message); @@ -513,9 +431,7 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogPerf(JNIEnv *env, free(s); } -/* ---- HDR10 output, round 1 (droiddeck_color.h / wl_color_mgmt.c) ---- */ -/* The opt-in: mode 0 off, 1 DROIDDECK_WAYLAND_HDR=1, 2 =force (testing). Before the compositor starts. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrRequest(JNIEnv *env, jclass clazz, jint mode, jstring source, jboolean dxvkHdr, jboolean zeroCopyForced) { @@ -524,7 +440,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrRequest(JNIEnv free(s); } -/* The game's display as android.view.Display reports it (before the start; again on every change). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrDisplay(JNIEnv *env, jclass clazz, jint id, jstring name, jstring formats, jboolean hdr10, jfloat maxLum, jfloat maxAvg, jfloat minLum, jboolean ratioAvailable, @@ -535,38 +450,32 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrDisplay(JNIEnv free(n); free(f); } -/* One Display.getHdrSdrRatio() reading (listener = from the display's ratio listener). Any thread. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrSdrRatioSample(JNIEnv *env, jclass clazz, jfloat ratio, jboolean listener) { droiddeck_color_ratio_sample((float)ratio, listener ? 1 : 0); } -/* ms since an HDR frame last reached a display layer, -1 = never this session. Any thread. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrLastFrameAgeMs(JNIEnv *env, jclass clazz) { return (jint)droiddeck_color_last_frame_age_ms(); } -/* -1 = not decided yet, 0 = closed, 1 = open. Any thread. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrGateState(JNIEnv *env, jclass clazz) { return (jint)droiddeck_color_gate_state(); } -/* The session is ending: the "HDR on screen: ..." summary line. Any thread, once. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrSessionEnd(JNIEnv *env, jclass clazz) { droiddeck_color_session_end(); } -/* HDR frames really on screen right now (the HUD badge). Any thread. */ JNIEXPORT jboolean JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrOnScreen(JNIEnv *env, jclass clazz) { return droiddeck_color_hdr_on_screen() ? JNI_TRUE : JNI_FALSE; } -/* One "color" line in the session log from Java. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogColor(JNIEnv *env, jclass clazz, jstring message) { char *s = dup_jstr(env, message); @@ -574,8 +483,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLogColor(JNIEnv *env free(s); } -/* One line in the session log under an area Java names ("gpu", "nvapi"): launch facts the app decides. - * The area is the log's 9-character column; an empty one reads "app". */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLog(JNIEnv *env, jclass clazz, jstring area, jstring message) { char *a = dup_jstr(env, area), *s = dup_jstr(env, message); @@ -584,18 +491,15 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeLog(JNIEnv *env, jcl free(s); } -/* SDR content's level inside an HDR picture, in nits (default 203). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrSdrWhite(JNIEnv *env, jclass clazz, jfloat nits) { droiddeck_color_set_sdr_white((float)nits); } -/* The drawer's live HDR output switch: on = HDR frames as HDR, off = the same frames tone-mapped to SDR. - * Applied on the compositor thread (logged there, with a redraw). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrOutput(JNIEnv *env, jclass clazz, jboolean on) { if (droiddeck_get_display()) droiddeck_host_hdr_output(on ? 1 : 0); - else droiddeck_color_set_output(on ? 1 : 0); /* no compositor thread yet: nothing is drawing either */ + else droiddeck_color_set_output(on ? 1 : 0); } JNIEXPORT jboolean JNICALL @@ -603,7 +507,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrOutput(JNIEnv *en return droiddeck_color_output() ? JNI_TRUE : JNI_FALSE; } -/* Device evidence for the HDR lines: thermal status + headroom, brightness + mode. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrEnvSample(JNIEnv *env, jclass clazz, jint thermal, jfloat headroom, jint brightness, jint mode) { @@ -611,13 +514,11 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrEnvSample(JNIEnv (int)brightness, (int)mode); } -/* Display.getHighestHdrSdrRatio() (Android 16+), <= 0 = not reported. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetHdrHighestRatio(JNIEnv *env, jclass clazz, jfloat ratio) { droiddeck_color_set_highest_ratio((float)ratio == (float)ratio ? (float)ratio : -1.0f); } -/* The HDR headroom the screen surface should ask for (HDR10 swapchain frames in the last 1.5 s), 0 = none. */ JNIEXPORT jfloat JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrScreenHeadroom(JNIEnv *env, jclass clazz) { return (jfloat)droiddeck_color_screen_headroom(NULL, 0); @@ -630,25 +531,21 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrScreenHeadroomWhy return (*env)->NewStringUTF(env, why); } -/* The app's screen-surface request, for the no-headroom lines: > 0 asked, 0 cleared, -1 not possible. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrNoteHeadroomRequest(JNIEnv *env, jclass clazz, jfloat ratio) { droiddeck_color_note_headroom_request((float)ratio); } -/* 0 none, 1 HDR frames on screen with headroom, 2 HDR frames on screen without headroom for 5 s+. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrState(JNIEnv *env, jclass clazz) { return (jint)droiddeck_color_hdr_state(); } -/* An HDR game's frames were shown tone-mapped to SDR in the last 1.5 s (the drawer's status line). */ JNIEXPORT jboolean JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeHdrToneMappedOnScreen(JNIEnv *env, jclass clazz) { return droiddeck_color_tonemapped_on_screen() ? JNI_TRUE : JNI_FALSE; } -/* The Look the controls currently match (null = Custom) - only named in the session log. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetLookName(JNIEnv *env, jclass clazz, jstring name) { char *s = dup_jstr(env, name); @@ -656,14 +553,12 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetLookName(JNIEnv * free(s); } -/* The in-game FPS limiter: frames per second, 0 = unlimited. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetFpsLimit(JNIEnv *env, jclass clazz, jint fps) { g_fps_limit = fps > 0 ? fps : 0; __android_log_print(ANDROID_LOG_INFO, TAG, "fps limit %d", fps); } -/* Swap/clear the output window (e.g. SurfaceView recreated/destroyed). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetSurface( JNIEnv *env, jclass clazz, jobject surface) { @@ -674,8 +569,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetSurface( } } -/* ---- clipboard + text input (any thread; queued to the compositor thread) ---- - * Text crosses as UTF-8 byte arrays: JNI's modified UTF-8 would mangle emoji. */ static char *dup_bytes(JNIEnv *env, jbyteArray arr, int *len) { *len = 0; @@ -689,7 +582,6 @@ static char *dup_bytes(JNIEnv *env, jbyteArray arr, int *len) { return buf; } -/* Android's clipboard text becomes the guest's selection (empty/null = clear). */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetClipboardText(JNIEnv *env, jclass clazz, jbyteArray utf8) { int len; @@ -698,7 +590,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetClipboardText(JNI free(buf); } -/* Soft-keyboard text committed to the program accepting text input. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputCommit(JNIEnv *env, jclass clazz, jbyteArray utf8) { int len; @@ -707,7 +598,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputCommit(JNIE free(buf); } -/* Composing (pre-edit) text; cursorBegin/cursorEnd are character indexes into it, -1 = end. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputPreedit(JNIEnv *env, jclass clazz, jbyteArray utf8, jint cursorBegin, jint cursorEnd) { @@ -717,15 +607,11 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputPreedit(JNI free(buf); } -/* The IME deleted characters around the caret. */ JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputDelete(JNIEnv *env, jclass clazz, jint before, jint after) { droiddeck_host_text_delete(before, after); } -/* KGSL's power control, off (the GPU held at its top clock) or back on. The property is the - * device's, not this process's: it outlives the fd and the app, so the caller clears it at stop - * and again at every app start. No root; the kernel's thermal limits still apply. */ extern void adrenotools_set_turbo(bool turbo); JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetGpuTurbo(JNIEnv *env, jclass clazz, jboolean on) { @@ -733,10 +619,6 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetGpuTurbo(JNIEnv * adrenotools_set_turbo(on == JNI_TRUE); } -/* Raises one of our own processes or threads to `nice` with a bare setpriority(), never lowering - * it. android.os.Process.setThreadPriority is not used for this: it also moves the thread between - * scheduling groups by priority, and a guest process must stay where Android put the app. Returns - * the nice value it is left at, or 100 when it could not be read. */ JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeRaisePriority(JNIEnv *env, jclass clazz, jint tid, jint nice) { (void)env; (void)clazz; diff --git a/app/src/main/cpp/waylandcomp/src/wl_clipboard.c b/app/src/main/cpp/waylandcomp/src/wl_clipboard.c index 7668090f7..6547db7db 100644 --- a/app/src/main/cpp/waylandcomp/src/wl_clipboard.c +++ b/app/src/main/cpp/waylandcomp/src/wl_clipboard.c @@ -1,24 +1,3 @@ -/* - * Clipboard: wl_data_device_manager (core, v3) and zwlr_data_control_manager_v1 (v1), selection - * only - drag and drop is refused. Both protocols share one selection: - * - * owner CLIENT a program's wl_data_source / zwlr_data_control_source_v1 (its mime list) - * owner HOST text the app pushed from Android's clipboard (offered under the text mimes) - * owner NONE nothing - * - * Every selection change is announced to every data-control device (they see the selection - * regardless of focus, as the protocol says) and to the wl_data_devices of the client holding - * keyboard focus; a client also gets the current selection when its keyboard focus arrives. - * - * winewayland prefers zwlr_data_control (it then runs the clipboard from the desktop process - * without needing keyboard focus) and tags its own offers with application/x.winewayland.tag - * so it can ignore them when they come back; we pass mime lists through untouched. - * - * Guest → Android: when a program sets a selection with a text mime, the compositor asks for it - * once over a pipe on the event loop (non-blocking, 1 MiB cap, 3 s timeout) and hands the text - * to the app (droiddeck_on_clipboard_text → ClipboardManager). Android → guest: the app's text - * becomes the HOST selection; receive requests are answered with a non-blocking writer. - */ #define _GNU_SOURCE 1 #include <stdlib.h> #include <string.h> @@ -37,17 +16,17 @@ static struct wl_display *g_disp; enum owner { OWNER_NONE, OWNER_CLIENT, OWNER_HOST }; static enum owner g_owner; -static uint32_t g_gen; /* bumps on every selection change */ +static uint32_t g_gen; -struct source { /* a client's data source (either protocol) */ +struct source { struct wl_resource *res; int is_control; char *mimes[MAX_MIMES]; int nmimes; }; -static struct source *g_src; /* current CLIENT selection */ +static struct source *g_src; -static char *g_host_text; /* current HOST selection */ +static char *g_host_text; static size_t g_host_len; static const char *const k_text_mimes[] = { "text/plain;charset=utf-8", "text/plain", "UTF8_STRING", "TEXT", "STRING", NULL @@ -59,18 +38,17 @@ struct device { struct wl_list link; }; static struct wl_list g_devices; -static struct wl_client *g_kb_client; /* keyboard focus holder */ +static struct wl_client *g_kb_client; struct offer { struct wl_resource *res; int is_control; - uint32_t gen; /* the selection this offer describes */ + uint32_t gen; }; -static char *g_last_guest_text; /* echo guard: the last text handed to Android */ +static char *g_last_guest_text; static size_t g_last_guest_len; -/* ------------------------------------------------------------------ helpers */ static int is_text_mime(const char *m) { for (int i = 0; k_text_mimes[i]; i++) if (!strcmp(m, k_text_mimes[i])) return 1; @@ -94,7 +72,6 @@ static void source_send_cancelled(struct source *s) { else wl_data_source_send_cancelled(s->res); } -/* ------------------------------------------------------------------ host → client writer */ struct writer { int fd; @@ -115,9 +92,9 @@ static int writer_pump(struct writer *w) { while (w->off < w->len) { ssize_t n = write(w->fd, w->data + w->off, w->len - w->off); if (n > 0) { w->off += (size_t)n; continue; } - if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return 0; /* wait */ + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return 0; if (n < 0 && errno == EINTR) continue; - break; /* gone */ + break; } return 1; } @@ -150,13 +127,12 @@ static void writer_start(int fd, const char *data, size_t len) { if (w->timer) wl_event_source_timer_update(w->timer, CLIP_WRITE_TIMEOUT_MS); } -/* ------------------------------------------------------------------ client → host reader */ struct reader { int fd; char *buf; size_t len; - const char *mime; /* points into the source's list */ + const char *mime; struct wl_event_source *src, *timer; }; static struct reader *g_reader; @@ -202,7 +178,7 @@ static int on_reader_readable(int fd, uint32_t mask, void *data) { r->buf = nb; ssize_t n = read(fd, r->buf + r->len, chunk); if (n > 0) { r->len += (size_t)n; continue; } - if (n == 0) { reader_finish(r); return 0; } /* EOF */ + if (n == 0) { reader_finish(r); return 0; } if (errno == EAGAIN || errno == EWOULDBLOCK) { if (mask & WL_EVENT_HANGUP) { reader_finish(r); return 0; } return 0; @@ -220,14 +196,13 @@ static int on_reader_timeout(void *data) { return 0; } -/* Ask the current client source for its text, once. */ static void read_client_text(void) { const char *mime; int p[2]; if (g_reader) reader_free(g_reader); if (!g_src || !(mime = source_text_mime(g_src))) return; if (pipe2(p, O_CLOEXEC) != 0) return; - fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL) | O_NONBLOCK); /* our end only; the client writes blocking */ + fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL) | O_NONBLOCK); struct reader *r = calloc(1, sizeof(*r)); if (!r) { close(p[0]); close(p[1]); return; } r->fd = p[0]; @@ -242,13 +217,12 @@ static void read_client_text(void) { wl_display_flush_clients(g_disp); } -/* ------------------------------------------------------------------ offers */ static void offer_res_destroy(struct wl_resource *r) { free(wl_resource_get_user_data(r)); } static void offer_receive(struct wl_client *c, struct wl_resource *r, const char *mime, int32_t fd) { struct offer *o = wl_resource_get_user_data(r); - if (!o || o->gen != g_gen) { close(fd); return; } /* stale offer */ + if (!o || o->gen != g_gen) { close(fd); return; } if (g_owner == OWNER_HOST) { if (is_text_mime(mime)) { droiddeck_log("clipboard", "Android clipboard → guest (%zu bytes) for %s", g_host_len, droiddeck_client_name(c)); @@ -257,7 +231,7 @@ static void offer_receive(struct wl_client *c, struct wl_resource *r, const char close(fd); } } else if (g_owner == OWNER_CLIENT && g_src) { - source_send_send(g_src, mime, fd); /* libwayland dups the fd */ + source_send_send(g_src, mime, fd); close(fd); wl_display_flush_clients(g_disp); } else { @@ -277,7 +251,6 @@ static const struct zwlr_data_control_offer_v1_interface control_offer_impl = { .receive = offer_receive, .destroy = offer_destroy, }; -/* Tell one device what the selection is now (a fresh offer, or nothing). */ static void device_send_selection(struct device *d) { struct wl_client *c = wl_resource_get_client(d->res); if (g_owner == OWNER_NONE) { @@ -315,8 +288,6 @@ static void device_send_selection(struct device *d) { else wl_data_device_send_selection(d->res, o->res); } -/* The selection changed: every data-control device hears it; wl_data_devices only if their - * client holds keyboard focus. */ static void broadcast_selection(void) { struct device *d; g_gen++; @@ -336,7 +307,6 @@ void droiddeck_clipboard_keyboard_focus(struct wl_client *client) { if (!d->is_control && wl_resource_get_client(d->res) == client) device_send_selection(d); } -/* ------------------------------------------------------------------ sources */ static void source_res_destroy(struct wl_resource *r) { struct source *s = wl_resource_get_user_data(r); @@ -380,7 +350,6 @@ static void create_source(struct wl_client *c, uint32_t id, int is_control, uint s, source_res_destroy); } -/* A device set (or cleared) the selection. */ static void set_selection(struct wl_client *c, struct wl_resource *source_res) { struct source *s = source_res ? wl_resource_get_user_data(source_res) : NULL; if (s && s == g_src) return; @@ -404,7 +373,6 @@ static void set_selection(struct wl_client *c, struct wl_resource *source_res) { read_client_text(); } -/* ------------------------------------------------------------------ devices */ static void device_res_destroy(struct wl_resource *r) { struct device *d = wl_resource_get_user_data(r); @@ -429,7 +397,6 @@ static void control_device_set_selection(struct wl_client *c, struct wl_resource set_selection(c, source); } static void control_device_set_primary(struct wl_client *c, struct wl_resource *r, struct wl_resource *source) { - /* No primary selection on Android; a source given here just gets cancelled. */ if (source) { struct source *s = wl_resource_get_user_data(source); if (s && s != g_src) source_send_cancelled(s); @@ -455,11 +422,9 @@ static void create_device(struct wl_client *c, uint32_t id, int is_control, uint : (const void *)&data_device_impl, d, device_res_destroy); wl_list_insert(&g_devices, &d->link); - /* Data-control devices see the selection at once; wl_data_devices when focus arrives. */ if (is_control || (g_kb_client && c == g_kb_client)) device_send_selection(d); } -/* ------------------------------------------------------------------ managers */ static void dm_create_source(struct wl_client *c, struct wl_resource *r, uint32_t id) { create_source(c, id, 0, wl_resource_get_version(r)); @@ -493,12 +458,10 @@ static void bind_data_control_manager(struct wl_client *c, void *data, uint32_t wl_resource_set_implementation(r, &cm_impl, NULL, NULL); } -/* ------------------------------------------------------------------ host side */ -/* Android's clipboard text (compositor thread, via the host queue). Takes the buffer. */ void clipboard_host_text(char *utf8, size_t len) { if (len && g_last_guest_text && len == g_last_guest_len && !memcmp(utf8, g_last_guest_text, len)) { - free(utf8); /* our own copy coming back: not a change */ + free(utf8); return; } if (len && g_owner == OWNER_HOST && g_host_len == len && !memcmp(utf8, g_host_text, len)) { diff --git a/app/src/main/cpp/waylandcomp/src/wl_color_mgmt.c b/app/src/main/cpp/waylandcomp/src/wl_color_mgmt.c index f18aab9f4..1ba254c4c 100644 --- a/app/src/main/cpp/waylandcomp/src/wl_color_mgmt.c +++ b/app/src/main/cpp/waylandcomp/src/wl_color_mgmt.c @@ -1,16 +1,3 @@ -/* - * wp_color_manager_v1 (wayland-protocols staging color-management-v1, version 1) - the subset Mesa's - * Wayland WSI binds - plus the HDR gate, the per-surface image descriptions and the session log's - * HDR evidence. See droiddeck_color.h for the design and HDR_RECON.md for why it is shaped this way. - * - * Strictness policy. A protocol error disconnects the client, and the client here is a GAME, so - * errors are only raised where the protocol leaves no choice and a conforming client can never hit - * them: a request gated on a feature we did not advertise, a property set twice, an incomplete - * parameter set, a description that is not ready, an intent we did not advertise. Everything a - * conforming client CAN produce on a bad day is accepted and logged instead: out-of-range HDR - * metadata is dropped (Android gets none rather than nonsense), a second colour-management object - * for the same surface replaces the first, a request on an inert object is ignored. - */ #define _GNU_SOURCE #include "droiddeck_color.h" #include "droiddeck_ext.h" @@ -29,7 +16,6 @@ extern volatile int g_zero_copy; #define TAG "color" -/* AHARDWAREBUFFER_FORMAT_* (android/hardware_buffer.h), for the log only. */ const char *droiddeck_ahb_format_name(uint32_t f) { static char other[32]; switch (f) { @@ -80,24 +66,24 @@ static const char *dataspace_name(int32_t ds) { } } -/* ---------------------------------------------------------------- request (app) + gate */ -static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; /* g_req, g_hdr (app threads + compositor) */ + +static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; static struct { - int mode; /* 0 off, 1 on, 2 forced */ + int mode; char source[64]; int dxvk_hdr, zero_copy_forced; int display_known; int display_id, hdr10, ratio_available, api; char display_name[96], formats[96]; float max_lum, max_avg, min_lum, ratio; - float highest_ratio; /* Display.getHighestHdrSdrRatio() (Android 16+), < 0 = not reported */ + float highest_ratio; } g_req = {.highest_ratio = -1.0f}; -static _Atomic int g_gate = -1; /* -1 undecided, 0 closed, 1 open */ -static char g_gate_why[320]; /* the closed gate's reason (for the summary) */ -static _Atomic int g_output_on = 1; /* the drawer's HDR output switch (per session, starts on) */ +static _Atomic int g_gate = -1; +static char g_gate_why[320]; +static _Atomic int g_output_on = 1; void droiddeck_color_set_request(int mode, const char *source, int dxvk_hdr, int zero_copy_forced) { pthread_mutex_lock(&g_mu); @@ -106,7 +92,7 @@ void droiddeck_color_set_request(int mode, const char *source, int dxvk_hdr, int g_req.dxvk_hdr = dxvk_hdr ? 1 : 0; g_req.zero_copy_forced = zero_copy_forced ? 1 : 0; pthread_mutex_unlock(&g_mu); - atomic_store(&g_output_on, 1); /* the drawer's switch is per session and starts on */ + atomic_store(&g_output_on, 1); } void droiddeck_color_set_display(int id, const char *name, const char *formats, int hdr10, float max_lum, @@ -123,8 +109,6 @@ void droiddeck_color_set_display(int id, const char *name, const char *formats, g_req.ratio = ratio; g_req.api = api; pthread_mutex_unlock(&g_mu); - /* The gate is decided once, when the compositor starts: a display that changes under a running - * session is recorded, and what it means is said out loud. */ if (was_known && atomic_load(&g_gate) >= 0 && (was_hdr10 != (hdr10 ? 1 : 0) || was_id != id)) { if (atomic_load(&g_gate) == 1 && !hdr10) droiddeck_log(TAG, "the game is now on \"%s\" (display %d), which reports no HDR10: HDR frames stay tagged " @@ -139,48 +123,39 @@ void droiddeck_color_set_display(int id, const char *name, const char *formats, int droiddeck_color_gate_state(void) { return atomic_load(&g_gate); } int droiddeck_color_hdr_open(void) { return atomic_load(&g_gate) == 1; } -/* ---------------------------------------------------------------- HDR evidence (summary) */ static struct { - unsigned descs; /* HDR image descriptions made ready */ - unsigned applied; /* commits that made one current on a surface */ - char applied_who[160]; /* the last surface that got one */ - uint64_t layer_frames, layer_10bit, layer_copy8, copy_frames; /* layer_frames = every frame shown AS HDR */ + unsigned descs; + unsigned applied; + char applied_who[160]; + uint64_t layer_frames, layer_10bit, layer_copy8, copy_frames; uint64_t composed, swapchain, tonemapped; - uint64_t tm_off; /* of `tonemapped`: while the drawer's HDR output switch was off */ - unsigned output_offs; /* times the switch was turned off */ + uint64_t tm_off; + unsigned output_offs; unsigned win_layer, win_copy, win_copy8, win_zc, win_composed, win_swapchain, win_tonemapped; char copy_reason[160]; char layer_fmt[48]; int ratio_n, win_ratio_n, ratio_live_n; float ratio_min, ratio_max, ratio_last, win_ratio_min, win_ratio_max, ratio_logged; - float ratio_live_max; /* highest reading taken WHILE HDR frames were on screen (the verdict's) */ + float ratio_live_max; int64_t ratio_logged_ns, ratio_periodic_ns; - /* Live headroom. The display may grant HDR frames no headroom at all while they are on screen - seen - * on the Fold while the screen was being RECORDED (Android turns HDR headroom off for a recording, - * which is SDR), and plausibly with the brightness slider at maximum (SDR white already at the panel's - * limit) - so "the ratio rose once" is not enough for a reader at minute 3: time with HDR frames on - * screen, the part of it with the ratio above 1, and the current no-headroom streak. */ int64_t prev_sample_ns; int prev_live; float prev_ratio; int64_t live_ns, headroom_ns; - int64_t nohead_since_ns; /* HDR frames on screen and ratio <= 1.01 since then (0 = not now) */ - int nohead_said; /* the streak's "no headroom" line is written */ + int64_t nohead_since_ns; + int nohead_said; char verdict[512]; } g_hdr; static _Atomic int64_t g_last_frame_ns; -static _Atomic int64_t g_last_tm_ns; /* the last HDR frame shown tone-mapped to SDR */ +static _Atomic int64_t g_last_tm_ns; -/* What the device says beside the headroom (the app, non-root APIs; droiddeck_color_env_*). The Fold showed - * three ways to lose HDR headroom with HDR frames on screen - a screen recording, heat under load, and - * (plausibly) the brightness slider at maximum - so every no-headroom line carries this. Under g_mu. */ static struct { int known; - int thermal; /* PowerManager THERMAL_STATUS_* (0 none .. 6 shutdown), -1 unknown */ - float headroom; /* PowerManager.getThermalHeadroom(10), < 0 = not available */ - int brightness; /* Settings.System.SCREEN_BRIGHTNESS (0..255), -1 unknown */ - int bmode; /* SCREEN_BRIGHTNESS_MODE: 1 automatic, 0 manual, -1 unknown */ - float requested; /* the last HDR headroom asked for (layer or screen surface): > 0 the ratio, - * 0 none asked, -1 the API is missing on this Android (< 15) */ + int thermal; + float headroom; + int brightness; + int bmode; + float requested; + } g_env = {0, -1, -1.0f, -1, -1, 0.0f}; static const char *thermal_name(int t) { @@ -190,7 +165,6 @@ static const char *thermal_name(int t) { } } -/* "thermal MODERATE (headroom 0.83), brightness 180/255 manual". Caller holds g_mu. */ static void env_text_locked(char *out, size_t n) { char t[48], b[40]; if (g_env.thermal < 0) snprintf(t, sizeof(t), "thermal ?"); @@ -203,9 +177,6 @@ static void env_text_locked(char *out, size_t n) { snprintf(out, n, "%s, %s", t, b); } -/* What the evidence says about headroom lost with HDR frames on screen. A screenshot or a screen recording - * is not detected in this build (that needs extra permissions), so it is named as a possibility when - * nothing the app can measure explains the loss. Caller holds g_mu. */ static void nohead_causes_locked(char *out, size_t n, int brief) { const char *parts[2]; char hot[48]; @@ -214,7 +185,6 @@ static void nohead_causes_locked(char *out, size_t n, int brief) { snprintf(hot, sizeof(hot), "the device is hot (thermal %s)", thermal_name(g_env.thermal)); parts[np++] = hot; } - /* Maximum brightness only counts when it is MANUAL: the Fold kept 3.00 of headroom at 255 auto. */ if (g_env.brightness >= 250 && g_env.bmode == 0) parts[np++] = "brightness at maximum (manual)"; if (np == 1) snprintf(out, n, "likely %s", parts[0]); else if (np == 2) snprintf(out, n, "likely %s and %s", parts[0], parts[1]); @@ -235,11 +205,11 @@ static void nohead_causes_locked(char *out, size_t n, int brief) { snprintf(out, n, "a screenshot or screen recording, heat, or manual brightness at maximum"); } -/* ---- the explicit HDR headroom request (API 35; sc_layer.c for the game layer, the app for the screen - * surface): content peak / SDR white, capped at the display's highest ratio when it reports one. */ -static _Atomic int g_screen_hr_x1000; /* the screen surface's wanted headroom x1000 (HDR10 swapchain) */ -static _Atomic int64_t g_screen_hr_ns; /* the last HDR10-swapchain frame */ -static char g_screen_hr_why[200]; /* under g_mu */ + + +static _Atomic int g_screen_hr_x1000; +static _Atomic int64_t g_screen_hr_ns; +static char g_screen_hr_why[200]; static float peak_of(const struct droiddeck_color *c, float disp_max, const char **src) { if (c->max_cll > 0.0f) { *src = "max CLL"; return c->max_cll; } @@ -308,15 +278,12 @@ int droiddeck_color_last_frame_age_ms(void) { return age > 0x7fffffff ? 0x7fffffff : (int)age; } -/* The one-line answer a tester quotes. Caller holds g_mu. */ static void verdict_locked(char *out, size_t size) { int gate = atomic_load(&g_gate); if (gate < 0) { snprintf(out, size, "not decided yet (the compositor has not started)"); return; } if (gate == 0) { snprintf(out, size, "no, because %s", g_gate_why); return; } if (g_hdr.layer_frames) { const char *who = g_hdr.applied_who[0] ? g_hdr.applied_who : "the game"; - /* Only the paths that carried frames, so the ratio, the share and the device evidence still fit - * the log line. */ char paths[200] = ""; size_t pp = 0; const uint64_t zc_other = g_hdr.layer_frames - g_hdr.layer_10bit - g_hdr.layer_copy8 - g_hdr.composed - @@ -336,12 +303,7 @@ static void verdict_locked(char *out, size_t size) { (unsigned long long)g_hdr.layer_frames, who, paths); char env[128] = "", causes[256] = ""; if (g_env.known) env_text_locked(env, sizeof(env)); - /* Brief: the verdict has to fit one log line with everything else in it. */ if (g_hdr.nohead_said) nohead_causes_locked(causes, sizeof(causes), 1); - /* Only readings taken while HDR frames were on screen count: the ratio says what the display did - * with THEM, not with whatever else was up at another moment. And the share of that time with any - * headroom, plus where it stands now: a ratio that rose once and then fell to 1.00 for minutes is - * not the same answer. */ char share[384] = ""; const long long live_s = (long long)(g_hdr.live_ns / 1000000000LL); if (g_hdr.live_ns >= 1000000000LL) @@ -351,7 +313,6 @@ static void verdict_locked(char *out, size_t size) { (long long)(g_hdr.headroom_ns / 1000000000LL), live_s, g_hdr.ratio_last, g_hdr.nohead_said ? " - no headroom now: " : "", g_hdr.nohead_said ? causes : "", env[0] ? " [" : "", env, env[0] ? "]" : ""); - /* The display's own ceiling (Android 16+) and what was asked for, where known. */ char ceil[80] = "", req[48] = ""; if (g_req.highest_ratio > 0.0f) snprintf(ceil, sizeof(ceil), ", highest possible %.2f", g_req.highest_ratio); if (g_env.requested > 0.0f) snprintf(req, sizeof(req), ", %.1fx requested", g_env.requested); @@ -400,7 +361,6 @@ static void verdict_locked(char *out, size_t size) { "image description (is DXVK_HDR=1 set, and is HDR switched on in the game's own display options?)"); } -/* Log the verdict when it changed (force = log it even if not). */ static void log_verdict(int force) { char v[512]; pthread_mutex_lock(&g_mu); @@ -435,7 +395,7 @@ void droiddeck_color_frame_shown(const struct droiddeck_color *c, int path, uint snprintf(g_hdr.layer_fmt, sizeof(g_hdr.layer_fmt), "HDR10 swapchain"); atomic_store(&g_screen_hr_ns, now_ns()); break; - default: /* DROIDDECK_HDR_TONEMAPPED: shown, but not as HDR */ + default: g_hdr.tonemapped++; g_hdr.win_tonemapped++; if (!atomic_load(&g_output_on)) g_hdr.tm_off++; pthread_mutex_unlock(&g_mu); @@ -447,8 +407,6 @@ void droiddeck_color_frame_shown(const struct droiddeck_color *c, int path, uint pthread_mutex_unlock(&g_mu); atomic_store(&g_last_frame_ns, now_ns()); if (path == DROIDDECK_HDR_SWAPCHAIN) { - /* The screen surface's headroom request (the app applies it): worked out again when the - * description changes, else once a second. */ static uint32_t ident; static int64_t calc_ns; const int64_t t = now_ns(); @@ -494,7 +452,6 @@ void droiddeck_color_ratio_sample(float ratio, int listener) { pthread_mutex_lock(&g_mu); was = g_hdr.ratio_logged; if (ratio > 0.0f) { - /* Time with HDR frames on screen, credited to the reading that stood over it. */ if (g_hdr.prev_sample_ns && g_hdr.prev_live) { int64_t dt = t - g_hdr.prev_sample_ns; if (dt > 0 && dt < 3000000000LL) { @@ -520,8 +477,6 @@ void droiddeck_color_ratio_sample(float ratio, int listener) { if (!g_hdr.ratio_live_n || ratio > g_hdr.ratio_live_max) g_hdr.ratio_live_max = ratio; g_hdr.ratio_live_n++; } - /* A change is logged at once (at most 4 lines a second); while HDR frames are on screen the - * steady value is restated every 5 s, so a tester's log shows it holding, not just moving. */ float moved = ratio - g_hdr.ratio_logged; if (moved < 0.0f) moved = -moved; if ((g_hdr.ratio_n == 1 || moved >= 0.02f) && t - g_hdr.ratio_logged_ns > 250000000LL) @@ -562,7 +517,7 @@ void droiddeck_color_stats_tick(void) { char line[512]; int any; pthread_mutex_lock(&g_mu); - any = g_hdr.win_layer || g_hdr.win_copy || g_hdr.win_tonemapped; /* the ratio alone is logged when it moves */ + any = g_hdr.win_layer || g_hdr.win_copy || g_hdr.win_tonemapped; if (any) { char ratio[256] = "no HDR/SDR ratio reading", env[128] = "", causes[256] = ""; if (g_env.known) env_text_locked(env, sizeof(env)); @@ -591,7 +546,7 @@ int droiddeck_color_hdr_state(void) { int age = droiddeck_color_last_frame_age_ms(); if (age < 0 || age >= 1500) return 0; pthread_mutex_lock(&g_mu); - int confirmed = !g_hdr.ratio_n || g_hdr.ratio_last > 1.01f; /* no ratio on this display: the tag is all there is */ + int confirmed = !g_hdr.ratio_n || g_hdr.ratio_last > 1.01f; int nohead = g_hdr.nohead_said; pthread_mutex_unlock(&g_mu); return confirmed ? 1 : nohead ? 2 : 0; @@ -617,7 +572,7 @@ int droiddeck_color_requested(void) { int droiddeck_color_output(void) { return atomic_load(&g_output_on); } void droiddeck_color_env_sample(int thermal, float headroom, int brightness, int bmode) { - if (atomic_load(&g_gate) != 1) return; /* evidence for the HDR lines only */ + if (atomic_load(&g_gate) != 1) return; char msg[2][200]; int nmsg = 0; pthread_mutex_lock(&g_mu); @@ -656,7 +611,6 @@ int droiddeck_color_tonemapped_on_screen(void) { void droiddeck_color_set_output(int on) { on = on ? 1 : 0; if (atomic_load(&g_gate) != 1) { - /* The drawer only shows the switch in sessions whose gate is open; say so if it ever gets here. */ droiddeck_log(TAG, "HDR output switch (%s) ignored: HDR is not open in this session", on ? "on" : "off"); return; } @@ -674,7 +628,7 @@ void droiddeck_color_set_output(int on) { droiddeck_log(TAG, "HDR output switched OFF in the drawer: HDR frames are tone-mapped to SDR from the next frame " "(%s; %s). The game is not told - it keeps rendering HDR; its own HDR setting and DXVK_HDR are " "untouched", who, age >= 0 && age < 1500 ? "HDR frames were on screen" : "no HDR frames on screen just now"); - droiddeck_request_redraw(); /* a paused game commits nothing: show the change now */ + droiddeck_request_redraw(); } static _Atomic int g_session_ended; @@ -685,19 +639,16 @@ void droiddeck_color_session_end(void) { pthread_mutex_lock(&g_mu); int asked = g_req.mode != 0; pthread_mutex_unlock(&g_mu); - if (asked) log_verdict(1); /* nobody asked for HDR: no summary to write */ + if (asked) log_verdict(1); } -/* ---------------------------------------------------------------- image descriptions */ struct cm_desc { - int refs; /* protocol objects + surfaces (pending / current) */ + int refs; struct droiddeck_color c; - /* Set on the output / preferred descriptions the compositor serves (never on one a game built - * from parameters): get_information is answered rather than refused, with these as the target. */ int informative; - float ref_lum; /* nits: PQ reference white */ - float disp_min_lum, disp_max_lum; /* nits: the display's own volume, from the app */ + float ref_lum; + float disp_min_lum, disp_max_lum; }; static uint32_t g_next_identity = 1; @@ -705,11 +656,10 @@ static uint32_t g_next_identity = 1; static struct cm_desc *desc_ref(struct cm_desc *d) { if (d) d->refs++; return d; } static void desc_unref(struct cm_desc *d) { if (d && --d->refs <= 0) free(d); } -/* The parametric creator's collected properties. 0 / has_* = 0 means unset. */ struct cm_params { uint32_t tf, primaries; int has_mprim; int32_t mprim[8]; - int has_mlum; uint32_t mlum_min, mlum_max; /* min is cd/m² * 10000, max is cd/m² */ + int has_mlum; uint32_t mlum_min, mlum_max; int has_cll; uint32_t cll; int has_fall; uint32_t fall; }; @@ -718,7 +668,6 @@ static void image_description_destroy_req(struct wl_client *c, struct wl_resourc static void image_description_get_information(struct wl_client *c, struct wl_resource *r, uint32_t id) { struct cm_desc *d = wl_resource_get_user_data(r); if (!d || !d->informative) { - /* Built by the game from parameters: the protocol says it knows what it asked for. */ wl_resource_post_error(r, WP_IMAGE_DESCRIPTION_V1_ERROR_NO_INFORMATION, "get_information is not allowed on this image description"); return; @@ -726,7 +675,7 @@ static void image_description_get_information(struct wl_client *c, struct wl_res struct wl_resource *info = wl_resource_create(c, &wp_image_description_info_v1_interface, wl_resource_get_version(r), id); if (!info) { wl_client_post_no_memory(c); return; } - wl_resource_set_implementation(info, NULL, NULL, NULL); /* events only */ + wl_resource_set_implementation(info, NULL, NULL, NULL); const struct droiddeck_color *col = &d->c; int32_t xy[8] = { (int32_t)(col->red[0] * 1e6f + 0.5f), (int32_t)(col->red[1] * 1e6f + 0.5f), (int32_t)(col->green[0] * 1e6f + 0.5f), (int32_t)(col->green[1] * 1e6f + 0.5f), @@ -735,13 +684,11 @@ static void image_description_get_information(struct wl_client *c, struct wl_res wp_image_description_info_v1_send_primaries(info, xy[0], xy[1], xy[2], xy[3], xy[4], xy[5], xy[6], xy[7]); wp_image_description_info_v1_send_primaries_named(info, col->primaries); wp_image_description_info_v1_send_tf_named(info, col->tf); - /* min is cd/m² * 10000, max and reference are cd/m² */ wp_image_description_info_v1_send_luminances(info, (uint32_t)(col->min_lum * 10000.0f + 0.5f), (uint32_t)(col->max_lum + 0.5f), (uint32_t)(d->ref_lum + 0.5f)); wp_image_description_info_v1_send_target_primaries(info, xy[0], xy[1], xy[2], xy[3], xy[4], xy[5], xy[6], xy[7]); wp_image_description_info_v1_send_target_luminance(info, (uint32_t)(d->disp_min_lum * 10000.0f + 0.5f), (uint32_t)(d->disp_max_lum + 0.5f)); - /* done is a destructor event: the client's proxy is gone once it arrives, so the resource goes too. */ wp_image_description_info_v1_send_done(info); wl_resource_destroy(info); } @@ -753,15 +700,6 @@ static void image_description_resource_destroy(struct wl_resource *r) { desc_unref(wl_resource_get_user_data(r)); } -/* What the display layer shows while the gate is open - BT.2020 + PQ over the display's own - * volume - served as the output's image description and as every surface's preferred one. It is - * ready at once and open to get_information, and that is not optional: gamescope asks for the - * preferred description and for its information in the same breath, before any roundtrip - * (WaylandBackend.cpp, UpdateWPPreferredColorManagement). Answering 'failed' and then refusing - * get_information with a protocol error severs its connection mid-roundtrip - the Fold 8 on r5: - * "Broken pipe", signal 6 - while a device with the gate closed never offers colour management - * and never gets here. gamescope reads only the luminances from the answer (reference white vs - * the display's peak) to decide whether it exposes HDR to what it hosts. */ static void make_output_description(struct wl_client *c, struct wl_resource *parent, uint32_t id, const char *what) { struct cm_desc *d = calloc(1, sizeof(*d)); if (!d) { wl_client_post_no_memory(c); return; } @@ -774,9 +712,9 @@ static void make_output_description(struct wl_client *c, struct wl_resource *par col->has_st2086 = 1; col->red[0] = 0.708f; col->red[1] = 0.292f; col->green[0] = 0.170f; col->green[1] = 0.797f; col->blue[0] = 0.131f; col->blue[1] = 0.046f; col->white[0] = 0.3127f; col->white[1] = 0.3290f; - col->min_lum = 0.005f; col->max_lum = 10000.0f; /* the PQ volume */ + col->min_lum = 0.005f; col->max_lum = 10000.0f; d->informative = 1; - d->ref_lum = 203.0f; /* PQ reference white */ + d->ref_lum = 203.0f; pthread_mutex_lock(&g_mu); d->disp_min_lum = g_req.min_lum > 0.0f ? g_req.min_lum : 0.0f; d->disp_max_lum = g_req.max_lum > 0.0f ? g_req.max_lum : 1000.0f; @@ -811,8 +749,6 @@ static void params_create(struct wl_client *client, struct wl_resource *r, uint3 else if (p->primaries == WP_COLOR_MANAGER_V1_PRIMARIES_BT2020 && p->tf == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG) c->dataspace = DROIDDECK_ADATASPACE_BT2020_HLG; - /* HDR metadata, kept only when it makes sense. The PQ defaults stand in for what was not given - * (0.005 - 10000 cd/m², the target volume = the primary one); CTA-861.3 treats 0 as unknown. */ char dropped[160] = ""; float mlum_min = 0.005f, mlum_max = 10000.0f; if (p->has_mlum) { @@ -827,7 +763,7 @@ static void params_create(struct wl_client *client, struct wl_resource *r, uint3 c->green[0] = p->mprim[2] / 1e6f; c->green[1] = p->mprim[3] / 1e6f; c->blue[0] = p->mprim[4] / 1e6f; c->blue[1] = p->mprim[5] / 1e6f; c->white[0] = p->mprim[6] / 1e6f; c->white[1] = p->mprim[7] / 1e6f; - } else { /* the container's own primaries (BT.2020, D65) */ + } else { c->red[0] = 0.708f; c->red[1] = 0.292f; c->green[0] = 0.170f; c->green[1] = 0.797f; c->blue[0] = 0.131f; c->blue[1] = 0.046f; c->white[0] = 0.3127f; c->white[1] = 0.3290f; } @@ -862,8 +798,6 @@ static void params_create(struct wl_client *client, struct wl_resource *r, uint3 if (!out) { free(d); wl_client_post_no_memory(client); return; } d->refs = 1; wl_resource_set_implementation(out, &image_description_impl, d, image_description_resource_destroy); - /* Sent from inside the request: Mesa blocks its swapchain on this event (it dispatches its queue - * until ready/failed arrives), and the event loop flushes it before it sleeps again. */ wp_image_description_v1_send_ready(out, c->identity); if (c->dataspace) { pthread_mutex_lock(&g_mu); @@ -878,7 +812,7 @@ static void params_create(struct wl_client *client, struct wl_resource *r, uint3 droiddeck_log(TAG, "image description #%u: dropped HDR metadata that breaks the protocol's rules:%s " "(Android gets the rest)", c->identity, dropped); } - wl_resource_destroy(r); /* create is the creator's destructor */ + wl_resource_destroy(r); } #define PARAMS_ONCE(field, what) \ @@ -958,14 +892,11 @@ static const struct wp_image_description_creator_params_v1_interface params_impl }; static void params_resource_destroy(struct wl_resource *r) { free(wl_resource_get_user_data(r)); } -/* ---------------------------------------------------------------- per-surface state */ -/* One per wl_surface that ever had a colour-management object. Lives until the wl_surface goes (a - * swapchain rebuild destroys the object and makes a new one for the same surface). */ struct cm_surf { - struct wl_resource *surface; /* the wl_surface */ + struct wl_resource *surface; struct wl_listener surface_destroy; - struct wl_resource *owner; /* the live wp_color_management_surface_v1, NULL = none */ + struct wl_resource *owner; struct cm_desc *pending, *current; int pending_dirty; }; @@ -989,7 +920,7 @@ static void describe_surface(struct wl_resource *surface, char *out, size_t size static void cm_surface_destroyed(struct wl_listener *l, void *data) { struct cm_surf *cs = wl_container_of(l, cs, surface_destroy); wl_list_remove(&cs->surface_destroy.link); - if (cs->owner) wl_resource_set_user_data(cs->owner, NULL); /* inert from now on */ + if (cs->owner) wl_resource_set_user_data(cs->owner, NULL); desc_unref(cs->pending); desc_unref(cs->current); free(cs); @@ -1035,7 +966,6 @@ static const struct wp_color_management_surface_v1_interface cm_surface_impl = { .unset_image_description = cm_surface_unset_image_description, }; -/* Destroying the object does what unset_image_description does (double-buffered, like it). */ static void cm_surface_resource_destroy(struct wl_resource *r) { struct cm_surf *cs = wl_resource_get_user_data(r); if (!cs || cs->owner != r) return; @@ -1079,7 +1009,6 @@ const struct droiddeck_color *droiddeck_color_of(struct wl_resource *surface) { return cs && cs->current ? &cs->current->c : NULL; } -/* ---------------------------------------------------------------- output + feedback */ static void cm_output_destroy_req(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static void cm_output_get_image_description(struct wl_client *c, struct wl_resource *r, uint32_t id) { @@ -1100,7 +1029,6 @@ static const struct wp_color_management_surface_feedback_v1_interface cm_feedbac .get_preferred_parametric = cm_feedback_get_preferred, }; -/* ---------------------------------------------------------------- wp_color_manager_v1 */ static void cm_destroy_req(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } @@ -1124,7 +1052,6 @@ static void cm_get_surface(struct wl_client *c, struct wl_resource *r, uint32_t char who[160]; describe_surface(surface, who, sizeof(who)); if (cs->owner) { - /* The protocol says surface_exists; a game would be disconnected for it. The newer object wins. */ droiddeck_log(TAG, "%s: a second colour-management object for the same surface - the newer one takes over " "(protocol error surface_exists not raised)", who); wl_resource_set_user_data(cs->owner, NULL); @@ -1174,16 +1101,12 @@ static void bind_cm(struct wl_client *c, void *data, uint32_t ver, uint32_t id) struct wl_resource *r = wl_resource_create(c, &wp_color_manager_v1_interface, ver > 1 ? 1 : (int)ver, id); if (!r) { wl_client_post_no_memory(c); return; } wl_resource_set_implementation(r, &cm_impl, NULL, NULL); - /* All at bind, then done: Mesa reads them in the registry roundtrip that follows the bind and - * lists VK_COLOR_SPACE_HDR10_ST2084_EXT from exactly this pair (bt2020 + st2084_pq). Only what - * the display layer can honour is offered. */ wp_color_manager_v1_send_supported_intent(r, WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); wp_color_manager_v1_send_supported_feature(r, WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC); wp_color_manager_v1_send_supported_feature(r, WP_COLOR_MANAGER_V1_FEATURE_SET_MASTERING_DISPLAY_PRIMARIES); wp_color_manager_v1_send_supported_primaries_named(r, WP_COLOR_MANAGER_V1_PRIMARIES_BT2020); wp_color_manager_v1_send_supported_tf_named(r, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ); wp_color_manager_v1_send_done(r); - /* One line per program: Mesa binds again for every surface-format query. */ if (last_named != c) { last_named = c; droiddeck_log(TAG, "%s bound wp_color_manager_v1 version %u (offered: perceptual intent; parametric descriptions " @@ -1194,7 +1117,6 @@ static void bind_cm(struct wl_client *c, void *data, uint32_t ver, uint32_t id) void droiddeck_color_client_gone(struct wl_client *client) { if (atomic_load(&g_gate) != 1 || atomic_load(&g_session_ended)) return; - /* A program that presented HDR has left: say where the session stands now, while it is fresh. */ const char *name = droiddeck_client_name(client); int mine; pthread_mutex_lock(&g_mu); @@ -1203,7 +1125,6 @@ void droiddeck_color_client_gone(struct wl_client *client) { if (mine) log_verdict(1); } -/* ---------------------------------------------------------------- the gate */ void droiddeck_color_init(struct wl_display *display) { char why[320] = ""; @@ -1227,7 +1148,6 @@ void droiddeck_color_init(struct wl_display *display) { else snprintf(disp, sizeof(disp), "the app could not read the display's HDR capability"); - /* What asked for HDR, in the words the user knows it by: the editors' setting, or the env override. */ char asked[128]; if (mode == 2) snprintf(asked, sizeof(asked), "DROIDDECK_WAYLAND_HDR=force (%s)", source); else if (strstr(source, "env")) snprintf(asked, sizeof(asked), "DROIDDECK_WAYLAND_HDR=1 (%s)", source); @@ -1262,8 +1182,6 @@ void droiddeck_color_init(struct wl_display *display) { "(games that check fall back to SDR; some show washed-out colours) - remove it on this display"); log_verdict(1); } else if (dxvk_hdr || (known && hdr10)) { - /* Off, and silent unless it is worth a line: a display that could show HDR10, or a DXVK - * switch that promises games an HDR display they cannot get. */ droiddeck_log(TAG, "HDR output off for this session (%s)%s%s", off_why, (known && hdr10) ? " - this display lists HDR10, so switching HDR output on for this game would " "offer it to games" : "", diff --git a/app/src/main/cpp/waylandcomp/src/wl_dmabuf.c b/app/src/main/cpp/waylandcomp/src/wl_dmabuf.c index 2f8552f1e..cc85bd32e 100644 --- a/app/src/main/cpp/waylandcomp/src/wl_dmabuf.c +++ b/app/src/main/cpp/waylandcomp/src/wl_dmabuf.c @@ -1,8 +1,3 @@ -/* - * zwp_linux_dmabuf_v1: the buffer params a client builds a dma-buf wl_buffer from, the format and - * modifier table the renderer can import, and dma-buf feedback (version 4). Split out of - * compositor.c; the buffer records themselves are in compositor_internal.h. - */ #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -20,7 +15,6 @@ #include "droiddeck_color.h" #include "compositor_internal.h" -/* ------------------------------------------------------------ zwp_linux_dmabuf_v1 */ static void dbuf_buffer_resource_destroy(struct wl_resource *r) { dmabuf_buffer_unref(wl_resource_get_user_data(r)); @@ -44,7 +38,7 @@ static struct wl_resource *params_do_create(struct wl_client *c, struct wl_resou struct dmabuf_buffer *b = calloc(1, sizeof(*b)); if (!b) return NULL; b->n_planes = p->n_planes; - b->refs = 1; /* the wl_buffer resource's */ + b->refs = 1; b->width = w; b->height = h; b->format = format; b->modifier = p->modifier[0]; for (int i = 0; i < MAX_PLANES; i++) b->fd[i] = -1; @@ -52,7 +46,7 @@ static struct wl_resource *params_do_create(struct wl_client *c, struct wl_resou b->fd[i] = p->fd[i]; b->offset[i] = p->offset[i]; b->stride[i] = p->stride[i]; - p->fd[i] = -1; /* ownership moves to the buffer */ + p->fd[i] = -1; } struct wl_resource *buf = wl_resource_create(c, &wl_buffer_interface, 1, id); if (!buf) { @@ -109,14 +103,6 @@ static const struct zwp_linux_dmabuf_v1_interface dmabuf_impl = { .get_default_feedback = dmabuf_get_default_feedback, .get_surface_feedback = dmabuf_get_surface_feedback, }; -/* The advertised format/modifier table, built at the first bind from what the renderer's driver - * can import (vkp_dmabuf_modifiers). INVALID is always offered too (Mesa drops it; other clients - * may use it for the implicit path). Without a renderer the list is LINEAR + INVALID, as before. */ -/* The last two rows are HDR10's (droiddeck_color.h): A2B10G10R10 as AB30 (alpha) + XB30 (opaque) - Mesa - * lists a VkFormat only when both are advertised, and it is the one 10-bit layout our zero-copy WSI - * can put in a gralloc buffer (AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM). They are advertised ONLY - * while the HDR gate is open, and after the 8-bit rows, so every other session - and every client that - * takes the first format it sees - gets exactly the table it always had. */ #define DMABUF_NFMT_MAX 6 #define DMABUF_NFMT_SDR 4 #define DMABUF_NMOD 4 @@ -124,7 +110,7 @@ static const struct zwp_linux_dmabuf_v1_interface dmabuf_impl = { #define DRM_XBGR2101010 FOURCC('X', 'B', '3', '0') static struct { uint32_t fmt; uint64_t mods[DMABUF_NMOD]; int n; } g_dmabuf_fmts[DMABUF_NFMT_MAX] = { {DRM_ARGB8888}, {DRM_XRGB8888}, {DRM_ABGR8888}, {DRM_XBGR8888}, {DRM_ABGR2101010}, {DRM_XBGR2101010}}; -static int g_dmabuf_nfmt = DMABUF_NFMT_SDR; /* rows advertised: the SDR four, + 2 with the HDR gate open */ +static int g_dmabuf_nfmt = DMABUF_NFMT_SDR; static int g_dmabuf_fmts_ready; static void dmabuf_build_feedback(void); @@ -136,8 +122,6 @@ static void dmabuf_build_formats(void) { for (int f = 0; f < g_dmabuf_nfmt; f++) { uint64_t got[DMABUF_NMOD]; int n = vkp_dmabuf_modifiers(g_dmabuf_fmts[f].fmt, got, DMABUF_NMOD), k = 0; - /* LINEAR first: it is the layout every client and the shm fallback agree on, and the one - * we advertised before this table existed. */ g_dmabuf_fmts[f].mods[k++] = MOD_LINEAR; for (int i = 0; i < n && k < DMABUF_NMOD - 1; i++) { if (got[i] == MOD_LINEAR) continue; @@ -161,8 +145,6 @@ static void dmabuf_build_formats(void) { droiddeck_log("dmabuf", "the compositor's driver (%s) reports no importable qcom_compressed layout: " "game swapchains stay linear", vkp_gpu_name()); if (g_dmabuf_nfmt > DMABUF_NFMT_SDR) { - /* The rows above always carry LINEAR; say what the compositor's own driver can really import - * for 10-bit, since a gralloc buffer it cannot import is shown on the display layer only. */ uint64_t got[DMABUF_NMOD]; int n = vkp_dmabuf_modifiers(DRM_XBGR2101010, got, DMABUF_NMOD), ubwc10 = 0; for (int i = 0; i < n; i++) if (got[i] == VKP_MOD_QCOM_COMPRESSED) ubwc10 = 1; @@ -174,19 +156,6 @@ static void dmabuf_build_formats(void) { dmabuf_build_feedback(); } -/* ---- dmabuf feedback (zwp_linux_dmabuf_v1 version 4) - * - * Turnip's Vulkan WSI is happy with the version 3 format/modifier events, so every Vulkan game - * worked while we only advertised 3. Mesa's EGL is not: its Wayland platform only takes the GPU - * (kopper/Zink) path when it can bind this interface with FEEDBACK, and with 3 it silently drops - * to its wl_shm software path. On this Proton layer that path has no software rasteriser to fall - * back to (the gallium build is zink+kopper+swrast, no llvmpipe), so the shm buffer it commits is - * never written: a native OpenGL window came out solid black, ~30 shm commits/s and no GPU frame - * at all (Wizardry: The Labyrinth of Lost Souls). Feedback is what makes that path work. - * - * What a client needs from us is one tranche describing "everything the compositor can import": - * a format table it mmaps read-only, the device to allocate on, and the indices it may use. - * Clients binding versions 1-3 keep getting the old format/modifier events instead. */ #ifndef MFD_CLOEXEC #define MFD_CLOEXEC 0x0001U @@ -202,17 +171,13 @@ static void dmabuf_build_formats(void) { #define F_SEAL_WRITE 0x0008 #endif -struct dmabuf_fmt_entry { uint32_t format; uint32_t pad; uint64_t modifier; }; /* the wire layout */ +struct dmabuf_fmt_entry { uint32_t format; uint32_t pad; uint64_t modifier; }; -static int g_fmt_table_fd = -1; /* sealed read-only memfd of dmabuf_fmt_entry[] */ +static int g_fmt_table_fd = -1; static size_t g_fmt_table_size; -static uint16_t g_fmt_table_n; /* entries, == the indices a tranche may name */ -dev_t g_main_device; /* the render node clients should allocate on */ +static uint16_t g_fmt_table_n; +dev_t g_main_device; -/* The GPU we import through is reached with KGSL, not DRM, so there is no render node of our own - * to name. Clients only use main_device to match "the same device as the compositor" and to pick - * a driver; the one DRM render node this platform has is the right answer, and Zink ignores it - * anyway (it renders on the Vulkan device it already has). 0 if the platform has none. */ static dev_t dmabuf_render_node(void) { static const char *nodes[] = {"/dev/dri/renderD128", "/dev/dri/renderD129", "/dev/dri/card0"}; struct stat st; @@ -229,7 +194,7 @@ static void dmabuf_build_feedback(void) { g_main_device = dmabuf_render_node(); for (int f = 0; f < g_dmabuf_nfmt; f++) for (int m = 0; m < g_dmabuf_fmts[f].n; m++) { - if (g_dmabuf_fmts[f].mods[m] == MOD_INVALID) continue; /* never offer INVALID here */ + if (g_dmabuf_fmts[f].mods[m] == MOD_INVALID) continue; entries[n].format = g_dmabuf_fmts[f].fmt; entries[n].pad = 0; entries[n].modifier = g_dmabuf_fmts[f].mods[m]; @@ -246,15 +211,12 @@ static void dmabuf_build_feedback(void) { close(fd); return; } - /* The client mmaps this read-only and trusts it not to change under it. */ fcntl(fd, F_ADD_SEALS, F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE); g_fmt_table_fd = fd; g_fmt_table_size = size; g_fmt_table_n = (uint16_t)n; droiddeck_log("dmabuf", "feedback ready: %d format/modifier pairs, main device %u:%u", n, (unsigned)major(g_main_device), (unsigned)minor(g_main_device)); - /* Vulkan games never need the node. Mesa's EGL did until Wayland layer versionCode 9: without - * one it fell back to a software path that draws nothing here (black window, sound plays). */ if (!g_main_device) droiddeck_log("dmabuf", "%s: OpenGL games need Wayland layer versionCode 9 or newer, " "which runs OpenGL on the GPU without a DRM node; older layers show a black window", @@ -262,7 +224,6 @@ static void dmabuf_build_feedback(void) { : "this device gives apps no display (DRM) device (/dev/dri)"); } -/* One tranche: our device, every pair in the table, no scanout flag. */ static void dmabuf_feedback_send(struct wl_resource *fb) { struct wl_array dev, idx; uint16_t *ind; @@ -309,8 +270,6 @@ static void dmabuf_get_default_feedback(struct wl_client *c, struct wl_resource if (ci) ci->asked_feedback = 1; dmabuf_new_feedback(c, r, id); } -/* Per-surface feedback would let us hint a different tranche for a window on its own display - * layer; we have nothing better to say per surface, so it is the default one. */ static void dmabuf_get_surface_feedback(struct wl_client *c, struct wl_resource *r, uint32_t id, struct wl_resource *surface) { dmabuf_new_feedback(c, r, id); @@ -320,8 +279,6 @@ void bind_dmabuf(struct wl_client *c, void *data, uint32_t ver, uint32_t id) { struct wl_resource *r = wl_resource_create(c, &zwp_linux_dmabuf_v1_interface, ver, id); wl_resource_set_implementation(r, &dmabuf_impl, NULL, NULL); if (!g_dmabuf_fmts_ready) dmabuf_build_formats(); - /* From version 4 the format and modifier events are gone: the client asks for feedback - * instead, and sending both would only confuse it about which list is authoritative. */ if (ver >= 4) return; for (int f = 0; f < g_dmabuf_nfmt; f++) { zwp_linux_dmabuf_v1_send_format(r, g_dmabuf_fmts[f].fmt); diff --git a/app/src/main/cpp/waylandcomp/src/wl_single_pixel.c b/app/src/main/cpp/waylandcomp/src/wl_single_pixel.c index 9dfe78233..96c666496 100644 --- a/app/src/main/cpp/waylandcomp/src/wl_single_pixel.c +++ b/app/src/main/cpp/waylandcomp/src/wl_single_pixel.c @@ -1,15 +1,9 @@ -/* - * wp_single_pixel_buffer_manager_v1 (version 1): a wl_buffer of one pixel of one colour, which a - * client stretches over a surface with wp_viewporter. KWin's Wayland backend - the desktop's KWin - * nested in this compositor - refuses to start without it. The surface takes the pixel as a 1x1 - * image, the way it takes a wl_shm buffer (compositor.c, take_pixel). - */ #include <stdlib.h> #include "droiddeck_ext.h" #include "single-pixel-buffer-v1-server-protocol.h" struct single_pixel { - uint8_t bgra[4]; /* premultiplied, as wl_shm's ARGB8888 lies in memory */ + uint8_t bgra[4]; }; static void buffer_destroy(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } @@ -30,7 +24,6 @@ static void mgr_create_buffer(struct wl_client *c, struct wl_resource *r, uint32 struct single_pixel *p = calloc(1, sizeof(*p)); struct wl_resource *buffer = p ? wl_resource_create(c, &wl_buffer_interface, 1, id) : NULL; if (!buffer) { free(p); wl_client_post_no_memory(c); return; } - /* The protocol's values span the whole u32 range; the top byte is the 8-bit channel. */ p->bgra[0] = blue >> 24; p->bgra[1] = green >> 24; p->bgra[2] = red >> 24; diff --git a/app/src/main/cpp/waylandcomp/src/wl_text_input.c b/app/src/main/cpp/waylandcomp/src/wl_text_input.c index 631167e91..8b0ade227 100644 --- a/app/src/main/cpp/waylandcomp/src/wl_text_input.c +++ b/app/src/main/cpp/waylandcomp/src/wl_text_input.c @@ -1,25 +1,3 @@ -/* - * Text input: zwp_text_input_manager_v3 (advertised at version 1), the host input method - * bridge winewayland.drv uses for IME text (our soft keyboard). - * - * Focus model. winewayland enables its text input as soon as our `enter` names a surface and - * routes every IME update to that surface's window - inside that window's process (the update - * list is per process). Keyboard focus in this compositor sits on the desktop surface (the - * desktop owner's process), so text input can't follow it; it follows droiddeck_ime_target(): - * the program window last clicked, else the topmost program window. Clicking into notepad - * moves the text-input focus to notepad's process, which is where the edit control lives. - * - * Protocol state. Requests between commits are pending; `commit` bumps the serial and applies - * them (enable resets the state as the protocol says). We answer with `done(serial)` only when - * we sent events (preedit/commit strings), never for every commit. - * - * Host side. droiddeck_on_text_input(enabled, program, cursor rect in scene pixels) tells the app - * when a program starts/stops accepting IME text or moves its caret (Wine reports the rect only - * when an edit control positions its IME composition window, via ImmSetCompositionWindow). - * Typed text arrives through text_input_host_*: preedit_string / commit_string + done. - * delete_surrounding_text is NOT used: winewayland's handler for it is empty, so deletions - * become Backspace/Delete key presses on the wl_keyboard path instead. - */ #define _GNU_SOURCE 1 #include <stdlib.h> #include <string.h> @@ -39,28 +17,25 @@ struct text_input { struct wl_resource *res; struct wl_client *client; struct wl_list link; - struct wl_resource *entered; /* surface we sent enter for, or NULL */ - uint32_t serial; /* commits received */ - /* pending, applied on commit */ + struct wl_resource *entered; + uint32_t serial; + int p_enable, p_disable, p_rect_set, p_content_set; int p_rect[4]; uint32_t p_hint, p_purpose; - /* current */ int enabled; int rect[4]; uint32_t hint, purpose; - int has_preedit; /* we sent a non-empty preedit last */ + int has_preedit; }; static struct wl_list g_inputs; -static struct wl_resource *g_target; /* surface text input is focused on */ +static struct wl_resource *g_target; -/* What the app was last told. */ static int g_told_enabled, g_told_rect[4]; static struct text_input *g_told_ti; static const char *ti_program(struct text_input *ti) { return droiddeck_client_name(ti->client); } -/* The enabled text input focused on the current target, if any. */ static struct text_input *active_input(void) { struct text_input *ti; if (!g_target) return NULL; @@ -92,13 +67,12 @@ static void tell_app(void) { droiddeck_on_text_input(enabled, enabled ? ti_program(ti) : "", rect[0], rect[1], rect[2], rect[3]); } -/* ------------------------------------------------------------------ focus */ static void send_leave(struct text_input *ti) { if (!ti->entered) return; zwp_text_input_v3_send_leave(ti->res, ti->entered); ti->entered = NULL; - ti->enabled = 0; /* the protocol disables on leave */ + ti->enabled = 0; ti->has_preedit = 0; } @@ -129,7 +103,6 @@ void droiddeck_text_input_surface_gone(struct wl_resource *surface) { if (g_told_ti && g_told_ti->entered == NULL && g_told_enabled) tell_app(); } -/* ------------------------------------------------------------------ requests */ static void ti_res_destroy(struct wl_resource *r) { struct text_input *ti = wl_resource_get_user_data(r); @@ -166,7 +139,7 @@ static void ti_commit(struct wl_client *c, struct wl_resource *r) { if (!ti) return; ti->serial++; if (ti->p_enable) { - ti->enabled = ti->entered != NULL; /* enable only counts while focused */ + ti->enabled = ti->entered != NULL; memset(ti->rect, 0, sizeof(ti->rect)); ti->hint = 0; ti->purpose = 0; ti->has_preedit = 0; @@ -212,7 +185,6 @@ static void bind_text_input_manager(struct wl_client *c, void *data, uint32_t ve wl_resource_set_implementation(r, &tim_impl, NULL, NULL); } -/* ------------------------------------------------------------------ host side (compositor thread) */ static size_t utf8_chars(const char *s, size_t len) { size_t n = 0; @@ -220,7 +192,6 @@ static size_t utf8_chars(const char *s, size_t len) { return n; } -/* Byte offset of UTF-8 character index `chars` (negative = end). */ static int32_t utf8_offset(const char *s, size_t len, int chars) { if (chars < 0) return (int32_t)len; size_t i = 0; @@ -259,7 +230,7 @@ void text_input_host_delete(int before, int after) { if (before > 64) before = 64; if (after > 64) after = 64; struct text_input *ti = active_input(); - if (ti && ti->has_preedit) { /* deleting inside a composition: clear it first */ + if (ti && ti->has_preedit) { zwp_text_input_v3_send_preedit_string(ti->res, "", 0, 0); zwp_text_input_v3_send_done(ti->res, ti->serial); ti->has_preedit = 0; diff --git a/app/src/main/cpp/waylandcomp/src/wl_toplevel_icon.c b/app/src/main/cpp/waylandcomp/src/wl_toplevel_icon.c index ce1cd0e5f..667a22534 100644 --- a/app/src/main/cpp/waylandcomp/src/wl_toplevel_icon.c +++ b/app/src/main/cpp/waylandcomp/src/wl_toplevel_icon.c @@ -1,8 +1,3 @@ -/* - * xdg_toplevel_icon_manager_v1 (version 1): window icons are accepted and dropped. The scene - * has no title bars to draw them in; winewayland only stops complaining that the global is - * missing. We send no icon_size (any size is fine) and `done` at bind, as the protocol asks. - */ #include <stdlib.h> #include "droiddeck_ext.h" #include "xdg-toplevel-icon-v1-server-protocol.h" diff --git a/app/src/main/java/com/droiddeck/launcher/App.kt b/app/src/main/java/com/droiddeck/launcher/App.kt index 1b1a2fd82..43d3ffc12 100644 --- a/app/src/main/java/com/droiddeck/launcher/App.kt +++ b/app/src/main/java/com/droiddeck/launcher/App.kt @@ -5,11 +5,6 @@ import com.droiddeck.launcher.runtime.BwrapSpawner import com.droiddeck.launcher.session.CrashHandler import com.droiddeck.launcher.session.GpuClockPin -/** - * Process-wide setup: the crash handler, so a session's folder is finished even when we die, and - * the GPU clock pin cleared in case a killed process left it set, and the socket Flatpak's - * sandboxes are started through (BwrapSpawner). - */ class App : Application() { override fun attachBaseContext(base: android.content.Context) = super.attachBaseContext(com.droiddeck.launcher.core.AppLanguage.wrap(base)) @@ -28,7 +23,6 @@ class App : Application() { GpuClockPin.clearLeftover(this) BwrapSpawner.start(this) com.droiddeck.launcher.frontend.GameFileSync.start(this) - // The game stores the Stores section can talk to; the native engine loads only when one needs it. 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) diff --git a/app/src/main/java/com/droiddeck/launcher/ComponentsMenu.kt b/app/src/main/java/com/droiddeck/launcher/ComponentsMenu.kt index 1fd549f80..cbd760370 100644 --- a/app/src/main/java/com/droiddeck/launcher/ComponentsMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/ComponentsMenu.kt @@ -8,10 +8,6 @@ import androidx.compose.runtime.setValue import com.droiddeck.launcher.session.ComponentsManager import com.droiddeck.launcher.session.ProtonDefault -/** - * Component state and work for the launcher and session drawer: the Protons and their - * DXVK/VKD3D sets, the Nightlies catalog, downloads and the swap/restore actions. - */ internal class ComponentsMenu(private val activity: android.app.Activity, private val ui: Handler) { var compSnapshot by mutableStateOf<ComponentsManager.Snapshot?>(null) var compCatalog by mutableStateOf<List<ComponentsManager.CatalogItem>>(emptyList()) @@ -26,7 +22,6 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat val compDownloadRevision: Long get() = downloadRevision private var shownDefault: String? = null - /** Reads the Protons (and on first open saves their originals) off the UI thread; the Nightlies list comes from its cache. */ fun refreshComponents(snapshotFirst: Boolean = false) { Thread({ if (snapshotFirst) runCatching { ComponentsManager.snapshotAll(activity) } @@ -107,8 +102,6 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat private companion object { const val TAG = "ComponentsMenu" - // Both screens use the same package store and .part files. Keep the in-flight registry - // across menu/activity instances so a package cannot be downloaded twice at once. var downloads by mutableStateOf<Map<String, Int>>(emptyMap()) var downloadRevision by mutableStateOf(0L) } diff --git a/app/src/main/java/com/droiddeck/launcher/DeckyMenu.kt b/app/src/main/java/com/droiddeck/launcher/DeckyMenu.kt index efffe789d..9f8a27f9f 100644 --- a/app/src/main/java/com/droiddeck/launcher/DeckyMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/DeckyMenu.kt @@ -9,17 +9,12 @@ import com.droiddeck.launcher.runtime.DeckyManager import com.droiddeck.launcher.session.SessionState import java.io.File -/** - * Decky Loader's state in the launcher: what is installed, the releases on offer and an install - * in progress. The launcher screen keeps one and shows it. - */ internal class DeckyMenu(private val activity: android.app.Activity, private val ui: android.os.Handler) { var deckyInstalled by mutableStateOf<String?>(null) var deckyReleases by mutableStateOf<List<DeckyManager.Release>>(emptyList()) var deckyChecking by mutableStateOf(false) var deckyStage by mutableStateOf<String?>(null) var deckyPercent by mutableIntStateOf(-1) - /** True while the stage is the plugin's binary download (DeckyManager.downloadingBinary). */ var deckyDownloadingBinary by mutableStateOf(false) var deckySupervisor by mutableStateOf(false) var deckyReleaseRequest = 0 @@ -32,8 +27,6 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val .getOrElse { DeckyManager.ReleaseChannels(emptyList(), emptyList()) } ui.post { if (request == deckyReleaseRequest) { - // Use the newest compatible stable build, or the newest compatible - // prerelease when the fork has not published a stable ARM64 asset. deckyReleases = channels.stable.ifEmpty { channels.prerelease } deckyChecking = false } diff --git a/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt b/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt index ababdc230..09c195201 100644 --- a/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt +++ b/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt @@ -25,35 +25,21 @@ import com.droiddeck.launcher.ui.GpuDriversState import com.droiddeck.launcher.ui.PairRow import com.droiddeck.launcher.wayland.CompositorHost -/** - * The GPU drivers: what this GPU is, the matched driver pairs the release repos offer for it, and - * - in Auto, the default - keeping the recommended pair installed and set. Under Advanced, the - * Linux (runtime) and Android (display) lists as before, each picked on its own - except a - * bundle's two halves (DriverBundle), which are always picked, and deleted, together. The launcher - * screen keeps one and hands it to the Components page's GPU drivers tab. - */ internal class DriverMenus(private val activity: Activity, private val ui: Handler) { var linuxRows by mutableStateOf<List<DriverRow>>(emptyList()) - /** The latest Banners-Turnip release as each driver menu offers it (see [refreshReleaseRows]). */ var linuxDownloads by mutableStateOf<List<com.droiddeck.launcher.ui.DownloadRow>>(emptyList()) var androidDownloads by mutableStateOf<List<com.droiddeck.launcher.ui.DownloadRow>>(emptyList()) - // Empty until the first refresh: the activity has no context yet while its fields are made. var releaseStatus by mutableStateOf("") var releaseChecking by mutableStateOf(false) var canRestoreBundled by mutableStateOf(false) - /** Asset name -> download percent, while it downloads. */ private val releaseProgress = HashMap<String, Int>() var linuxSelected by mutableStateOf("") val gpu: GpuInfo = GpuInfo.detect() - // Read in refreshDrivers/ensureAuto: the activity has no context yet while its fields are made. var mode by mutableStateOf(SessionPrefs.GPU_DRIVERS_AUTO) var pairRows by mutableStateOf<List<PairRow>>(emptyList()) - /** The pair being installed, and how far along. */ var pairBusy by mutableStateOf<String?>(null) var pairPercent by mutableIntStateOf(-1) - /** What Auto last did or found, for the line under the pair in use. */ var autoStatus by mutableStateOf("") - /** The bundle both drivers are set to, as "DD-Turnip 0.1.0", or null when they are not one. */ var activeBundle by mutableStateOf<String?>(null) private var autoCheckedThisProcess = false @@ -107,14 +93,12 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl refreshPairs() } - /** One line for the settings row that opens this: "Auto · WinNative · Balanced". */ fun summary(): String { val active = pairRows.firstOrNull { it.active }?.name ?: activeBundle 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 } - /** The pairs the last check found, with what is installed and in use; this GPU's first. */ fun refreshPairs() { val lm = LinuxVulkanDriverManager(activity) val td = TurnipDriver(activity) @@ -143,12 +127,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl if (auto) ensureAuto(force = false) } - /** - * Auto: make the recommended pair for this GPU the one installed and set. Looks online when - * [force]d (refresh), or when the last check is a day old - Banners-Turnip rebuilds hourly, and - * a new pair a day is plenty - otherwise works from the last check. Once per app start on its - * own; the pair it replaces is removed, anything the user imported or picked is left alone. - */ fun ensureAuto(force: Boolean) { mode = SessionPrefs.gpuDriverMode(activity) if (mode != SessionPrefs.GPU_DRIVERS_AUTO || pairBusy != null || releaseChecking) return @@ -184,18 +162,12 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl }, "gpu-driver-auto").start() } - /** Manual: install (if need be) and set both halves of a pair. */ fun selectPair(key: String) { if (pairBusy != null) return val pair = DriverPairs.from(TurnipReleases.cached(activity)).firstOrNull { it.key == key && it.complete } ?: return installPair(pair, auto = false) } - /** - * Download what is missing of [pair], then set both halves - only once both are in, so a - * failed download never leaves a half-changed pair. Auto's own downloads are recorded, and the - * ones the new pair replaces are removed. - */ private fun installPair(pair: DriverPairs.DriverPair, auto: Boolean) { if (!pair.complete) return pairBusy = pair.key @@ -252,21 +224,15 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl }, "gpu-driver-pair").start() } - /** The bundle a release bundle was downloaded as, while both its halves are installed. */ private fun installedBundle(asset: TurnipReleases.Asset): DriverBundle.Bundle? = TurnipReleases.installedId(activity, asset) { DriverBundle.get(activity, it) != null }?.let { DriverBundle.get(activity, it) } - /** The id a release asset was installed as (a bundle's id for a bundle), or null when it is not installed. */ private fun installedIdOf(asset: TurnipReleases.Asset): String? = when { asset.bundle -> installedBundle(asset)?.id asset.linux -> TurnipReleases.installedId(activity, asset, LinuxVulkanDriverManager(activity)::isInstalled) else -> TurnipReleases.installedId(activity, asset, TurnipDriver(activity)::isInstalled) } - /** - * One release asset, downloaded, checked and installed through the importer; returns its id, - * a bundle's id for a bundle. - */ private fun installAsset(asset: TurnipReleases.Asset, progress: (Int) -> Unit): String { var file: File? = null try { @@ -284,12 +250,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl } } - /** - * Import off the main thread - a driver zip is a few MB and the glibc check reads the whole - * library - then say what happened. A refusal's message is the user-facing reason. A bundle, - * from any import, installs both halves and is set as both drivers at once. [linux] null (the - * tab's own Import .zip) takes a single driver to whichever list its libc belongs in. - */ fun importDriver(uri: Uri, linux: Boolean?) { val name = activity.displayNameOf(uri) Thread({ @@ -328,10 +288,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl }, "import-driver").start() } - /** - * Pick one driver from a list, which is Manual (Auto would put its pair back). A bundle's half - * sets the bundle's other half in the other list as well. - */ fun selectDriver(id: String, linux: Boolean) { val bundle = DriverBundle.containing(activity, id, linux) when { @@ -343,7 +299,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl refreshDrivers() } - /** Set both halves of [bundle] as the drivers in use; returns what to tell the user. */ private fun useBundle(bundle: DriverBundle.Bundle): String { val displayChanged = SessionPrefs.androidDriver(activity) != bundle.androidId DriverBundle.select(activity, bundle) @@ -354,11 +309,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl ) } - /** - * Delete an imported or downloaded driver. A mode still set to it goes back to its default, so a - * session never starts on a driver that is gone; a release download is forgotten, so the menu - * offers it again. - */ fun deleteDriver(id: String, linux: Boolean) { val bundle = DriverBundle.containing(activity, id, linux) if (bundle != null) { @@ -381,7 +331,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl refreshDrivers() } - /** The download entries and the refresh line, from what the last check found. */ fun refreshReleaseRows() { val check = TurnipReleases.cached(activity) fun rows(linux: Boolean) = check?.assets.orEmpty() @@ -413,7 +362,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl } } - /** Only when the user taps refresh: nothing goes online on its own. */ fun checkLatestTurnip() { if (releaseChecking) return releaseChecking = true @@ -431,7 +379,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl }, "turnip-release-check").start() } - /** Download one release driver and import it through the same importer a picked zip uses. */ fun downloadReleaseDriver(assetName: String) { val asset = TurnipReleases.cached(activity)?.assets?.firstOrNull { it.name == assetName } ?: return if (releaseProgress.containsKey(assetName)) return diff --git a/app/src/main/java/com/droiddeck/launcher/HomeActivity.kt b/app/src/main/java/com/droiddeck/launcher/HomeActivity.kt index 7a6c60cbf..54526834b 100644 --- a/app/src/main/java/com/droiddeck/launcher/HomeActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/HomeActivity.kt @@ -5,7 +5,6 @@ import android.content.Intent import android.os.Bundle import com.droiddeck.launcher.session.SessionService -/** Receives the system Home launch and routes it without consuming KEYCODE_HOME. */ class HomeActivity : Activity() { override fun attachBaseContext(newBase: android.content.Context) { super.attachBaseContext(newBase) diff --git a/app/src/main/java/com/droiddeck/launcher/HomeApp.kt b/app/src/main/java/com/droiddeck/launcher/HomeApp.kt index cc1b2afc0..74b49821c 100644 --- a/app/src/main/java/com/droiddeck/launcher/HomeApp.kt +++ b/app/src/main/java/com/droiddeck/launcher/HomeApp.kt @@ -13,7 +13,6 @@ import androidx.core.graphics.drawable.toBitmap import com.droiddeck.launcher.session.SessionService import com.droiddeck.launcher.session.SessionState -/** Android Home role and the launchable Android apps shown by the in-session app drawer. */ object HomeApp { data class LaunchableApp( val label: String, @@ -22,17 +21,10 @@ object HomeApp { val icon: Bitmap?, ) - /** - * Whether the app offers itself as a Home app at all. HomeActivity is declared disabled and - * only enabled when the user turns on "Use as a Home screen": a player who does not want a - * launcher never gets Android's "which Home app?" question, and it is one apk either way. - * The setting is the component's own state, which Android keeps across updates. - */ fun isHomeScreenEnabled(context: Context): Boolean = context.packageManager.getComponentEnabledSetting(ComponentName(context, HomeActivity::class.java)) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED - /** Turned off while DroidDeck is the Home app, the phone goes back to its own launcher. */ fun setHomeScreenEnabled(context: Context, enabled: Boolean) { context.packageManager.setComponentEnabledSetting( ComponentName(context, HomeActivity::class.java), @@ -113,7 +105,6 @@ object HomeApp { try { context.startActivity(settingsIntent()) } catch (_: Exception) { - // A role request remains available on Android 10+ if the settings page is missing. val request = roleRequestIntent(context) if (request != null) context.startActivity(request) } diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index bd7577a78..d72953db8 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -79,11 +79,6 @@ import com.droiddeck.launcher.session.GameStorage import com.droiddeck.launcher.input.SecondScreenDisplay import com.droiddeck.launcher.input.SecondScreenDisplays -/** - * The whole app outside a session: is the runtime installed, is there a newer one, frame - * generation, and one button that starts Steam. Everything a Steam client can do - the library, - * the store, downloads, settings - is the client's own job once [SessionActivity] has it on screen. - */ class MainActivity : ComponentActivity() { override fun attachBaseContext(newBase: android.content.Context) { super.attachBaseContext(newBase) @@ -97,13 +92,10 @@ class MainActivity : ComponentActivity() { private val updates = UpdatesMenu(this, ui) private val protons = ProtonMenu(this, ui) - // The screen's state. Compose redraws whatever reads these when they change. private var installed by mutableStateOf<String?>(null) - /** A session launch waiting on its button to fill the page (LaunchFlood). */ private var flood by mutableStateOf<PendingFlood?>(null) private var floodProgress by androidx.compose.runtime.mutableFloatStateOf(0f) private class PendingFlood(val from: androidx.compose.ui.geometry.Rect, val intent: Intent) - /** A session stopped behind a flood: its blue, drawn back into the button it started from (FloodReturn). */ private var returning by mutableStateOf<ReturningFlood?>(null) private class ReturningFlood(val color: Int, val to: androidx.compose.ui.geometry.Rect?) private var ready by mutableStateOf(false) @@ -123,12 +115,10 @@ class MainActivity : ComponentActivity() { private var steamRepairQueued by mutableStateOf(false) private var mangoapp by mutableStateOf(true) private var steamController by mutableStateOf(SessionPrefs.CONTROLLER_DECK) - // Components page: FEX / DXVK / VKD3D-Proton per Proton (ComponentsManager). private var showComponents by mutableStateOf(false) private var focusComponentsContent by mutableStateOf(true) private var showMapping by mutableStateOf(false) private var saveBusy: String? = null - /** What to do with the zip or folder the file picker hands back after a game page's Manage saves. */ private var onSavePicked: ((File) -> Unit)? = null private var controllerSettings by mutableStateOf<ControllerPrefs.Settings?>(null) private var catalog by mutableStateOf<List<DesktopCatalog.Entry>?>(null) @@ -167,7 +157,6 @@ class MainActivity : ComponentActivity() { private var gameStoresEnabled by mutableStateOf(true) private var gameStoresSpeedTier by mutableStateOf("fast") private var storesOpenTab by mutableStateOf(SessionPrefs.STORES_OPEN_LIBRARY) - /** A page a notification asked for ("stores:downloads", "app:<id>"), until the front end has gone there. */ private var navRequest by mutableStateOf<String?>(null) private var storesShowMature by mutableStateOf(false) private var mic by mutableStateOf(false) @@ -200,8 +189,6 @@ class MainActivity : ComponentActivity() { refreshWifiDiscovery() } - // The app's own picker (files/), once per kind of pick: the two driver lists validate - // differently, and the reason a zip is refused names the list it belongs in. private val pickComponent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r -> if (r.resultCode == RESULT_OK) InAppFilePicker.pickedUri(r.data)?.let { importComponent(it) } } @@ -235,7 +222,6 @@ class MainActivity : ComponentActivity() { wifiDiscoveryAsked = SessionPrefs.wifiDiscoveryAsked(this) wifiDiscoveryBlocked = !wifiDiscoveryPermission && wifiDiscoveryAsked && WifiDiscovery.permissions.none { shouldShowRequestPermissionRationale(it) } - // A revoked grant must not leave an enabled switch behind. if (!wifiDiscoveryPermission && SessionPrefs.wifiDiscoveryEnabled(this)) { SessionPrefs.setWifiDiscoveryEnabled(this, false) } @@ -251,7 +237,6 @@ class MainActivity : ComponentActivity() { } } - /** Takes a new or updated Lossless Scaling from Steam and shows what LSFG can use. */ private fun syncLossless() { Thread({ Lossless.sync(this) @@ -271,7 +256,6 @@ class MainActivity : ComponentActivity() { }, "lossless-import").start() } - /** Import a save zip into [game]: pick it in the app's file picker, then back up and unzip off the main thread. */ private fun importSaves(name: String, game: () -> GameSaves.Game) { if (SessionState.running) { android.widget.Toast.makeText(this, R.string.main_saves_close_session, android.widget.Toast.LENGTH_LONG).show() @@ -290,7 +274,6 @@ class MainActivity : ComponentActivity() { pickSaveZip.launch(InAppFilePicker.buildIntent(this, listOf("zip"), getString(R.string.main_saves_pick_zip, name), GameSaves.savesDir().parentFile?.parentFile?.path)) } - /** Export [game]'s saves in [layout] to a folder picked in the app's file picker. */ private fun exportSaves(name: String, layout: GameSaves.Layout, game: () -> GameSaves.Game) { onSavePicked = { dir -> saveAction(getString(R.string.main_saves_exporting, name)) { @@ -301,7 +284,6 @@ class MainActivity : ComponentActivity() { GameSaves.savesDir().mkdirs() pickSaveDir.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.main_saves_pick_folder, name, layout.label(this)), GameSaves.savesDir().path)) } - /** The Games tab's +: the picked .exe becomes a game (or selects / switches the one it belongs to). */ private fun addGameExe(path: String) { val exe = java.io.File(path) Thread({ @@ -327,7 +309,6 @@ class MainActivity : ComponentActivity() { } private var addedGamesArt by mutableStateOf(true) @Volatile private var artFetchRunning = false - /** The user entered a SteamGridDB API key of their own. */ private var sgdbUserKey by mutableStateOf(false) private val pickRomsDir = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r -> if (r.resultCode == RESULT_OK) InAppFilePicker.pickedPath(r.data)?.let { path -> @@ -335,7 +316,6 @@ class MainActivity : ComponentActivity() { romsDir = path } } - // The mode whose lifted sheet last read its settings, with what it shows; refreshed by loadModeSettings(). private var settingsMode by mutableStateOf<String?>(null) private var resolution by mutableStateOf(com.droiddeck.launcher.session.SessionDisplay.DEFAULT_RESOLUTION) private var fexPreset by mutableStateOf("") @@ -386,7 +366,6 @@ class MainActivity : ComponentActivity() { refreshHomeAppState() } - /** The session surface rises over the front end instead of cutting to it. */ override fun startActivity(intent: Intent?) { if (intent?.component?.className == SessionActivity::class.java.name) { when { @@ -406,8 +385,6 @@ class MainActivity : ComponentActivity() { } if (intent?.component?.className == SessionActivity::class.java.name) { if (flood != null) return - // Started by a blue button: it floods the page first. A running session is re-joined - // as before, rising over the front end. val from = com.droiddeck.launcher.ui.LaunchOrigin.take() if (from != null && !SessionState.running && com.droiddeck.launcher.ui.Motion.scale != 0f) { flood = PendingFlood(from, intent) @@ -419,7 +396,6 @@ class MainActivity : ComponentActivity() { if (intent?.component?.className == SessionActivity::class.java.name) overridePendingTransition(R.anim.session_rise, R.anim.session_hold) } - /** The page is all blue: the session opens on the same blue, with no animation of its own. */ private fun launchFlooded(f: PendingFlood) { if (busy || LinuxRuntimeInstaller.isBusy()) { flood = null; floodProgress = 0f; return } val signal = com.droiddeck.launcher.ui.Themes.byId(theme).signal @@ -453,7 +429,6 @@ class MainActivity : ComponentActivity() { 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 if (shortcutPicker) { - // The shortcut picker must only offer games from the scan started for this request. steamGames = emptyList() shortcutLibraryScanning = true } else shortcutLibraryScanning = false @@ -531,7 +506,6 @@ class MainActivity : ComponentActivity() { theme = SessionPrefs.theme(this) frameGenLabel = getString(R.string.frame_gen_off) appScale = com.droiddeck.launcher.core.AppUiPrefs.scale(this) - // Last start's game list, so the Steam wall is up on the first frame; refresh() replaces it. steamGames = if (shortcutPicker) emptyList() else com.droiddeck.launcher.frontend.LibraryCache.load(this) backActionsInverted = SessionPrefs.backActionsInverted(this) orientation = SessionPrefs.orientation(this) @@ -603,18 +577,14 @@ class MainActivity : ComponentActivity() { ), FrontEndActions( onPlay = { startSteamSession() }, - // The client's desktop UI, with gamescope's Deck integration disabled. onPlayDesktopUi = { startSession(Intent(this, SessionActivity::class.java) .putExtra(SessionService.EXTRA_MODE, SessionService.MODE_STEAM) .putExtra(SessionService.EXTRA_STEAM_UI, "desktop"), steamSession = true) }, - // A store game may need something done first (an Epic game its sign-in code). onSteamGame = { g -> if (shortcutPicker) chooseGameShortcut(g) else { - // The launch takes the Windows components already on; the game's list is - // looked at again in the background once the session is over. com.droiddeck.launcher.frontend.AutoComponents.beforeLaunch(this, g) com.droiddeck.launcher.stores.StoreLaunch.prepare(this, g) { launchGame(g) } } @@ -624,7 +594,6 @@ class MainActivity : ComponentActivity() { onSyncGameFiles = { pickGameExport(null) }, onAddGameExe = { path -> addGameExe(path) }, onAddedGamesChanged = { folders -> - // New games want art; an edit does not. val known = steamGames.mapNotNull { it.gameFiles?.path }.toSet() updateAddedGames(folders, then = { if (folders.any { it !in known }) fetchAddedGameArt() }) }, @@ -652,7 +621,6 @@ class MainActivity : ComponentActivity() { .putExtra(SessionService.EXTRA_MODE, SessionService.MODE_DESKTOP)) }, onEmulator = { e -> launchProgram(e.program) }, - // An app added on the Desktop page, full screen under gamescope like an emulator. onUserApp = { app -> app.args.firstOrNull()?.let { Library.flatpakNames[it] = app.name } startActivity(Intent(this, SessionActivity::class.java) @@ -660,7 +628,6 @@ class MainActivity : ComponentActivity() { .putExtra(SessionService.EXTRA_PROGRAM, app.program) .putExtra(SessionService.EXTRA_PROGRAM_ARGS, app.args.toTypedArray())) }, - // A Flatpak app from the store, full screen under gamescope like an emulator. onFlatpakApp = { id, name -> Library.flatpakNames[id] = name startActivity(Intent(this, SessionActivity::class.java) @@ -675,7 +642,6 @@ class MainActivity : ComponentActivity() { .putExtra(SessionService.EXTRA_PROGRAM, e.program) .putExtra(SessionService.EXTRA_PROGRAM_ARGS, Library.launchArgs(e.id, g.guestPath).toTypedArray())) }, - // The activity re-attaches to the session that is running; nothing restarts. onResume = { startActivity(Intent(this, SessionActivity::class.java)) }, onSteamSettings = { loadModeSettings(SessionService.MODE_STEAM) }, onRemoveDesktop = { removeDesktop() }, @@ -694,7 +660,6 @@ class MainActivity : ComponentActivity() { onProtons = { openProtons() }, onComponents = { focusContent -> openComponents(focusContent) }, onComponentsTab = { tab -> openComponents(focusContent = true, tab = tab) }, - // A game page's Manage saves: the game's Proton and saves are read when the work runs, off the main thread. onSaveImport = { sg -> importSaves(sg.name) { GameSaves.game(sg) } }, onSaveExport = { sg, layout -> exportSaves(sg.name, layout) { GameSaves.game(sg) } }, onPerformance = { refreshCores(); showComponents = false; showMapping = false; showPerformance = true }, @@ -742,7 +707,6 @@ class MainActivity : ComponentActivity() { onLanguage = { tag -> if (tag != com.droiddeck.launcher.core.AppLanguage.chosen(this)) { com.droiddeck.launcher.core.AppLanguage.setChosen(this, tag) - // Redrawn in the new language; Setup's page and tab are saved state. recreate() } }, @@ -895,25 +859,17 @@ class MainActivity : ComponentActivity() { if (savedInstanceState == null) ui.post { startSteamAtStartupIfEnabled() } - // The session's logs land in Downloads so a failed run can be handed over as a folder - // rather than dug out of app-private storage. targetSdk 28 means the old permission still - // grants exactly that. val wanted = ArrayList<String>() if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { wanted.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) } - // The microphone is off by default. Someone who turned it on before the permission was - // granted is asked once here, with the storage prompt; a refusal is not asked again - the - // toggle asks when used. if (SessionPrefs.micEnabled(this) && !SessionPrefs.micAsked(this) && checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { wanted.add(Manifest.permission.RECORD_AUDIO) SessionPrefs.setMicAsked(this) } if (wanted.isNotEmpty()) requestPermissions(wanted.toTypedArray(), 1) - // A store sign-in an earlier build left in plain text is sealed with the Keystore key. Thread({ com.droiddeck.launcher.stores.StoreAccounts.encryptAll(this) }, "store-credentials").start() - // A session folder left without its ending - the process was killed - gets it now. if (!SessionState.running) Thread({ com.droiddeck.launcher.session.LogMigration.run(this) com.droiddeck.launcher.frontend.GamesFolderMigration.run(this) @@ -969,7 +925,6 @@ class MainActivity : ComponentActivity() { } } - /** On the Components page the pad's LB / RB step through the stack's layers, wrapping around. */ override fun dispatchKeyEvent(event: KeyEvent): Boolean { if (event.keyCode == KeyEvent.KEYCODE_BUTTON_A) com.droiddeck.launcher.ui.HeldKeys.confirm = event.action == KeyEvent.ACTION_DOWN if (showComponents && (event.keyCode == KeyEvent.KEYCODE_BUTTON_L1 || event.keyCode == KeyEvent.KEYCODE_BUTTON_R1)) { @@ -978,8 +933,6 @@ class MainActivity : ComponentActivity() { } return true } - // Compose throws when its focus tree has lost the focused node; one key is dropped - // instead of the app. return try { super.dispatchKeyEvent(event) } catch (e: IllegalStateException) { @@ -994,25 +947,17 @@ class MainActivity : ComponentActivity() { orientation = SessionPrefs.orientation(this) requestedOrientation = orientation refreshWifiDiscovery() - // Back from a store's sign-in page: the Stores chips show the account at once. A store - // install finishing while this screen is up rebuilds the Games list through the listener. com.droiddeck.launcher.stores.StoresState.refresh(this) com.droiddeck.launcher.stores.StoresState.libraryListener = { refreshAddedGames(); refresh() } com.droiddeck.launcher.ui.Motion.refresh(this) - // Back from a session stopped behind a flood: open on its blue, before the first frame. com.droiddeck.launcher.ui.QuitFlood.take()?.let { c -> returning = ReturningFlood(c, com.droiddeck.launcher.ui.LaunchOrigin.takeReturn()) } refreshPhantomStatus() syncLossless() - // Opening the app and coming back from a session both land here. updates.onResume() - // Auto keeps this GPU's recommended driver pair installed and set: once per app start. drivers.ensureAuto(force = false) - // Newer GE-Proton / proton-cachyos / DroidDeck-Proton releases: looked up every few hours at most. protons.checkUpdates() - // Swaps queued while a game ran on that Proton go in once nothing uses it (usually the - // session has just ended). Cheap when nothing is queued. if (!SessionState.running) Thread({ val applied = runCatching { ComponentsManager.applyQueued(this) }.getOrDefault(emptyList()) if (applied.isNotEmpty()) ui.post { @@ -1029,8 +974,6 @@ class MainActivity : ComponentActivity() { decky.deckyInstalled = DeckyManager.installed(this) DeckyManager.syncCefMarker(this) decky.deckySupervisor = DeckyManager.supervisorEnabled(this) - // Added games' art (a store lookup for what the folders lack) starts here, not only when - // the cog opens. refreshAddedGames() if (catalog == null) loadDesktopCatalog() if (!busy) Thread({ checkCatalog() }, "catalog").start() @@ -1057,10 +1000,8 @@ class MainActivity : ComponentActivity() { unregisterReceiver(wifiLocationReceiver) unregisterReceiver(mediaReceiver) ui.removeCallbacks(mediaRefresh) - // An install finishing while this screen is away is picked up by onResume's refresh. com.droiddeck.launcher.stores.StoresState.libraryListener = null displayManager.unregisterDisplayListener(secondScreenDisplayListener) - // The session covers the page by now; coming back finds it as it was. flood = null returning = null floodProgress = 0f @@ -1116,7 +1057,6 @@ class MainActivity : ComponentActivity() { }, "catalog-desktop").start() } - /** Runs a save import or export off the main thread, one at a time, and says how it went. */ private fun saveAction(label: String, work: () -> String) { if (busy || LinuxRuntimeInstaller.isBusy() || saveBusy != null) return saveBusy = label @@ -1129,7 +1069,6 @@ class MainActivity : ComponentActivity() { }, "game-saves-action").start() } - /** The Protons DroidDeck installs are the Components page's Proton layer. */ private fun openProtons() = openComponents(focusContent = true, tab = com.droiddeck.launcher.ui.PROTON_TAB) private fun refreshController() { @@ -1153,7 +1092,6 @@ class MainActivity : ComponentActivity() { showPerformance = false showMapping = false showComponents = true - // The Proton layer's options for every Proton, as they are now. fexPreset = SessionPrefs.fexPreset(this) forceSsbs = SessionPrefs.forceSsbs(this) fastSync = SessionPrefs.fastSync(this) @@ -1196,7 +1134,6 @@ class MainActivity : ComponentActivity() { onDownload = { components.downloadComponent(it) }, onRefresh = { components.refreshComponentCatalog() }, onImport = { pickComponent.launch(InAppFilePicker.buildIntent(this, WCP_EXT, getString(R.string.comp_pick_wcp))) }, - // Reached by a hop (a game's Components card, Play's Graphics drivers): back the way it came. onBack = { if (!com.droiddeck.launcher.ui.Hops.back { showComponents = false }) showComponents = false }, games = steamGames, generalFexPreset = fexPreset, @@ -1206,7 +1143,6 @@ class MainActivity : ComponentActivity() { onAuto = { on -> drivers.setMode(on) }, onPair = { key -> drivers.selectPair(key) }, onRefresh = { if (drivers.mode == SessionPrefs.GPU_DRIVERS_AUTO) drivers.ensureAuto(force = true) else drivers.checkLatestTurnip() }, - // Picking one driver on its own is Manual: Auto would put its pair back. onSelectLinux = { id -> drivers.selectDriver(id, linux = true) }, onSelectAndroid = { id -> drivers.selectDriver(id, linux = false) }, onRemoveLinux = { id -> drivers.deleteDriver(id, linux = true) }, @@ -1241,9 +1177,7 @@ class MainActivity : ComponentActivity() { ) }, ) - // A download can finish in the session drawer while this page is visible. androidx.compose.runtime.LaunchedEffect(components.compDownloadRevision) { components.refreshComponents() } - // A Proton installed or removed changes the stack's list of Protons. androidx.compose.runtime.LaunchedEffect(protons.protonBusyId) { if (protons.protonBusyId == null) components.refreshComponents() } } @@ -1292,7 +1226,6 @@ class MainActivity : ComponentActivity() { @Composable private fun ModeSheetHost(mode: String, tab: String, extras: com.droiddeck.launcher.ui.SheetExtras) { - // Read for the other mode last (or not yet): what this one shows is read as it opens. if (settingsMode != mode) return com.droiddeck.launcher.ui.ModeSheetRows( ModeSettings( @@ -1377,8 +1310,6 @@ class MainActivity : ComponentActivity() { onMic = { on -> SessionPrefs.setMicEnabled(this, on) mic = on - // The session checks the grant itself at start; asking here means the - // answer is in before the first session that wants it. if (on && checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), 2) } @@ -1461,17 +1392,12 @@ class MainActivity : ComponentActivity() { ) } - /** - * The Games tab after these added games were added or edited: each read alone and put in - * place of its old entry (or added), no library-wide scan. [then] runs after, on the main thread. - */ private fun updateAddedGames(folders: List<String>, then: () -> Unit = {}) { if (folders.isEmpty()) return Thread({ val fresh = folders.mapNotNull { f -> com.droiddeck.launcher.frontend.AddedGames.single(this, f)?.let { com.droiddeck.launcher.frontend.Library.addedGame(this, it) } } - // An added or edited game's Windows components, from its source's list. fresh.forEach { com.droiddeck.launcher.frontend.AutoComponents.refreshLater(this, it) } ui.post { val paths = folders.toSet() @@ -1481,19 +1407,16 @@ class MainActivity : ComponentActivity() { }, "added-games-update").start() } - /** An added game removed: off the Games tab now; the rest (cache, Steam, listing) off the main thread. */ 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() } - /** After the added games changed: art that is missing is fetched (off the main thread). */ private fun refreshAddedGames() { sgdbKeyKnown() fetchAddedGameArt() } - /** Whether the user has a SteamGridDB key of their own, for Setup; read off the main thread. */ private fun sgdbKeyKnown() { Thread({ val set = com.droiddeck.launcher.frontend.SteamGridDb.userKey(this).isNotEmpty() @@ -1502,8 +1425,6 @@ class MainActivity : ComponentActivity() { } private fun fetchAddedGameArt() { - // Art the games do not have yet, from Steam's store, off the main thread; the rail - // redraws when something arrives. if (SessionPrefs.addedGamesArt(this) && !artFetchRunning) { artFetchRunning = true Thread({ @@ -1550,8 +1471,6 @@ class MainActivity : ComponentActivity() { gameStorage = SessionPrefs.gameStorage(this) storageDiagnostics = SessionPrefs.storageDiagnosticsEnabled(this) settingsMode = mode - // The sheet lifts at once, on what was last read; the slow part (driver files, a walk of the - // added-games folders, the storage volumes) lands while it rises. Thread({ val zram = com.droiddeck.launcher.core.ZramSupport.supported() val zramOn = SessionPrefs.zramCompression(this) @@ -1572,7 +1491,6 @@ class MainActivity : ComponentActivity() { }, "mode-settings").start() } - /** A second Steam library, proven writable first; "" = internal only. */ private fun setGameStorage(path: String, label: String) { if (path.isNotEmpty() && path != SessionPrefs.GAME_STORAGE_OFF) { val problem = GameStorage.prepare(this, path) @@ -1585,7 +1503,6 @@ class MainActivity : ComponentActivity() { gameStorage = path } - /** The two masks as the dialog shows them; an empty stored list shows as every core ticked. */ private fun refreshCores() { clientOverride = SessionPrefs.clientCpusOverride(this) glThread = SessionPrefs.glThread(this) @@ -1626,17 +1543,11 @@ class MainActivity : ComponentActivity() { ?: SessionState.program?.substringAfterLast('/')?.substringBefore('.') ?: getString(R.string.main_running_program) else -> "Steam" } else null - // The libraries, off the main thread: manifests and a folder scan. val scanGeneration = ++libraryScanGeneration if (shortcutPicker) shortcutLibraryScanning = true Thread({ - // The emulators first: they need no walk of the games, and Desktop's dock is made of them. val emus = Library.emulators(this) { id -> DesktopCatalog.installed(this, id) != null } ui.post { if (scanGeneration == libraryScanGeneration) emulatorList = emus } - // Steam's own games next, from their manifests alone (quick), beside the added games - // already listed: a game Steam installed during the session shows as the session ends, - // without waiting for the added games' walk below. Its Windows components' list is - // queued then too, on the low-priority worker, so its page is usually ready. if (ready) { val quick = runCatching { Library.steamGames(this) }.getOrNull() if (quick != null) ui.post { @@ -1650,15 +1561,9 @@ class MainActivity : ComponentActivity() { } } } - // Then one update with the whole list: the wall places games by their position in it, - // so a partial list first would shuffle every capsule when the rest arrived. - // LibraryCache covers the wait. val games = if (ready) Library.launchableGames(this) else emptyList() val all = games.distinctBy { it.gameId } if (ready) com.droiddeck.launcher.frontend.LibraryCache.save(this, all) - // Once per app start: the Steam games' Windows components from Steam's lists. - // Queued only, after the launcher has been up a little; the worker takes them one at a time. - // And again after each session (a game installed in it, files changed). val endedSession = SessionState.sessionId.takeIf { !SessionState.running && it != null && it != componentSweepSession } if (ready && (!componentSweepDone || endedSession != null)) { componentSweepDone = true @@ -1672,8 +1577,6 @@ class MainActivity : ComponentActivity() { handleGameLink(all) } } - // Box art for the games that have none, fetched after the list is up; the list is - // rebuilt once if any was found. if (!OfflineMode.enabled(this) && CoverArt.fetchMissing(this, emus.flatMap { it.games })) { val refreshed = Library.emulators(this) { id -> DesktopCatalog.installed(this, id) != null } ui.post { emulatorList = refreshed } @@ -1685,7 +1588,6 @@ class MainActivity : ComponentActivity() { if (runtimeChangesBlocked()) return val release = available if (removalPending || installed != null && release?.version == installed) { - // Nothing to install: offer the one destructive thing this screen can do. showRemove = true return } @@ -1693,14 +1595,10 @@ class MainActivity : ComponentActivity() { Thread({ checkCatalog() }, "catalog").start() return } - // The runtime draws with Turnip, an Adreno driver: on Mali, Xclipse or PowerVR the - // compositor gets no usable Vulkan device and a session is sound over a black screen. - // Said before the download, not after it; the user may still go ahead. if (installed == null && !com.droiddeck.launcher.core.DeviceSupport.adreno()) { showNonAdreno = release; return } install(release) } - /** Starts a session; with no runtime on a non-Adreno, the same warning Setup gives comes first, before any download. */ private fun startSession(intent: Intent, steamSession: Boolean = false): Boolean { if (busy || LinuxRuntimeInstaller.isBusy()) return false refreshPhantomStatus() @@ -1759,7 +1657,6 @@ class MainActivity : ComponentActivity() { } } - /** Setup's Reset all settings; the activity is recreated so every screen reads the defaults (and the language) again. */ private fun resetSettings(gameProfiles: Boolean) { if (SessionState.running) return val scopes = steamGames.map { it.profileKey } @@ -1784,16 +1681,10 @@ class MainActivity : ComponentActivity() { } private fun install(release: LinuxRuntimeInstaller.Release) { - // The service keeps the process alive if the user switches away; this screen joins the - // same install (or starts it, if it gets there first) to show the progress. com.droiddeck.launcher.runtime.RuntimeInstallService.start(this, release) followRuntimeOperation { listener -> LinuxRuntimeInstaller.install(this, release, listener) } } - /** - * Shows a runtime operation's progress until it ends. [run] either starts one or joins the one already - * running (null: nothing was), which is how a launcher rebuilt mid-install picks it back up. - */ private fun followRuntimeOperation(run: (LinuxRuntimeInstaller.ProgressListener) -> Boolean?) { busy = true failed = false @@ -1823,7 +1714,6 @@ class MainActivity : ComponentActivity() { companion object { private const val TAG = "MainActivity" private const val MEDIA_SETTLE_MS = 1500L - /** What the picker offers for a driver zip; some file apps label a zip as a plain stream. */ private val ZIP_EXT = listOf("zip") private val WCP_EXT = listOf("wcp") } diff --git a/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt b/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt index e73babbc1..5bd651cf5 100644 --- a/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/ProtonMenu.kt @@ -9,11 +9,6 @@ import com.droiddeck.launcher.session.ProtonExtras import com.droiddeck.launcher.ui.ProtonRow import com.droiddeck.launcher.session.SessionState -/** - * The optional ARM64 Protons (GE-Proton, proton-cachyos, DroidDeck-Proton): the rows the page lists, - * an install or removal in progress, and whether a newer release is out. The launcher screen keeps - * one and shows it. - */ internal class ProtonMenu(private val activity: android.app.Activity, private val ui: android.os.Handler) { var protonRows by mutableStateOf<List<ProtonRow>>(emptyList()) var protonBusyId by mutableStateOf<String?>(null) @@ -42,7 +37,6 @@ internal class ProtonMenu(private val activity: android.app.Activity, private va }, "install-proton-$id").start() } - /** Removes the build in [dir], or with [older] every build of it but the newest. */ fun removeProton(id: String, dir: String? = null, older: Boolean = false) { val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return if (protonBusyId != null || SessionState.running) return @@ -66,7 +60,6 @@ internal class ProtonMenu(private val activity: android.app.Activity, private va } } - /** Looks for newer releases, at most every few hours (see ProtonExtras.checkLatest); quiet offline. */ fun checkUpdates(force: Boolean = false) { if (!checking.compareAndSet(false, true)) return Thread({ diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index f584f69ba..b414cd672 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -110,17 +110,6 @@ import com.droiddeck.launcher.ui.sessionDrawerPages import com.droiddeck.launcher.ui.step import kotlin.math.abs -/** - * The session's screen: our Wayland compositor presenting onto this activity's Surface, and the - * input that reaches it. The session itself - gamescope, the Steam client, audio - belongs to - * [SessionService] and keeps running when this activity does not exist, which is what lets the - * user leave Big Picture for another app and come back to it still signed in and still - * downloading. - * - * Three layers: the SurfaceView the compositor draws into, the on-screen pad (a canvas View, - * since it is input rather than a menu), and one Compose layer on top for everything else - - * the HUD line, the loading overlay, the drawer and its dialogs. - */ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { override fun attachBaseContext(newBase: android.content.Context) { super.attachBaseContext(newBase) @@ -130,7 +119,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private lateinit var pip: com.droiddeck.launcher.session.SessionPipController private var pipUi by mutableStateOf(false) private var pipAutoEnter by mutableStateOf(false) - /** Unfolded controls: the setting, and the Deck controls under the game while they show. */ private var unfoldedControls by mutableStateOf(false) private var unfoldedPanel: com.droiddeck.launcher.ui.DeckControlsPanel? = null private lateinit var sessionRoot: FrameLayout @@ -140,20 +128,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private lateinit var loading: LoadingState private lateinit var hud: PerfHud private var padBridge: PadBridge? = null - /** The handheld's gyro and accelerometer, fed to the Deck controller while the session shows. */ private var padMotion: com.droiddeck.launcher.input.PadMotion? = null - /** Between onResume and onPause. */ private var resumed = false private val deckPadListener: () -> Unit = { uiHandler.post { updatePadMotion(); openDeckRing("deck pad changed"); applyUnfoldedControls() } } - /** - * A Deck's controller is there from the moment it boots, and the client looks for it once, as - * it starts. The ring behind the Deck (libfakeinput's /dev/hidraw16) opens on the pad's first - * input otherwise - on a phone with nothing attached, the first touch of the on-screen - * controls, seconds after the client had looked, found no controller and never set the touch - * mode, so its interface took a finger as a mouse. Opened as soon as the session settles on a - * Deck instead. - */ private fun openDeckRing(why: String) { val deck = SessionState.deckPad val opened = if (deck) padBridge?.start() else null @@ -164,23 +142,16 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }) } - /** Motion is read while the session shows and only if the pad is a Deck controller - which - * the service can decide after this activity has resumed (SessionState.deckPadListener). */ private fun updatePadMotion() { - // A Steam Controller brings its own gyro (SteamControllerBle); the phone's would fight it. if (resumed && !pipUi && SessionState.deckPad && !SteamControllerBle.connected) padMotion?.start() else padMotion?.stop() } private var steamController: SteamControllerBle? = null private var onScreenControls: OnScreenControls? = null - /** Steam's touch controls (SteamTouchControls), made once the session offers the device. */ private var steamTouchControls: com.droiddeck.launcher.input.SteamTouchControls? = null private var steamTouchAvailable by mutableStateOf(false) private val steamTouchRetry = Runnable { updateOnScreenControls() } - /** While Steam's touch controls are up: how the game's touch config says touches outside them - * act (Steam Link's input and mouse modes), in place of the drawer's touch mode. */ private var steamTouchOptions: com.droiddeck.launcher.input.SteamTouchConfig.Options? = null - /** The touch mode touches are routed by: the drawer's, or the game's while Steam's controls are up. */ private var routedTouchMode = SessionPrefs.TOUCH_AUTO private var keyboard: KeyboardHost? = null private var controllerSettings by mutableStateOf<ControllerPrefs.Settings?>(null) @@ -194,22 +165,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private var touchMode by mutableStateOf(SessionPrefs.TOUCH_AUTO) private var cursorPos by mutableStateOf(androidx.compose.ui.geometry.Offset(-100f, -100f)) private var cursorVisible by mutableStateOf(false) - /** The blue the front end flooded to on the way in; the loading screen opens on it (LaunchFlood). */ private var floodColor by mutableStateOf<Int?>(null) private val cursorHide = Runnable { cursorVisible = false } - /** When the pad or the on-screen controls were last used (uptimeMillis); see [showCursor]. */ private var lastPadInputMs = 0L - // The program's own pointer, from wl_pointer.set_cursor (see syncClientCursor): its image in - // session pixels, hotspot, and whether it asked for no pointer at all. private val cursorBuf = IntArray(WaylandCompositor.CURSOR_BUF_INTS) private var cursorSerial = 0 private var clientHidesCursor = false - /** - * A program holds a pointer lock (zwp_locked_pointer_v1: a mouse-look game, through gamescope). - * While it does, a physical mouse is captured and its raw deltas go to the program as relative - * motion. Without capture the deltas are worked out from Android's pointer position, which stops - * at the screen edge, and so did the camera (#153). - */ private var pointerLocked = false private val pointerLockListener = WaylandCompositor.PointerLockListener { locked, _, _ -> uiHandler.post { @@ -223,7 +184,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private var cursorHotX by mutableStateOf(0) private var cursorHotY by mutableStateOf(0) private var cursorImageScale by mutableStateOf(1f) - /** labwc changes the shape a moment after the move that reached an edge: look again then. */ private val cursorResync = Runnable { if (syncClientCursor() && clientHidesCursor) cursorVisible = false } private val uiHandler = Handler(Looper.getMainLooper()) private var pendingBackAction: Runnable? = null @@ -254,17 +214,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } private val resumeKeysDown = mutableSetOf<Pair<Int, Int>>() - // Compose reads these; the activity writes them. private var drawerOpen by mutableStateOf(false) - /** A stop growing out of the dialog's Stop (overlay px); the session ends once it covers the screen. */ private var quitFrom by mutableStateOf<androidx.compose.ui.geometry.Rect?>(null) - /** Left behind that flood: the front end opens on its blue, so no sink of our own. */ private var quitFlooded = false - /** Leaving behind a flood spreading out of the throbber (FloodSpread); see finish(). */ private var leaving by mutableStateOf(false) - /** Close without the leaving flood: a retry opens a fresh session at once. */ private var closeAtOnce = false - /** The drawer's Components tab: the Protons as last read (ComponentsManager). */ private var drawerComponents by mutableStateOf<ComponentsManager.Snapshot?>(null) private val drawerComponentMenu by lazy { ComponentsMenu(this, uiHandler) } private var drawerGameProfileId by mutableStateOf<Long?>(null) @@ -289,24 +243,18 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private var drawerPage by mutableStateOf(SessionDrawerPage.CONTROLLER) private var drawerControllerActive by mutableStateOf(false) private var backActionsInverted by mutableStateOf(false) - /** What the pad is to the Steam client (SessionPrefs.CONTROLLER_*), switched from the drawer. */ private var padType by mutableStateOf(SessionPrefs.CONTROLLER_DECK) - /** The on-screen PC keyboard (ui/PcKeyboard): real key presses, Esc and F1 included. */ private var pcKeyboardOpen by mutableStateOf(false) - /** The game in front, when its source has achievements. */ private var achievementsInFront by mutableStateOf<AchievementsInFront?>(null) private var achievementsOpen by mutableStateOf(false) private var hudOn by mutableStateOf(true) private var upscaler by mutableStateOf(0) private var upscaleSharpness by mutableStateOf(75) private var effects by mutableStateOf(ScreenEffects.OFF) - /** Cardboard mode (gpu/Cardboard) and the sensor that turns the virtual screen with the head. */ private var cardboard by mutableStateOf(Cardboard.OFF) private var headTracker: HeadTracker? = null private var steamVrStatus by mutableStateOf<SteamVr.Status?>(null) - /** The picture was switched to side-by-side for SteamVR and goes back when it stops. */ private var steamVrSwitchedPicture by mutableStateOf(false) - /** SteamVR has been seen running since the switch: only then does "not running" mean it stopped. */ private var steamVrSeenRunning = false private val hasRotationSensor by lazy { (getSystemService(SENSOR_SERVICE) as android.hardware.SensorManager).let { @@ -314,8 +262,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { ?: it.getDefaultSensor(android.hardware.Sensor.TYPE_ROTATION_VECTOR) } != null } - // Held in a field: SharedPreferences keeps its listeners weakly. A change from anywhere (the - // drawer, tools/droiddeckctl prefs set) applies live. private val cardboardPrefListener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key -> if (key == SessionPrefs.CARDBOARD_KEY) uiHandler.post { applyCardboard(SessionPrefs.cardboard(this)) } } @@ -336,7 +282,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private var secondScreenMode by mutableStateOf(SessionState.secondScreenMode) private var secondScreenDisplays by mutableStateOf<List<SecondScreenDisplay>>(emptyList()) private var selectedSecondScreenDisplay by mutableStateOf(SessionState.secondScreenDisplay) - /** Between onStart and onStop: while stopped the second screen stays closed, its choice kept. */ private var started = false private lateinit var displayManager: DisplayManager private var secondScreenPresentation: SecondScreenPresentation? = null @@ -349,12 +294,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - /** - * The panel's rate, followed for the rest of the session. It is read once at start, and a - * panel that drops from 120 to 60 Hz later (battery saver, heat, a vendor overriding the mode - * asked for) left the compositor telling gamescope every frame took 8.3 ms, so the client and - * its games paced against a vblank that no longer came. (WinNative 53836ca9.) - */ private fun followPanelRefresh(displayId: Int) { if (!CompositorHost.isStarted) return val display = (if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay) ?: return @@ -365,11 +304,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private var isHomeApp by mutableStateOf(false) private var androidApps by mutableStateOf<List<HomeApp.LaunchableApp>>(emptyList()) - /** - * Shows the on-screen pad when nothing is plugged in and takes it away the moment something - * is - a user with a controller in their hands should not be looking at buttons they cannot - * press, and a user without one must not be left with no way to answer Big Picture. - */ private val deviceListener = object : InputManager.InputDeviceListener { override fun onInputDeviceAdded(deviceId: Int) { logInputDevice("connected", deviceId) @@ -399,7 +333,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } if (!SessionState.running && SessionState.phase in setOf(SessionPhase.IDLE, SessionPhase.FAILED)) { val mode = intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM - // A Flatpak app or AppImage is named by its arguments, which the name lookup reads here. SessionState.programArgs = intent.getStringArrayExtra(SessionService.EXTRA_PROGRAM_ARGS)?.toList().orEmpty() SessionEvents.begin(this, mode, SessionPaths.label(mode, intent.getStringExtra(SessionService.EXTRA_PROGRAM))) } @@ -413,9 +346,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { SessionEvents.record("agent.start_requested", mapOf("mode" to SessionState.mode)) } window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - // Game-tier power policy for the whole session: the panel's fastest mode (one the frame cap - // divides, when there is a cap), and the OS told it is in gameplay. Logged so a slow device - // says why. The cap is the session's: an activity re-created mid-session keeps it. if (!SessionState.running) { SessionState.fpsLimit = SessionPrefs.fpsLimit( this, SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM), @@ -425,18 +355,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (!SessionState.running) SessionState.followScreen = followChoice() applyFollowOrientation(resources.configuration) goFullscreen() - // The device's volume keys change the stream the session plays on (the relay and - // PulseAudio are media playback); they are never forwarded to the guest. volumeControlStream = android.media.AudioManager.STREAM_MUSIC displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager refreshSecondScreenDisplays() - // No runtime is not a reason to leave: the loading screen installs it (installThenStart, - // below) and the session starts when it is in. if (!LinuxRuntime.isInstalled(this)) Log.i(TAG, "the Linux runtime is not installed; the loading screen installs it") - // Captured mouse events go down the focus chain, and focus sits somewhere in the overlay: - // the root sees every one of them whichever view that is. val root = object : FrameLayout(this) { override fun dispatchCapturedPointerEvent(event: MotionEvent): Boolean = onCapturedPointer(event) || super.dispatchCapturedPointerEvent(event) @@ -449,7 +373,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { surfaceView = SurfaceView(this) surfaceView.holder.addCallback(this) root.addView(surfaceView) - // The window's size is only known once laid out, and changes as the phone folds or turns. root.addOnLayoutChangeListener { _, l, t, r, b, ol, ot, or, ob -> if (r - l != or - ol || b - t != ob - ot) uiHandler.post { applyUnfoldedControls() } } @@ -458,7 +381,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { bridge.setSelectSteam(ControllerPrefs.read(this).selectSteam) padBridge = bridge SessionState.padBridge = bridge - // A paired Steam Controller is read over Bluetooth LE, out of lizard mode, as Steam Link reads it. SteamControllerBle.listener = { uiHandler.post { updatePadMotion(); updateOnScreenControls() } } steamController = SteamControllerBle(this).also { it.start() } padMotion = com.droiddeck.launcher.input.PadMotion(this) { @@ -467,8 +389,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } SessionState.deckPadListener = deckPadListener openDeckRing("activity created") - // A player on the pad or the on-screen controls has no use for the mouse arrow; the next - // touchpad or mouse move brings it back (showCursor), once the pad has been quiet a moment. bridge.setOnPlayerInput { lastPadInputMs = android.os.SystemClock.uptimeMillis() uiHandler.removeCallbacks(cursorHide) @@ -477,8 +397,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { onScreenControls = OnScreenControls(this, bridge, onKeyboard = ::togglePcKeyboard).also { root.addView(it) } keyboard = KeyboardHost(this).also { root.addView(it) } touchpad = TouchpadGestures(PointerGestures.slop(this), pointerListener) - // One arrow, ours: Android draws a system pointer for a mouse over any window, and the - // session already draws the pointer it is sent. val noCursor = android.view.PointerIcon.getSystemIcon(this, android.view.PointerIcon.TYPE_NULL) root.pointerIcon = noCursor surfaceView.pointerIcon = noCursor @@ -496,7 +414,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { hud.onPresentingWindowChanged = { if (FrameGen.engine(this) != FrameGen.ENGINE_OFF) CompositorHost.rearmFrameGen { applyFrameGen() } } - // A session that has already drawn is past its milestones; do not cover its picture. if (SessionState.running && SessionState.firstFrameSeen) { loading.visible = false hud.start() @@ -520,12 +437,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { WaylandCompositor.setPointerLockListener(pointerLockListener) SessionEvents.markReadyIfPossible() SessionState.endListener = endListener - // A single Back opens the session menu; two quick presses/swipes send the Steam QAM chord. - // The single action waits out the double-press window so the two actions stay distinct. onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { if (pipUi) return - // The PC keyboard closes first (B on a controller), then Back is the drawer again. when { ::loading.isInitialized && loading.ended -> finish() drawerOpen -> drawerOpen = false @@ -538,18 +452,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { watchSession() } - /** - * One Compose layer for every menu and overlay. Touches nothing in it consumes fall through to - * the pad and the game underneath. - */ private fun createSessionOverlay(): ComposeView = ComposeView(this).apply { isFocusable = true isFocusableInTouchMode = true - // Compose draws each control's focus ring. Android's default highlight would cover - // this full-screen host and remain as a white veil after the drawer closes. defaultFocusHighlightEnabled = false setContent { - // The drawer, the PC keyboard and picture in picture take the mouse back. androidx.compose.runtime.LaunchedEffect(drawerOpen, pcKeyboardOpen, achievementsOpen, pipUi) { updatePointerCapture() } DroidDeckTheme { if (pipUi) { @@ -569,7 +476,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { loading.step, loading.percent, loading.elapsed, loading.hint, loading.ended, topic = loading.topic, readable = loading.readable, title = loadingTitle(), steam = loadingMode() == SessionService.MODE_STEAM, - // As the drawer's Stop does: the service stops, and a start still installing is cancelled. onCancel = { SessionService.stop(this@SessionActivity); finish() }, endedDetail = loading.endedDetail, onRetry = { retrySession() }, @@ -581,7 +487,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { window.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(android.graphics.Color.BLACK)) }, ) - // Opening the drawer takes the controller away from the game: release its pad. androidx.compose.runtime.LaunchedEffect(drawerOpen) { if (drawerOpen) { syncLossless() @@ -593,9 +498,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { releaseDrawerDirection() } } - // SteamVR's state for the Cardboard group, refreshed while the drawer shows it. - // Watched for as long as Cardboard mode is on (or the picture is switched for it), drawer open or - // not: however SteamVR is started or quit, the picture follows it. LaunchedEffect(cardboard.enabled, steamVrSwitchedPicture) { while (cardboard.enabled || steamVrSwitchedPicture) { val now = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { @@ -614,7 +516,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { achievementsInFront = found if (found == null) achievementsOpen = false } - // The PC keyboard takes it too, for moving over the keys; the drawer opens over it. androidx.compose.runtime.LaunchedEffect(pcKeyboardOpen) { if (pcKeyboardOpen) padBridge?.releaseAll() } if (pcKeyboardOpen) com.droiddeck.launcher.ui.PcKeyboard( sendKey = { code, down -> if (CompositorHost.isStarted) sendGuestKey(code, down) }, @@ -651,7 +552,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { onUpscaler = { m -> SessionPrefs.setUpscaler(this@SessionActivity, m); upscaler = m WaylandCompositor.nativeSetUpscaler(m) - // Two Looks carry a scaling mode of their own, so the mode decides which one the log names. WaylandCompositor.nativeSetLookName(ScreenEffectLooks.match(effects, m)?.name) }, onUpscaleSharpness = { pct -> @@ -700,7 +600,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { onQam = if (SessionState.mode == SessionService.MODE_STEAM) ({ triggerSteamQam() }) else null, onAchievements = achievementsInFront?.let { { drawerOpen = false; achievementsOpen = true } }, achievementsProgress = achievementsInFront?.progress, - // Only where the session set the pad up to be switched (SessionService: deckPad). padType = if (SessionState.mode == SessionService.MODE_STEAM && SessionState.deckPad) padType else null, onPadType = { id -> SessionPrefs.setSteamController(this@SessionActivity, id) @@ -806,7 +705,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { onComponentSwap = { pid, comp, value -> swapDrawerComponent(pid, comp, value) }, onGameComponent = ::setDrawerGameComponent, )) - // The panel takes the pad, as the PC keyboard does. LaunchedEffect(achievementsOpen) { if (achievementsOpen) { padBridge?.releaseAll() @@ -860,7 +758,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private fun closeAchievements() { achievementsOpen = false } - /** A live game window only; Steam's overlay may be in front of that window. */ private fun resolveAchievementsInFront(): AchievementsInFront? { if (!SessionState.running || SessionState.mode != SessionService.MODE_STEAM) return null val snapshot = AgentGuest.focus(this) ?: return null @@ -905,7 +802,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { performBackAction(double = false) } pendingBackAction = pending - // Wait 500 ms for a second Back press before performing the single-press action. uiHandler.postDelayed(pending, 500L) } @@ -923,7 +819,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } - /** Re-reads the Protons for the drawer's Components tab, first running swaps a closed game was holding back. */ private fun refreshDrawerComponents() { Thread({ val applied = runCatching { ComponentsManager.applyQueued(this) }.getOrDefault(emptyList()) @@ -1045,7 +940,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val build = snap.protons.firstOrNull { it.proton.id == protonId }?.proton?.version?.let(ComponentsManager::safeName) val current = active ?: "orig:$build" if (queued != null && value == current) { - // Picking what is already in place again cancels the swap waiting for the game. ComponentsManager.cancelQueued(this, protonId, comp); getString(R.string.comp_swap_cancelled) } else if (value.startsWith("orig:")) ComponentsManager.restore(this, protonId, comp, value.removePrefix("orig:")) else ComponentsManager.swap(this, protonId, value) @@ -1114,7 +1008,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }, "drawer-game-no-steam").start() } - /** The session this screen is bringing up: the live one when re-attached, else the intent's. */ private fun loadingMode(): String = if (SessionState.running) SessionState.mode else intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM @@ -1127,8 +1020,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } private fun loadingTitle(): String = when (loadingMode()) { - // Also when the desktop was asked for with Steam (Big Picture's Switch to Desktop): the client - // does not open on it. SessionService.MODE_DESKTOP -> getString(R.string.session_starting_desktop) SessionService.MODE_RUN -> { val program = if (SessionState.running) SessionState.program else intent.getStringExtra(SessionService.EXTRA_PROGRAM) @@ -1139,8 +1030,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } private fun shareCurrentSessionLogs() { - // Once a session has ended its folder has been handed on (SessionPaths.take), and the - // ended screen's Share logs is for that one. val folder = SessionPaths.current() ?: SessionPaths.lastEnded() if (folder == null || !folder.isDirectory) { Toast.makeText(this, R.string.session_no_logs, Toast.LENGTH_LONG).show() @@ -1167,8 +1056,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { upscaleSharpness = SessionPrefs.upscaleSharpness(this) effects = SessionPrefs.screenEffects(this) applyCardboard(SessionPrefs.cardboard(this)) - // A switch left from before this activity (or this resume): SteamVR was running then, so the next - // "not running" hands the picture back. if (SessionPrefs.cardboardSourceBeforeSteamVr(this) >= 0 && !steamVrSwitchedPicture) { steamVrSwitchedPicture = true steamVrSeenRunning = true @@ -1197,21 +1084,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { padType = SessionPrefs.steamController(this) } - // ── Compositor ────────────────────────────────────────────────────────────────────────── - /** True while the loading screen is installing the Linux runtime or the desktop; the session waits for it. */ @Volatile private var installingRuntime = false - /** - * First Play (or Desktop) on a fresh install: the runtime, and for the desktop its package, - * are downloaded and unpacked here, on the loading screen's own line and bar, and the session - * starts when they are in. Nothing else changes. - * - * The first Steam session also gets Valve's ARM64 Proton here, before the client has ever - * started, so the compatibility tool is installed and the default at sign-in with no install - * dialog, download or client restart. Not being able to fetch it stops nothing: the session - * then asks the client for it after sign-in, as it always has. - */ private fun installThenStart(runtime: Boolean, desktop: Boolean, proton: Boolean = false) { if (SessionState.stopRequested) return installingRuntime = true @@ -1264,7 +1139,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } loading.percent = -1 loading.say(getString(R.string.session_starting_session), LoadingState.Topic.SESSION, readable = false) - // The surface may have come and gone while the download ran; start on the live one. if (surfaceView.holder.surface?.isValid == true) surfaceCreated(surfaceView.holder) } }, "runtime-install").start() @@ -1279,16 +1153,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }, "session-failure-artifacts").start() } - /** - * The loading screen's lines for one package: downloading it (with and without its size), checking it, - * unpacking it; the checklist [topic] they belong to, and whether the checking line is shown as it is. - */ private class PackageLines( val downloading: Int, val downloadingMb: Int, val checking: Int, val unpacking: Int, val topic: LoadingState.Topic, val checkingReadable: Boolean, ) - /** Reports one package's download on the loading screen: "<what> · 332 of 791 MB", checking, unpacking. */ private fun progressFor(what: PackageLines, mb: Long) = object : com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.ProgressListener { override fun onProgress(stage: String, percent: Int) = onProgress(com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.Step.OTHER, stage, percent) @@ -1309,7 +1178,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - /** Null when the runtime is in, else the loading screen's closing line. */ private fun installRuntime(): String? { val release = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.fetchRelease() ?: return getString(R.string.session_runtime_catalog_unreachable) @@ -1318,7 +1186,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return if (ok) null else getString(R.string.session_runtime_install_failed) } - /** Null when the desktop package is in, else the loading screen's closing line. */ private fun installDesktop(): String? { uiHandler.post { loading.percent = -1; loading.say(getString(DESKTOP_LINES.downloading), DESKTOP_LINES.topic, readable = true) } val entry = com.droiddeck.launcher.runtime.DesktopCatalog.desktopEntry(this) @@ -1327,7 +1194,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return problem?.let { getString(R.string.session_desktop_install_failed, it) } } - /** Null when the ARM64 Proton is in, else why not; the session goes on either way. */ private fun installProton(): String? { uiHandler.post { loading.percent = -1; loading.say(getString(PROTON_LINES.downloading), PROTON_LINES.topic, readable = true) } val catalog = com.droiddeck.launcher.runtime.DesktopCatalog @@ -1342,22 +1208,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { SessionEvents.transition(SessionPhase.STARTING_COMPOSITOR, "compositor.starting") } val runtimeDir = File(filesDir, ".wayland-rt").apply { mkdirs() } - // The compositor hands this keymap to wl_keyboard clients, which is how the guest reads - // the evdev codes we inject. FileUtils.copyAsset(this, "wayland/keymap.xkb", File(runtimeDir, "keymap.xkb")) - // Turnip, not the system Adreno driver: importing the dma-bufs gamescope commits needs - // VK_EXT_image_drm_format_modifier, which the system driver does not implement. val turnip = TurnipDriver(this) com.droiddeck.launcher.gpu.DriverSetupLog.write(this, "surface.driver-setup") val driverId = if (CompositorHost.isStarted) null else turnip.install() - // The output size belongs to the session, not to the Surface: gamescope's display is - // sized once when the session starts and cannot change. A foldable recreates the Surface - // on the other panel, and recomputing the size there told the compositor a 21:9 buffer - // was 16:9 - the picture came back squashed sideways. While a session runs, keep its size. - // Follow screen is the exception: there the compositor tells gamescope the new size, so the - // session takes the shape of the Surface it now has. val previous = SessionState.outputSize val live = if (SessionState.followScreen && !isInPictureInPictureMode) com.droiddeck.launcher.session.SessionDisplay.followSize(holder.surfaceFrame.width(), holder.surfaceFrame.height()) @@ -1365,25 +1221,18 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val size = live ?: if (SessionState.running) previous else outputSize() SessionState.outputSize = size if (!SessionState.running) SessionState.refreshHz = refreshHz() - // How far the panel enlarges the session, for texture sharpness "Auto" (TextureFiltering): - // the game environment is published again so the next launch derives it from this session. val panel = panelBounds() SessionState.upscaleRatio = maxOf(panel.width(), panel.height()).toFloat() / maxOf(size.first, size.second).coerceAtLeast(1) Thread({ runCatching { GameEnvironmentStore.publish(this) } .onFailure { Log.e(TAG, "Could not update game environment", it) } }, "game-env-publish").start() - // Letterbox, never stretch or crop: the output can be a different shape from the panel, - // and a game's picture must keep its proportions with bars, not lose its edges. WaylandCompositor.nativeSetScaleMode(SCALE_FIT, ALIGN_CENTER) WaylandCompositor.nativeSetUpscaler(SessionPrefs.upscaler(this)) WaylandCompositor.nativeSetUpscaleSharpness(SessionPrefs.upscaleSharpness(this)) SessionPrefs.screenEffects(this).push(SessionPrefs.upscaler(this)) panelSizeMm().let { (w, h) -> SessionPrefs.cardboard(this).push(w, h) } WaylandCompositor.nativeSetCardboardPoseFile(File(filesDir, ".wayland-rt/droiddeck-headpose").path) - // The session's folder, claimed here because the compositor starts before the service and - // opens its log once. The compositor reads the path from its environment; setting it after - // it has started changes nothing, which is why the service copies the file in at teardown. if (!SessionState.running) { val waylandLog = File(SessionPaths.beginOrCurrent(this), "wayland.log") WaylandCompositor.setSessionLogFile(waylandLog) @@ -1393,9 +1242,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { Log.w(TAG, "could not point the compositor's log at $waylandLog", e) } } - // HDR10: the gate is decided once, when the compositor starts (it lives for the whole app - // process), from the panel's own word and the mode's setting. Zero-copy presentation is - // what puts an HDR frame on a display layer tagged BT2020_PQ, so it is turned on with it. if (!CompositorHost.isStarted) { val mode = SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM) val probe = HdrSupport.probe(this) @@ -1429,10 +1275,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { WaylandCompositor.nativeResizeOutput(size.first, size.second) } if (!SessionState.running) SessionEvents.record("compositor.started") - // The service owns everything below the compositor. It is started whenever no session is - // running - NOT only when the compositor was just started: the compositor lives for the - // whole process, so the second Play after a session ended used to re-attach the Surface, - // start nothing, and leave the loading panel counting up over a dead session. if (!SessionState.running) { SessionEvents.transition(SessionPhase.STARTING_GUEST, "guest.starting") SessionService.start( @@ -1460,12 +1302,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - /** - * Follow screen: the session's display takes the window's new size (a Fold opening onto its - * inner panel, or closing onto the cover), so the picture fills the panel instead of being - * letterboxed onto it. Not in picture-in-picture: that window is a preview of the session, - * and resizing the guest to it would leave Steam laid out for a thumbnail. - */ private fun followWindow(width: Int, height: Int) { if (!SessionState.followScreen || isInPictureInPictureMode || !CompositorHost.isStarted) return val size = com.droiddeck.launcher.session.SessionDisplay.followSize(width, height) ?: return @@ -1477,18 +1313,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { WaylandCompositor.nativeResizeOutput(size.first, size.second) } - /** Whether this session's resolution is Follow screen; decided once, when it starts. */ private fun followChoice(): Boolean { val mode = SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM) return SessionPrefs.resolutionChoice(this, mode, com.droiddeck.launcher.session.SessionDisplay.panelSize(this)) == com.droiddeck.launcher.session.SessionDisplay.FOLLOW_SCREEN } - /** - * Follow screen on a large panel (a Fold's inner screen) may turn with the device: a window - * locked to landscape on a squarer panel is letterboxed by the system itself, which no output - * size can undo. Every other case keeps the landscape orientation picked in Setup. - */ private fun applyFollowOrientation(config: android.content.res.Configuration) { val wanted = if (SessionState.followScreen && config.smallestScreenWidthDp >= 600) { android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER @@ -1510,11 +1340,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { CompositorHost.detach(holder.surface) } - /** - * The size the session renders at. gamescope is told this and scales its output onto whatever - * the panel is, so a 1440p phone can run the client at 1080p without the client knowing. - */ - /** The panel, not the window: resources.displayMetrics is what is left after the system bars and the cutout are taken out. */ private fun panelBounds(): android.graphics.Rect = if (Build.VERSION.SDK_INT >= 30) { windowManager.maximumWindowMetrics.bounds } else { @@ -1528,7 +1353,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val mode = SessionPrefs.prefMode(intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM) val panel = com.droiddeck.launcher.session.SessionDisplay.panelSize(this) val program = intent.getStringExtra(SessionService.EXTRA_PROGRAM) - // Lightweight emulators keep their panel-sized default until a resolution is chosen. val choice = if (intent.getStringExtra(SessionService.EXTRA_MODE) == SessionService.MODE_RUN && Library.drawsAtPanel(program) && !SessionPrefs.resolutionChosen(this, mode)) com.droiddeck.launcher.session.SessionDisplay.MATCH_SCREEN @@ -1536,13 +1360,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return com.droiddeck.launcher.session.SessionDisplay.resolveChoice(panel, choice) } - /** - * The rate of the mode PerfMode asked for, not the one the panel is still in: the switch lands - * some frames after onCreate, and read too early it gave gamescope `-r 60` on a 120 Hz panel - - * the client and every game then paced themselves to 60. (WinNative's requestedPanelRefreshRate.) - * Never above the user's peak refresh rate, which Android holds the panel to whatever mode is - * active; it is the rate gamescope gives Steam, and the QAM's Frame Limit divides it. - */ private fun refreshHz(): Float { val display = if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay val wanted = window.attributes.preferredDisplayModeId @@ -1552,10 +1369,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return if (capped > 1f) capped else 60f } - /** - * The saved frame-generation setting, pushed to the compositor. Off the main thread: LSFG's - * first use translates the shader chain out of Lossless.dll, which takes seconds. - */ private fun applyFrameGen() { val hz = refreshHz() Thread({ @@ -1568,7 +1381,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }, "frame-gen").start() } - /** A Lossless Scaling installed or updated in Steam during the session is taken when the drawer opens. */ private fun syncLossless() { Thread({ Lossless.sync(this) @@ -1589,9 +1401,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }, "lossless-import").start() } - // ── Session state ─────────────────────────────────────────────────────────────────────── - /** Keeps the loading overlay current from the session log, and its clock moving. */ private fun watchSession() { val handler = Handler(Looper.getMainLooper()) var ticks = 0 @@ -1625,10 +1435,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (status == 0) { runOnUiThread { if (isFinishing || isDestroyed) return@runOnUiThread - // A session asked for in this one's place (the desktop's Steam launchers or - // Steam's Switch to Desktop) starts here: this activity restarts on its intent, as an agent - // start does. startActivity(next) could not do it - the activity is singleTop, so - // the request landed in this instance's onNewIntent and went with its finish(). SessionState.relaunch?.let { next -> SessionState.relaunch = null next.putExtra(EXTRA_RETURN_HOME, intent.getBooleanExtra(EXTRA_RETURN_HOME, false)) @@ -1640,7 +1446,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } return } - // What the log says about why, read off the main thread (notifyEnded arrives on it). Thread({ val hint = sessionEndHint(SessionState.logFile) runOnUiThread { @@ -1655,7 +1460,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { finish() return } - // It stays until the user leaves: a failure read against a timer is a failure not read. val stopped = when (SessionState.mode) { SessionService.MODE_RUN -> com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?.let { getString(R.string.session_ended_named, it) } ?: getString(R.string.session_ended_program) @@ -1669,14 +1473,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { focusEndedScreen() } - /** The ended screen's buttons take the pad: it is no longer driving a game. */ private fun focusEndedScreen() { drawerOpen = false padBridge?.releaseAll() uiHandler.post { sessionOverlay.requestFocus() } } - /** The same start again, in a fresh activity: the ended one keeps no session to reuse. */ private fun retrySession() { val again = Intent(intent) closeAtOnce = true @@ -1684,13 +1486,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { startActivity(again) } - /** - * One line of advice for a failure the log identifies, or null. The one this recognises: the - * ENOSYS storm - dozens of `socket(): Function not implemented` / `shared memfd open() failed` - * lines - which is proot's seccomp acceleration failing an x86 helper (Steam's xalia under - * FEX) on some devices (a Fold 5, twice). The switch that answers it is in Performance, and a - * user who never opens the log would not know. - */ private class EndHint(val text: String, val evenOnSuccess: Boolean = false) private fun sessionEndHint(log: File?): EndHint? { @@ -1698,7 +1493,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return try { var enosys = 0 var fexMissing = false - // The tail is where a dying session says why; 512 KB covers the storm without reading a 1 GB log. val size = log.length() java.io.RandomAccessFile(log, "r").use { f -> val start = maxOf(0L, size - 512L * 1024) @@ -1722,11 +1516,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - // ── Input ─────────────────────────────────────────────────────────────────────────────── override fun dispatchKeyEvent(event: KeyEvent): Boolean { - // The device's own volume keys belong to Android: forwarded to the guest as keys they - // changed nothing anyone could hear (a Pocket FIT report, on the desktop). when (event.keyCode) { KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE -> return super.dispatchKeyEvent(event) @@ -1737,7 +1528,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (drawerOpen && !drawerControllerActive) sessionOverlay.requestFocus() drawerControllerActive = true } - // While the session starts, B is the loading screen's Cancel, as its corner says. if (fromController && event.keyCode == KeyEvent.KEYCODE_BUTTON_B && ::loading.isInitialized && loading.visible && !loading.ended && !drawerOpen) { if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) { SessionService.stop(this) @@ -1745,7 +1535,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } return true } - // The ended screen is a page of buttons: the pad drives them, and B leaves. if (::loading.isInitialized && loading.ended) { if (event.keyCode == KeyEvent.KEYCODE_BUTTON_B || event.keyCode == KeyEvent.KEYCODE_BACK) { if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) finish() @@ -1786,27 +1575,16 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } if ((pcKeyboardOpen || achievementsOpen) && fromController) return super.dispatchKeyEvent(event) if (event.keyCode != KeyEvent.KEYCODE_BACK && padBridge?.onKeyEvent(event) == true) return true - // A hardware keyboard, forwarded to the compositor's wl_keyboard. Back is left to the - // activity, which opens the drawer. if (CompositorHost.isStarted && event.keyCode != KeyEvent.KEYCODE_BACK && !fromController) { val down = event.action == KeyEvent.ACTION_DOWN if (down || event.action == KeyEvent.ACTION_UP) { var evdev = EvdevKeys.fromKeyCode(event.keyCode) - // What the key stands for, and the key it would sit on without Shift - non-zero - // only for a character Shift puts there: every capital and the symbol row. val ch = event.unicodeChar val plain = if (ch > 0) EvdevKeys.unshiftedChar(ch) else 0 - // A soft keyboard's symbol keys are in neither the table nor a scan code, so they - // reached the session as nothing at all - a sign-in took an address without its @. - // Work back from the character instead: which key carries it. if (evdev <= 0 && ch > 0) { val code = EvdevKeys.fromKeyCode(EvdevKeys.keycodeForChar(if (plain != 0) plain else ch)) if (code > 0) evdev = code } - // The modifier has to be made here. A soft keyboard reports Shift in the event's - // meta state and sends no Shift key of its own, so a capital arrives as a key this - // side already knows and came out lowercase. A hardware keyboard sends its own - // Shift, and a second one would release the modifier while the key is still held. val shiftEvdev = if (evdev > 0 && plain != 0 && event.deviceId <= 0) 42 else 0 if (evdev <= 0 && event.scanCode > 0) evdev = event.scanCode if (evdev > 0) { @@ -1843,7 +1621,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return super.dispatchGenericMotionEvent(event) } - /** With no view focused, captured mouse events skip the root and arrive here as trackball events. */ override fun dispatchTrackballEvent(event: MotionEvent): Boolean = onCapturedPointer(event) || super.dispatchTrackballEvent(event) @@ -1895,7 +1672,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { uiHandler.postDelayed(drawerDirectionRepeat, drawerFirstRepeatDelayMs) } - /** Use Android's per-device flat region, then rescale the remaining stick travel to 0..1. */ private fun centeredStickAxis(event: MotionEvent, axis: Int): Float { val value = event.getAxisValue(axis) val flat = (event.device?.getMotionRange(axis, event.source)?.flat ?: 0f) @@ -1948,17 +1724,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { showCursor(cx, cy) } - /** - * The arrow stays on a desktop; in a Steam session it shows for a moment after each move. While - * a controller is driving (input in the last [CURSOR_PAD_HOLD_MS]) it stays hidden, so a touch - * during play does not flash it up - Bannerlator's rule for its Wayland pointer. - */ private fun showCursor(x: Float, y: Float) { cursorPos = androidx.compose.ui.geometry.Offset(x, y) syncClientCursor() uiHandler.removeCallbacks(cursorResync) uiHandler.postDelayed(cursorResync, 60) - // A program that asked for no pointer (a mouse-look game) wins over everything below. if (clientHidesCursor) { cursorVisible = false; return } if (android.os.SystemClock.uptimeMillis() - lastPadInputMs < CURSOR_PAD_HOLD_MS) return cursorVisible = true @@ -1966,14 +1736,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (SessionState.mode == SessionService.MODE_STEAM && SessionState.steamUi != "desktop") uiHandler.postDelayed(cursorHide, 2500) } - /** - * Pulls the program's pointer (wl_pointer.set_cursor) if it changed: labwc's resize arrows at a - * window's edges and corners, a text field's I-beam, or a request for no pointer. Serial 0 = no - * program has set one yet, and the built-in arrow stays. Returns true when something changed. - */ private fun syncClientCursor(): Boolean { if (!CompositorHost.isStarted) return false - // Asked on every pointer move; the image is copied only when the client has changed it. if (WaylandCompositor.nativeCursorSerial() == cursorSerial) return false val n = WaylandCompositor.nativeCursorSnapshot(cursorBuf) if (n < 6) return false @@ -1994,7 +1758,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { cursorImage = bitmap.asImageBitmap() cursorHotX = cursorBuf[4] cursorHotY = cursorBuf[5] - // Session pixels -> screen pixels, as the session's picture is scaled (movePointer). val width = surfaceView.width.takeIf { it > 0 } val height = surfaceView.height.takeIf { it > 0 } val out = SessionState.outputSize @@ -2016,7 +1779,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } if (selectedSecondScreenDisplay !in newIds) selectedSecondScreenDisplay = candidates.firstOrNull()?.id ?: -1 if (secondScreenMode != SecondScreenMode.NONE && candidates.isEmpty()) closeSecondScreen(reset = true) - // A choice kept while its display was away (asleep, waking): back as soon as it is. else if (started && secondScreenMode != SecondScreenMode.NONE && secondScreenPresentation == null) showSecondScreen(secondScreenMode) } @@ -2046,8 +1808,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (pipUi || !started) return val target = displayManager.getDisplay(selectedSecondScreenDisplay) if (target == null || !target.isValid || (target.flags and Display.FLAG_PRESENTATION) == 0) { - // Not there right now - a panel still waking, say. The choice is kept; the display - // listener shows it again when the display comes back (refreshSecondScreenDisplays). closeSecondScreen(reset = false) return } @@ -2098,13 +1858,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { applyUnfoldedControls() } - /** - * Unfolded controls: on a foldable's inner screen, the game above and the Steam Deck controller's - * trackpads and back grips below ([com.droiddeck.launcher.ui.DeckControlsPanel], as a second - * screen shows them, plus a whole gamepad). The on-screen pad is hidden meanwhile. The surface shrinks to the top part, so a Follow screen session resizes - * to it and any other is fitted into it. Folded onto the cover screen - a phone-sized panel - - * or in picture-in-picture, the game is full screen again. - */ private fun applyUnfoldedControls() { if (!::sessionRoot.isInitialized) return val width = sessionRoot.width @@ -2129,7 +1882,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (panel == null) { panel = com.droiddeck.launcher.ui.DeckControlsPanel(this, onClose = { chooseUnfoldedControls(false) }, onSteamMenu = ::tapSteamButton, onQam = ::tapQam, pad = padBridge) - // Just above the surface: the on-screen pad, keyboard and drawer still draw over it. sessionRoot.addView(panel, sessionRoot.indexOfChild(surfaceView) + 1) unfoldedPanel = panel updateOnScreenControls() @@ -2157,7 +1909,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - /** The touch mode in force: the game's own while Steam's touch controls are shown. */ private fun effectiveTouchMode(): String { val options = steamTouchOptions if (options == null || steamTouchControls?.visibility != View.VISIBLE) return SessionPrefs.touchMode(this) @@ -2181,14 +1932,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { routedTouchMode = next } - /** Touchpad on the desktop, direct in Steam, unless the drawer says otherwise. */ private fun usingTouchpad(): Boolean = when (routedTouchMode) { SessionPrefs.TOUCH_PAD -> true SessionPrefs.TOUCH_DIRECT -> false else -> SessionState.mode != SessionService.MODE_STEAM || SessionState.steamUi == "desktop" } - /** The picture's rectangle inside the view: where the pointer may go. */ private fun drawnRect(): android.graphics.RectF? { val width = surfaceView.width.takeIf { it > 0 } ?: return null val height = surfaceView.height.takeIf { it > 0 } ?: return null @@ -2203,7 +1952,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { override fun onTouchEvent(event: MotionEvent): Boolean { if (event.isFromSource(android.view.InputDevice.SOURCE_MOUSE)) return onMouse(event) - // Child views get the event first, so menus, keyboards and the on-screen pad still work. if (routedTouchMode == SessionPrefs.TOUCH_OFF) return true if (usingTouchpad()) { val rect = drawnRect() ?: return false @@ -2222,8 +1970,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { InputSpace.x(x, surfaceView.width), InputSpace.y(y, surfaceView.height)) } when (event.actionMasked) { - // A first finger starts a new gesture, and Android has ended every earlier one: whatever - // the compositor still holds (an up lost to the drawer opening mid-touch) goes first. MotionEvent.ACTION_DOWN -> { WaylandCompositor.nativeSendTouch(3, -1, 0, 0) listOf(PointerGestures.BTN_LEFT, PointerGestures.BTN_RIGHT, PointerGestures.BTN_MIDDLE).forEach { @@ -2240,8 +1986,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return true } - /** A mouse: hover moves, buttons press, the wheel scrolls. Android sends buttons as touch - * actions and hover as generic motion, so both paths land here. */ private fun onMouse(event: MotionEvent): Boolean { when (event.actionMasked) { MotionEvent.ACTION_HOVER_MOVE, MotionEvent.ACTION_MOVE, MotionEvent.ACTION_HOVER_ENTER -> @@ -2259,19 +2003,17 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val v = event.getAxisValue(MotionEvent.AXIS_VSCROLL) if (v != 0f) WaylandCompositor.nativeSendSceneInput(4, -Math.round(v), 0) } - MotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP -> {} // the BUTTON_* events carry these + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP -> {} else -> return false } return true } - /** A physical mouse, not a touchscreen, stylus or the system's own virtual devices. */ private fun mouseAttached(): Boolean = InputDevice.getDeviceIds().any { id -> val d = InputDevice.getDevice(id) d != null && !d.isVirtual && d.supportsSource(InputDevice.SOURCE_MOUSE) } - /** Capture the mouse exactly while a program holds a lock and the session has the mouse. */ private fun updatePointerCapture() { if (!::surfaceView.isInitialized) return val view = window.decorView @@ -2280,10 +2022,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (want) view.requestPointerCapture() else view.releasePointerCapture() } - /** - * A captured mouse: raw motion as relative pointer deltas, buttons and the wheel as on the - * uncaptured path. Anything else (a captured touchpad's absolute fingers) is left alone. - */ private fun onCapturedPointer(event: MotionEvent): Boolean { if (!pointerLocked || !event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE)) return false when (event.actionMasked) { @@ -2308,14 +2046,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val v = event.getAxisValue(MotionEvent.AXIS_VSCROLL) if (v != 0f) WaylandCompositor.nativeSendSceneInput(4, -Math.round(v), 0) } - MotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP -> {} // the BUTTON_* events carry these + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP -> {} else -> return false } return true } - /** The mode decides which controls appear; `droiddeck-osc` in Downloads still overrides. */ - /** Which controller the session has, so a pad report says what was plugged in and when. */ private fun logInputDevice(what: String, deviceId: Int) { val d = InputDevice.getDevice(deviceId) ?: return if (!PadBridge.isFromController(d)) return @@ -2323,12 +2059,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { what, d.name, d.vendorId, d.productId, d.id, d.sources)) } - /** Steam's touch controls, once the session has made the device (SessionService). */ private fun steamTouch(): com.droiddeck.launcher.input.SteamTouchControls? { val device = com.droiddeck.launcher.input.SteamTouchDevice.current ?: return null steamTouchControls?.let { view -> if (view.device === device) return view - // A new session made a new device: the view of the old one goes. view.releaseAll() (view.parent as? android.view.ViewGroup)?.removeView(view) steamTouchControls = null @@ -2341,13 +2075,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { steamTouchOptions = options applyTouchRouting() } - // Paste: the clipboard is shared with the session's X servers; Ctrl+V types it. view.onPaste = { - sendGuestKey(29, true); sendGuestKey(47, true) // KEY_LEFTCTRL, KEY_V + sendGuestKey(29, true); sendGuestKey(47, true) sendGuestKey(47, false); sendGuestKey(29, false) } - // The client reads a controller's config when it connects: after a save, plug the device - // out and back in so it takes the new one. view.onSaved = { device.plugged = false uiHandler.postDelayed({ updateOnScreenControls() }, 700) @@ -2364,8 +2095,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { ?.let { FileUtils.readString(it)?.trim()?.lowercase() } ?: SessionPrefs.oscMode(this) val controls = onScreenControls ?: return - // In picture-in-picture, or with the unfolded screen's gamepad below the game, the on-screen - // pad would only cover the picture or that panel. if (pipUi || unfoldedPanel != null) { onScreenButtonsVisible = false controls.releaseAll() @@ -2375,12 +2104,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { return } val buttonsOnly = forced == SessionPrefs.OSC_STEAM_QAM - // Steam's touch controller stands in for the app's pad, and is plugged into the client only - // while it is shown - as Steam Link withdraws its controls when a physical pad is in use. val wantsSteam = forced == SessionPrefs.OSC_STEAM_TOUCH && SessionState.mode == SessionService.MODE_STEAM val steam = if (wantsSteam) steamTouch() else null - // The service makes the device while the session starts, after this activity: the app's - // own pad until then, and another look in a moment. if (wantsSteam && steam == null && !isFinishing && !isDestroyed) { uiHandler.removeCallbacks(steamTouchRetry) uiHandler.postDelayed(steamTouchRetry, 1000) @@ -2394,13 +2119,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { SessionPrefs.OSC_ALWAYS -> true SessionPrefs.OSC_STEAM_QAM -> true SessionPrefs.OSC_NEVER -> false - // Shown throughout a Steam session: a handheld's built-in pad would keep Auto off for good. - // Outside Steam it is Auto. SessionPrefs.OSC_STEAM_TOUCH -> SessionState.mode == SessionService.MODE_STEAM || (!PadBridge.anyControllerConnected() && SessionState.mode != SessionService.MODE_DESKTOP) - // Auto: the touch pad when there is no controller - except on the desktop, where the - // screen is a touchpad for the pointer and a pad over it would be in the way. A game - // started from the rail, or Steam, gets it; the drawer turns it on anywhere. else -> !PadBridge.anyControllerConnected() && !SteamControllerBle.connected && SessionState.mode != SessionService.MODE_DESKTOP } @@ -2486,7 +2206,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { override fun onPictureInPictureModeChanged(inPip: Boolean, newConfig: android.content.res.Configuration) { super.onPictureInPictureModeChanged(inPip, newConfig) if (::pip.isInitialized) pip.changed(inPip) - // The panel may have changed while the session was a thumbnail (folded or unfolded in PiP). if (!inPip && surfaceW > 0) followWindow(surfaceW, surfaceH) applyUnfoldedControls() } @@ -2498,7 +2217,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { super.startActivityForResult(intent, requestCode, options) } - // ── Lifecycle ─────────────────────────────────────────────────────────────────────────── override fun onStart() { super.onStart() @@ -2514,8 +2232,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (runtimeRemovalBlocked) { super.onStop(); return } started = false displayManager.unregisterDisplayListener(displayListener) - // Closed while the session is out of sight (sleep, a closed lid, another app), and kept: - // onStart puts it back. closeSecondScreen(reset = false) SessionService.setActivityVisible(this, false) super.onStop() @@ -2532,7 +2248,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { recreate() return } - // Home/notification resumes must retain the external frontend's return destination. if (this.intent.getBooleanExtra(EXTRA_RETURN_HOME, false)) intent.putExtra(EXTRA_RETURN_HOME, true) setIntent(intent) if (intent.action == SessionService.ACTION_HOME_GUIDE) { @@ -2572,14 +2287,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { override fun onPause() { if (runtimeRemovalBlocked) { super.onPause(); return } sessionClipboard.stop() - // Do not carry transient session UI across an app/display transition. In particular, the - // drawer's dim layer can otherwise remain over Steam when this activity returns. drawerOpen = false pcKeyboardOpen = false achievementsOpen = false (getSystemService(INPUT_SERVICE) as? InputManager)?.unregisterInputDeviceListener(deviceListener) releaseDrawerDirection() - // A button held when the app goes away would stay held in the ring for the whole session. onScreenControls?.releaseAll() resumed = false updatePointerCapture() @@ -2590,8 +2302,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { super.onPause() } - /** SteamVR for Cardboard mode: install, start or stop it in the guest. Starting switches the - * picture to side-by-side, which is what SteamVR draws. */ private fun steamVrAction(action: String) { if (action == "start") { switchPictureForSteamVr() @@ -2603,7 +2313,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { val ok = SteamVr.run(this, action) val now = SteamVr.status(this) uiHandler.post { - // A start that failed leaves nothing side-by-side to show: the picture goes back. if (!ok && action == "start" && now != SteamVr.Status.RUNNING) restoreSteamVrPicture() onSteamVrStatus(now) if (!ok) Toast.makeText(this, getString(R.string.cardboard_steamvr_failed), Toast.LENGTH_LONG).show() @@ -2611,7 +2320,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { }, "steamvr-$action").start() } - /** SteamVR draws a side-by-side pair: the picture switches to that, remembering what it was. */ private fun switchPictureForSteamVr() { if (cardboard.source != Cardboard.SOURCE_SBS) { SessionPrefs.setCardboardSourceBeforeSteamVr(this, cardboard.source) @@ -2624,7 +2332,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private fun onSteamVrStatus(now: SteamVr.Status) { steamVrStatus = now - // Started some other way than the drawer (a script, a VR game): the picture follows it all the same. if (now == SteamVr.Status.RUNNING && cardboard.enabled && !steamVrSwitchedPicture && cardboard.source != Cardboard.SOURCE_SBS) { switchPictureForSteamVr() @@ -2635,7 +2342,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { else if (steamVrSeenRunning && now != SteamVr.Status.UNAVAILABLE) restoreSteamVrPicture() } - /** SteamVR stopped: the Cardboard picture goes back to what it was before Start. */ private fun restoreSteamVrPicture() { val before = SessionPrefs.cardboardSourceBeforeSteamVr(this) SessionPrefs.setCardboardSourceBeforeSteamVr(this, -1) @@ -2647,7 +2353,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { applyCardboard(c) } - /** Cardboard mode: hand the settings to the compositor and run the head tracker while it needs one. */ private fun applyCardboard(c: Cardboard) { val was = cardboard cardboard = c @@ -2657,8 +2362,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } private fun updateHeadTracker() { - // Side-by-side sources track too: the compositor leaves their picture alone, but whoever renders - // it (the SteamVR driver, tools/vr) reads the pose from the shared file. val want = resumed && cardboard.enabled && cardboard.headTracking if (want) { val t = headTracker ?: HeadTracker(this) { @@ -2671,7 +2374,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - /** The panel's physical size in millimetres, long side first, for lining the eyes up with the lenses. */ private fun panelSizeMm(): Pair<Float, Float> { val dm = resources.displayMetrics val landscape = dm.widthPixels >= dm.heightPixels @@ -2697,11 +2399,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { goFullscreen() refreshClipboard() } - // Android drops a capture with the focus; a lock still held takes it again on return. updatePointerCapture() } - /** The keyboard/trackpad presentation can own focus instead of the main display. */ fun refreshClipboard() { if (resumed) sessionClipboard.refresh() } @@ -2714,8 +2414,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) - // ALWAYS from Android 11: SHORT_EDGES left a black strip along a camera on the long edge, - // which is where a Fold's inner-screen camera sits with the device held in landscape. if (Build.VERSION.SDK_INT >= 28) { window.attributes = window.attributes.apply { layoutInDisplayCutoutMode = if (Build.VERSION.SDK_INT >= 30) @@ -2725,7 +2423,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } - /** Ours, so a finishing activity never unhooks the one that replaced it. */ private val endListener: (Int) -> Unit = { status -> onSessionEnded(status) } private val firstFrameListener = Runnable { SessionEvents.firstFrame() @@ -2739,8 +2436,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { override fun onDestroy() { uiHandler.removeCallbacks(steamTouchRetry) if (runtimeRemovalBlocked) { super.onDestroy(); return } - // Deliberately does NOT end the session: this activity can be destroyed while the user is - // in another app, and the whole point of the service is that Steam survives that. watching = false releaseDrawerDirection() pendingBackAction?.let(uiHandler::removeCallbacks) @@ -2759,7 +2454,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { super.onDestroy() } - /** The screen is all blue: the session stops and the front end opens on the same blue (FloodReturn). */ private fun finishFlooded() { if (isFinishing) return val signal = com.droiddeck.launcher.ui.Themes.byId(SessionPrefs.theme(this)).signal @@ -2770,20 +2464,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { finish() } - /** - * Every way out on screen (backing out of the loading screen, closing the ended screen, the - * session ending) leaves as a session arrives, in reverse: the blue spreads out of the - * throbber's ball, or the middle of the screen once a game is up, and the front end opens on it - * and draws it back into the button the session came from. Off screen, or with animations - * off, it just closes. - */ override fun finish() { if (isFinishing) return if (!quitFlooded && !closeAtOnce && com.droiddeck.launcher.ui.Motion.scale != 0f && lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.RESUMED)) { if (!leaving) { leaving = true - // Should the flood never get its frames, the session still closes. uiHandler.postDelayed({ completeLeave() }, LEAVE_TIMEOUT_MS) } return @@ -2799,7 +2485,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { else overridePendingTransition(R.anim.session_hold, R.anim.session_sink) } - /** The screen is all blue: close onto the front end, which opens on the same blue. */ private fun completeLeave() { if (isFinishing) return val signal = com.droiddeck.launcher.ui.Themes.byId(SessionPrefs.theme(this)).signal @@ -2811,7 +2496,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { companion object { const val EXTRA_RETURN_HOME = "returnHome" - /** How long the leaving flood may take before the session closes without it. */ private const val LEAVE_TIMEOUT_MS = 1_500L private const val TAG = "SessionActivity" private const val FEX_MISSING = "droiddeck-fex: x86 Linux programs need" @@ -2825,7 +2509,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { private const val DRAWER_SLOW_REPEAT_FLOOR_MS = 240L private const val DRAWER_FAST_REPEAT_INTERVAL_MS = 90L private const val DRAWER_REPEAT_ACCELERATION_MS = 1_400L - /** Compositor scale modes (Container.FULLSCREEN_* values): 1 = fit with bars, centred. */ private const val SCALE_FIT = 1 private const val ALIGN_CENTER = 0 private val RUNTIME_LINES = PackageLines( @@ -2846,5 +2529,4 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { } } -/** [progress]: (unlocked, total) from the cached list. */ private class AchievementsInFront(val game: StoreAchievements.Game, val title: String, val progress: Pair<Int, Int>?) diff --git a/app/src/main/java/com/droiddeck/launcher/UpdatesMenu.kt b/app/src/main/java/com/droiddeck/launcher/UpdatesMenu.kt index a55ac1930..8459bc7d9 100644 --- a/app/src/main/java/com/droiddeck/launcher/UpdatesMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/UpdatesMenu.kt @@ -18,13 +18,7 @@ import com.droiddeck.launcher.ui.UpdatesState import com.droiddeck.launcher.update.AppUpdates import com.droiddeck.launcher.update.SelfInstaller -/** - * The Updates page's state: the channels' latest builds, the one followed, and an update being - * downloaded or installed. Checked when the launcher comes to the front - opening the app, and - * coming back from a session - at most once a minute. - */ internal class UpdatesMenu(private val activity: Activity, private val ui: Handler) { - // Read in start(): the activity has no context yet while its fields are made. var catalog by mutableStateOf<AppUpdates.Catalog?>(null) var follow by mutableStateOf(AppUpdates.Follow(AppUpdates.Channel.STABLE)) var checking by mutableStateOf(false) @@ -32,7 +26,6 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl var stage by mutableStateOf<String?>(null) var percent by mutableIntStateOf(-1) var askPermission by mutableStateOf(false) - // An install waiting for the user to allow "Install unknown apps"; it goes on when they come back. private var pending: AppUpdates.Release? = null private var lastCheck = 0L private var request = 0 @@ -53,7 +46,6 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl val hasUpdate: Boolean get() = AppUpdates.hasUpdate(catalog, follow) - /** On every return to the launcher: go on with an install the user just allowed, and check if due. */ fun onResume() { pending?.let { r -> if (SelfInstaller.canInstall(activity)) { pending = null; install(r) } @@ -107,7 +99,6 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl }, "app-update-install").start() } - /** PackageInstaller's answer: Android's confirm prompt to show, or why it failed. Success restarts the app. */ private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) { @@ -127,7 +118,6 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl } } - /** From onCreate: the last check, the channel followed, and PackageInstaller's answers. */ fun start() { catalog = AppUpdates.cached(activity) follow = AppUpdates.follow(activity, catalog) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentAccess.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentAccess.kt index b6828c742..b755ca646 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentAccess.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentAccess.kt @@ -2,26 +2,16 @@ package com.droiddeck.launcher.agent import android.content.Context -/** - * Who may run code through the agent bridge. Reading state, starting and stopping sessions, - * launching and quitting games and sending input only need the shell (the provider's DUMP - * permission). Running commands in the guest, evaluating JavaScript in the Steam client, session - * environment, binary overrides and preference writes reach inside the app's sandbox - the guest - * holds the Steam login - which the shell of a stock device otherwise cannot. They require - * Setup > Diagnostics > Debugging tools to be on, in every build. The bridge itself cannot turn it on. - */ object AgentAccess { const val MAX_REQUEST_CHARS = 256 * 1024 private const val PREFS = "agent" private const val KEY_COMMANDS = "commands" - /** The preferences file the bridge must never write: set-pref refuses it. */ const val PROTECTED_PREFS = PREFS fun commandsAllowed(context: Context): Boolean = prefs(context).getBoolean(KEY_COMMANDS, false) - /** The Setup toggle's own state. */ fun commandsEnabled(context: Context): Boolean = prefs(context).getBoolean(KEY_COMMANDS, false) fun setCommandsEnabled(context: Context, enabled: Boolean) { @@ -44,5 +34,4 @@ object AgentAccess { private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) } -/** A refusal with a stable code, answered as {"ok": false, "error": {code, message}}. */ class AgentException(val code: String, message: String) : RuntimeException(message) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentBridgeProvider.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentBridgeProvider.kt index 2b8ba775b..4294e5ad9 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentBridgeProvider.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentBridgeProvider.kt @@ -27,14 +27,6 @@ import java.io.File import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -/** - * The shell's control surface (docs/agent-control.md), in every build: the provider and its start - * Activity need android.permission.DUMP, held by the shell and privileged or explicitly granted callers. Commands that - * reach into the app's sandbox are further gated by [AgentAccess]. - * - * A method's request is JSON, base64-encoded in the "request" extra; every answer is JSON in the - * "json" key of the returned Bundle, {"ok": true, ...} or {"ok": false, "error": {code, message}}. - */ class AgentBridgeProvider : ContentProvider() { override fun onCreate(): Boolean { val appContext = context?.applicationContext ?: return false @@ -58,7 +50,6 @@ class AgentBridgeProvider : ContentProvider() { val response = try { val request = decode(extras?.getString(EXTRA_REQUEST)) - // Binder identity is the shell's; everything below acts as the app. val token = Binder.clearCallingIdentity() try { dispatch(context, method, request) @@ -136,8 +127,6 @@ class AgentBridgeProvider : ContentProvider() { .put("status", SessionState.failureStatus ?: JSONObject.NULL) session.put("failure", failure) - // gamescope's focus as last published by the guest agent, without the window lists - // (the focus command returns those, fresh). val focus = AgentGuest.focus(context)?.let { snap -> JSONObject() .put("focusedApp", snap.opt("focusedApp") ?: JSONObject.NULL) @@ -225,11 +214,9 @@ class AgentBridgeProvider : ContentProvider() { return result.put("ok", true).put("command", "prefs") } - /** A guest agent request; its own answer already carries ok or error. */ private fun guest(context: Context, request: JSONObject, timeoutMs: Long): JSONObject = AgentGuest.call(context, request, timeoutMs) - /** The guest's own timeout plus slack for the round trip. */ private fun timeoutMs(request: JSONObject, defaultSeconds: Double, slackMs: Long): Long { val seconds = if (request.has("timeout")) request.opt("timeout")?.toString()?.toDoubleOrNull() ?: throw AgentException("INVALID_REQUEST", "timeout must be a number") else defaultSeconds @@ -262,7 +249,6 @@ class AgentBridgeProvider : ContentProvider() { companion object { const val RESULT_JSON = "json" const val EXTRA_REQUEST = "request" - /** 2 added agent and session.focus; everything in 1 is unchanged. */ const val SCHEMA = 2 private val recoveryComplete = CountDownLatch(1) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentEnv.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentEnv.kt index 83ea0ccd1..5f7f09262 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentEnv.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentEnv.kt @@ -10,18 +10,6 @@ import java.nio.file.Files import java.nio.file.LinkOption.NOFOLLOW_LINKS import java.nio.file.StandardOpenOption.READ -/** - * Experiment switches an agent sets without touching Download/droiddeck-env or rebuilding the apk: - * - * - environment lines (KEY=VALUE) for the session, applied after the user's own droiddeck-env, so - * they win. "next" lines are taken by the next session that starts and dropped when it ends; - * "persistent" ones stay until cleared. - * - binary overrides: a file pushed to the app's external files (agent-inbox/<name>) is installed - * as /opt/droiddeck-agent/bin/<name> in the rootfs and put first on the next session's PATH - - * a gamescope build under test, say. Removed when that session ends. - * - * Kept in the app's own files (agent-env.json), never in a place the shell can write. - */ object AgentEnv { private const val TAG = "AgentEnv" private const val FILE = "agent-env.json" @@ -38,7 +26,6 @@ object AgentEnv { load(context) } - /** Add [lines] for the next session ([persistent] = every session until cleared). */ fun set(context: Context, lines: List<String>, persistent: Boolean): JSONObject = synchronized(lock) { AgentAccess.requireCommands(context) if (lines.any { line -> KEY.matchEntire(line.substringBefore('=', "")) == null || !line.contains('=') || line.any { it == '\n' || it == '\r' || it == '\u0000' } }) { @@ -52,7 +39,6 @@ object AgentEnv { state } - /** Drop lines: [scope] is "next", "persistent" or "all". */ fun clear(context: Context, scope: String): JSONObject = synchronized(lock) { AgentAccess.requireCommands(context) val state = load(context) @@ -66,7 +52,6 @@ object AgentEnv { state } - /** Install agent-inbox/[name] as an override for the next session. */ fun override(context: Context, name: String): JSONObject = synchronized(lock) { AgentAccess.requireCommands(context) if (NAME.matchEntire(name) == null || name in setOf(".", "..")) throw AgentException("INVALID_NAME", "Override names are letters, digits, '.', '_' and '-'") @@ -93,10 +78,6 @@ object AgentEnv { state.put("installed", "$GUEST_BIN/$name") } - /** - * At session start: the lines this session gets, after the user's droiddeck-env. Pending lines - * and overrides become this session's own, to be dropped by [endSession]. - */ fun beginSession(context: Context): List<String> = synchronized(lock) { if (!AgentAccess.commandsAllowed(context)) { revoke(context) @@ -108,7 +89,6 @@ object AgentEnv { state.put("active", JSONArray(active)).put("pending", JSONArray()) state.put("activeOverrides", JSONArray(overrides)).put("pendingOverrides", JSONArray()) save(context, state) - // Anything else there was left by a session that never reached endSession (the app died). bin(context).listFiles() ?.filter { it.name !in overrides }?.forEach { it.delete() } val lines = strings(state.optJSONArray("persistent")) + active @@ -124,7 +104,6 @@ object AgentEnv { bin(context).listFiles()?.forEach { it.delete() } } - /** At session end: this session's lines and override binaries go. */ fun endSession(context: Context) = synchronized(lock) { val state = load(context) val overrides = strings(state.optJSONArray("activeOverrides")) @@ -135,7 +114,6 @@ object AgentEnv { save(context, state) } - /** A PATH= line among [lines] (the user's or an agent's) is extended rather than replaced. */ private fun pathOf(lines: List<String>) = lines.lastOrNull { it.startsWith("PATH=") }?.substringAfter('=')?.takeIf { it.isNotBlank() } ?: BASE_PATH @@ -178,7 +156,6 @@ object AgentEnv { } } -/** Typed writes to the app's SharedPreferences, for driving a settings matrix without the UI. */ object AgentPrefs { fun set(context: Context, file: String, key: String, type: String, value: String?): JSONObject { AgentAccess.requireCommands(context) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentGuest.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentGuest.kt index c572e943d..3b265ebb9 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentGuest.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentGuest.kt @@ -15,11 +15,6 @@ import org.json.JSONObject import java.io.File import java.util.UUID -/** - * The app's side of the guest agent (tools/linuxfs/overlay/usr/local/bin/droiddeck-agent): requests - * go in as files under the session root's agent directory and answers come back the same way, so - * whatever runs does so inside the live session with its own environment. - */ object AgentGuest { private val main by lazy { Handler(Looper.getMainLooper()) } var focusRevision by mutableIntStateOf(0) @@ -32,11 +27,6 @@ object AgentGuest { fun dir(context: Context) = File(LinuxRuntime.sessionRoot(context), "agent") - /** - * Before each session: nothing of the last one's daemon, answers or focus may be read as - * current. Then watch for the new daemon's hello and focus snapshots, which go into the - * session's events.jsonl. - */ fun reset(context: Context) { synchronized(lock) { observer?.stopWatching() @@ -59,7 +49,6 @@ object AgentGuest { } } - /** At the end of a session: no more events for a folder that is being closed. */ fun stop() { synchronized(lock) { observer?.stopWatching() @@ -82,7 +71,6 @@ object AgentGuest { SessionEvents.record("focus.changed", fields + ("seq" to snapshot.optInt("seq"))) } - /** The daemon's hello, when it is running in the current session. */ fun hello(context: Context): JSONObject? { if (!SessionState.running) return null val hello = readJson(File(dir(context), "hello.json")) ?: return null @@ -90,18 +78,15 @@ object AgentGuest { return hello.takeIf { pid > 1 && File("/proc/$pid").exists() } } - /** The newest focus snapshot the daemon published, when a session is running. */ fun focus(context: Context): JSONObject? = if (SessionState.running) readJson(File(dir(context), "focus.json")) else null - /** The Steam library game whose tile currently has controller or keyboard focus. */ fun selectedSteamAppId(context: Context): Long? { val result = call(context, JSONObject().put("kind", "steam-selection"), 6_000) if (!result.optBoolean("ok") || result.isNull("appId")) return null return result.optLong("appId").takeIf { it in 1..4_294_967_295L } } - /** The installed game Gamescope is presenting, including while Steam's overlay has focus. */ fun activeSteamAppId(context: Context, installedAppIds: Set<Long>): Long? { val snapshot = focus(context) ?: return null val focusedAppId = snapshot.optLong("focusedApp").takeIf { snapshot.has("focusedApp") } @@ -151,7 +136,6 @@ object AgentGuest { return null } - /** Send one request and wait for its answer. Throws [AgentException] with a stable code. */ 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") diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentInput.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentInput.kt index e2c8674b4..94cf53388 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentInput.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentInput.kt @@ -5,13 +5,7 @@ import com.droiddeck.launcher.session.SessionState import com.droiddeck.launcher.wayland.WaylandCompositor import org.json.JSONObject -/** - * Input into the running session the way a player gives it: keys and touches through the - * compositor (what the session screen sends), and the virtual pad through the same PadBridge the - * on-screen controls drive, so games that ignore touch still see a controller. - */ object AgentInput { - /** Out of the way of real fingers, which Android numbers from 0. */ private const val TOUCH_ID = 9 private const val TOUCH_SCALE_X = 1919f private const val TOUCH_SCALE_Y = 1079f @@ -56,7 +50,6 @@ object AgentInput { return JSONObject().put("code", code) } - /** Typed as key presses (US layout), which reaches X programs that have no text-input support. */ private fun text(request: JSONObject): JSONObject { val text = request.optString("text") if (request.optBoolean("commit")) { @@ -99,11 +92,6 @@ object AgentInput { return JSONObject().put("from", org.json.JSONArray().put(x1).put(y1)).put("to", org.json.JSONArray().put(x2).put(y2)) } - /** - * A point in the compositor's touch space (0..1919 x 0..1079 whatever the session's size), from - * the session's output pixels (space "output", the default: what the guest draws at) or 0..1 - * fractions (space "fraction"). - */ private fun point(request: JSONObject, xKey: String, yKey: String): Pair<Int, Int> { if (!request.has(xKey) || !request.has(yKey)) throw AgentException("INVALID_INPUT", "$xKey and $yKey are required") val x = request.getDouble(xKey) @@ -183,7 +171,6 @@ object AgentInput { "guide" to PadState.GUIDE, "steam" to PadState.GUIDE, ) - /** Linux evdev codes (input-event-codes.h) by name. */ private val KEYS: Map<String, Int> = buildMap { "1234567890".forEachIndexed { i, c -> put(c.toString(), 2 + i) } "QWERTYUIOP".forEachIndexed { i, c -> put(c.toString(), 16 + i) } @@ -202,7 +189,6 @@ object AgentInput { )) } - /** US-layout characters as (evdev code, shift). */ private val TYPED: Map<Char, Pair<Int, Boolean>> = buildMap { for (c in 'a'..'z') put(c, KEYS.getValue(c.uppercase()) to false) for (c in 'A'..'Z') put(c, KEYS.getValue(c.toString()) to true) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentStartActivity.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentStartActivity.kt index edf38dde6..9b9d85bbd 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentStartActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentStartActivity.kt @@ -10,7 +10,6 @@ import com.droiddeck.launcher.session.SessionService import org.json.JSONArray import org.json.JSONObject -/** Shell-launched trampoline: Android permits the shell to start this protected debug Activity. */ class AgentStartActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentUi.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentUi.kt index 6153da0f3..b6179e07b 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentUi.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentUi.kt @@ -19,16 +19,8 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference -/** - * The app's own UI as an agent sees it: every Compose node in every window of the process (popups - * and menus included), read from Compose's semantics rather than through UI Automator, which on a - * dual-screen device dumps whichever window has focus - often not this app's. Nodes carry their - * test tag (ui/TestTags.kt), text, content description, role, state and on-screen bounds; click - * and set-text act on a node by tag or text, so a test needs no coordinates. - */ object AgentUi { private const val TIMEOUT_MS = 5_000L - /** Setup's Debugging tools toggle: the bridge never acts on its own gate (AgentAccess). */ private const val GATE_TAG = "setting-agent-commands" fun handle(request: JSONObject): JSONObject = when (val op = request.optString("op", "dump")) { @@ -106,10 +98,6 @@ object AgentUi { return json } - /** - * The node a request names by "tag" (exact) or "match" (text or description containing it, - * ignoring case); "index" picks among several, in tree order. - */ private fun act(request: JSONObject, action: (SemanticsNode) -> Unit): JSONObject { val tag = request.optString("tag").takeIf { it.isNotEmpty() } val text = if (tag == null) request.optString("match").takeIf { it.isNotEmpty() } else null diff --git a/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java b/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java index ad5278117..15ffd7bdb 100644 --- a/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java +++ b/app/src/main/java/com/droiddeck/launcher/audio/DirectAudioRelayComponent.java @@ -9,34 +9,12 @@ import com.droiddeck.launcher.core.HostProcess; import java.io.File; import java.util.ArrayList; -/** - * The DirectAudio relay helper, which owns Android's speaker and microphone on behalf of a Linux - * session. - * - * <p>Nothing inside the session can open Android's audio itself. A game is a glibc process in a - * rootfs that binds no {@code /system}, and a glibc process cannot call libaaudio at all; the - * session is also never given a way to spawn a bionic helper of its own. So the helper runs out - * here, under the app's own uid, and the driver inside Wine talks to it over a unix socket. The uid - * matters for more than tidiness: Android checks the microphone permission against whoever opens - * the stream, so a helper started any other way could play but never record. - * - * <p>It is executed from the app's native library directory because Android will not execute a - * binary from anywhere else - the same reason the PulseAudio daemon ships as {@code - * libpulseaudio.so} despite being a program rather than a library. - * - * <p>Started before the client, because the driver asks once whether a helper is there, when Wine - * picks an audio driver, and reports itself unavailable if not. Ported from Bannerlator (GPL-3.0); - * the helper binary is the one built by The412Banner/directaudio's relay workflow. - */ public class DirectAudioRelayComponent extends SessionPart { private static final String TAG = "DirectAudioRelay"; - /** The helper, named as a library so Android will run it. */ public static final String BINARY = "libdirectaudiorelay.so"; private final File socketPath; - /** Shared with PulseAudioComponent: one audio.log per session, appended by both. */ private File logFile; - /** Null when the microphone was not asked for; the helper then opens no input stream. */ private final File micFifoPath; private volatile int pid = -1; @@ -45,7 +23,6 @@ public class DirectAudioRelayComponent extends SessionPart { this.micFifoPath = micFifoPath; } - /** Send the helper's output to this file as well as logcat. Set before {@link #start()}. */ public void setLogFile(File file) { this.logFile = file; } @@ -60,21 +37,10 @@ public class DirectAudioRelayComponent extends SessionPart { } File parent = socketPath.getParentFile(); if (parent != null) { - //noinspection ResultOfMethodCallIgnored parent.mkdirs(); } - // A socket left by a session that did not shut down cleanly is not a listener, and the - // helper would fail to bind onto it. - //noinspection ResultOfMethodCallIgnored socketPath.delete(); - // PulseAudio's pipe module insists on creating the pipe itself and fails if one is already - // there. The helper creates one too when the path is missing, and it starts a few - // milliseconds after the daemon is exec'd - well before the daemon has read its config. So - // left to itself the helper won its own race, the daemon's mkfifo failed with EEXIST, and - // the microphone was lost while everything else came up. Wait for the daemon to make the - // pipe before the helper runs; it appears within a second, and if it never does the helper - // starts anyway and creates it, which is the right thing when the daemon is not involved. if (micFifoPath != null) { long deadline = System.currentTimeMillis() + 3000L; while (!micFifoPath.exists() && System.currentTimeMillis() < deadline) { @@ -91,12 +57,8 @@ public class DirectAudioRelayComponent extends SessionPart { StringBuilder command = new StringBuilder(binary.getAbsolutePath()); command.append(" --socket ").append(socketPath.getAbsolutePath()); - // Diagnostics into the session's audio.log: every 1000 callbacks the device buffer, - // Android's own xrun count and the ring's fill, plus every buffer change. command.append(" --log"); if (micFifoPath != null) { - // The helper creates the pipe if it is not there. PulseAudio is told the same path and - // reads from the other end of it. command.append(" --mic-fifo ").append(micFifoPath.getAbsolutePath()); } ArrayList<String> env = new ArrayList<>(); @@ -126,8 +88,6 @@ public class DirectAudioRelayComponent extends SessionPart { @Override public void stop() { if (pid != -1) { - // The helper exits on a signal and tears down its streams; there is nothing below it to - // orphan, unlike the proot tree. Process.killProcess(pid); pid = -1; } diff --git a/app/src/main/java/com/droiddeck/launcher/audio/PulseAudioComponent.java b/app/src/main/java/com/droiddeck/launcher/audio/PulseAudioComponent.java index c4e1db8c7..849455b7a 100644 --- a/app/src/main/java/com/droiddeck/launcher/audio/PulseAudioComponent.java +++ b/app/src/main/java/com/droiddeck/launcher/audio/PulseAudioComponent.java @@ -17,30 +17,15 @@ import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.concurrent.TimeUnit; -/** - * PulseAudio 13.0 running in the app process, with Android's AAudio as its sink, so the guest's - * libpulse clients (Steam and everything it launches) have a server to talk to. The socket lives - * in the app's files directory and is bound into the session at its own path, so PULSE_SERVER - * needs no translating. - */ public class PulseAudioComponent extends SessionPart { private static final String TAG = "PulseAudio"; - /** Where the guest reaches the daemon; the session exports PULSE_SERVER=unix:<this>. */ public static final String SOCKET_NAME = "PS0"; - /** Identifies the bundled pulseaudio.tzst; a change here re-unpacks it over what a device has. */ private static final String BUNDLE_STAMP = "2026-10-08-pa13-directaudio-native"; private final File workingDir; - /** Where the daemon's own output is kept for this session, or null for logcat only. */ private File logFile; - /** - * A named pipe carrying microphone audio, or null for no microphone. The bundle ships - * module-aaudio-sink but no matching source, and the daemon runs where Android permits - * recording - so pointing module-pipe-source at a pipe the DirectAudio relay helper writes - * turns that one stream into a source the client can see, named DirectAudioMic. - */ private final String micFifoPath; private volatile int pid = -1; @@ -48,13 +33,11 @@ public class PulseAudioComponent extends SessionPart { this(context, null); } - /** As above, with a microphone fed from {@code micFifoPath}; null for output only. */ public PulseAudioComponent(Context context, String micFifoPath) { this.workingDir = new File(context.getFilesDir(), "pulseaudio"); this.micFifoPath = micFifoPath; } - /** CRC-32 of the bundled pulseaudio.tzst, or "?" if it cannot be read. About 600 KB; cheap. */ private String bundleChecksum() { java.util.zip.CRC32 crc = new java.util.zip.CRC32(); byte[] buf = new byte[64 * 1024]; @@ -67,7 +50,6 @@ public class PulseAudioComponent extends SessionPart { return Long.toHexString(crc.getValue()); } - /** Send the daemon's output to this file as well as logcat. Set before {@link #start()}. */ public void setLogFile(File file) { this.logFile = file; } @@ -80,18 +62,9 @@ public class PulseAudioComponent extends SessionPart { public void start() { stop(); if (!workingDir.isDirectory()) { - //noinspection ResultOfMethodCallIgnored workingDir.mkdirs(); FileUtils.chmod(workingDir, 0771); } - // The loadable modules (module-aaudio-sink - the app's own, from tools/aaudio-sink - the - // native protocol, the pipe modules) ride in the apk; the daemon and its libraries come from the native library directory, the one - // place an app may execute a file from. The bundle is unpacked once per BUNDLE_STAMP, not - // once ever: an installed app kept the modules it unpacked on its first run, so a bundle - // fixed in a later build never reached the device - which is how a 17.0 glibc build of - // module-pipe-source sat beside the 13.0 daemon, failed to dlopen, and the microphone - // never appeared. The stamp carries a checksum of the bundle itself, so any change to it - - // a rebuilt sink module included - re-unpacks, whether or not BUNDLE_STAMP was bumped. File modulesDir = new File(workingDir, "modules"); File stamp = new File(modulesDir, ".bundle"); String have = FileUtils.readString(stamp); @@ -112,38 +85,18 @@ public class PulseAudioComponent extends SessionPart { if (pactl.isFile()) FileUtils.chmod(pactl, 0771); copyFromLibraryDir(); - //noinspection ResultOfMethodCallIgnored socket().delete(); - // module-pipe-source creates the pipe with mkfifo and fails outright if one is already - // there - EEXIST, reported as "Unknown error 17" - and the module then does not load at - // all, so the source never appears and the client reports no microphone. Ours lives in the - // app's files directory and survives a session, so after the very first run the path would - // always be occupied. Removed here, before the daemon reads this config: the daemon makes - // it, and the relay helper starts afterwards and is content to find one already made. if (micFifoPath != null && !micFifoPath.isEmpty()) { - //noinspection ResultOfMethodCallIgnored new File(micFifoPath).delete(); } ArrayList<String> config = new ArrayList<>(); config.add("load-module module-native-protocol-unix auth-anonymous=1 auth-cookie-enabled=0 socket=\"" + socket().getAbsolutePath() + "\""); - // volume=1.0 is not optional: with no volume argument module-aaudio-sink defaults - // the sink to 0% and the session plays silence. - // The sink's name is what the client's Audio settings show as the output device, so it - // says which road the sound takes. - // The client's sound: DirectAudio's own engine, in this process, one step from Android. - // decay=0: shrinking the device buffer after a quiet spell probes below what works and that - // probe is a click; the client plays menus. Between .nofail and .fail a module that cannot - // open its stream does not take the daemon down; ensureClientSink() then loads the plain - // AAudio sink in its place. config.add(".nofail"); config.add("load-module module-directaudio-native-sink sink_name=DirectAudio performance_mode=1 adaptive=1 decay=0 limiter=1 watchdog=1 volume=1.0"); config.add("set-default-sink DirectAudio"); config.add(".fail"); if (micFifoPath != null && !micFifoPath.isEmpty()) { - // The format is the helper's, fixed at s16le/48000/mono: it resamples when the device - // grants another input rate, so the daemon is never told a rate the bytes are not. - // A pipe has no clock, so nothing here corrects drift - acceptable for voice. config.add("load-module module-pipe-source source_name=DirectAudioMic file=\"" + micFifoPath + "\" format=s16le rate=48000 channels=1"); config.add("set-default-source DirectAudioMic"); @@ -159,12 +112,7 @@ public class PulseAudioComponent extends SessionPart { String command = workingDir.getAbsolutePath() + "/libpulseaudio.so" + " --system=false --disable-shm=true --fail=false" + " -n --file=default.pa --daemonize=false --use-pid-file=false --exit-idle-time=-1" - // Info level: the classic sink reports the stream Android granted (burst, capacity, - // starting buffer) only there, and that is what a stutter report needs. + " --log-level=info"; - // A module that refuses to load, a pipe that could not be made, the daemon exiting at - // startup: all of it used to reach logcat and nothing else, so a user's folder said nothing - // at all about sound. Four separate faults hid behind "no input device" in one night. final java.io.PrintWriter out = openLog(); pid = HostProcess.start(command, env.toArray(new String[0]), workingDir, null, line -> { @@ -176,11 +124,6 @@ public class PulseAudioComponent extends SessionPart { guard.start(); } - /** - * The fallback: once the daemon answers, if the DirectAudio sink is not - * there (the module failed to load or to open its stream), load the plain AAudio sink so the - * client is never left without an output. Says so in the audio log either way. - */ private void ensureClientSink(java.io.PrintWriter out) { try { long deadline = System.currentTimeMillis() + 15000L; @@ -208,7 +151,6 @@ public class PulseAudioComponent extends SessionPart { } } - /** Runs pactl against this daemon; its stdout, or null when it failed or timed out. */ private String pactl(String... args) { File pactl = new File(workingDir, "pactl"); if (!pactl.isFile() || pid <= 1) return null; @@ -298,7 +240,6 @@ public class PulseAudioComponent extends SessionPart { } } - /** The session's audio log, appended to by both the daemon and the relay helper. */ private java.io.PrintWriter openLog() { if (logFile == null) return null; try { diff --git a/app/src/main/java/com/droiddeck/launcher/core/AppLanguage.kt b/app/src/main/java/com/droiddeck/launcher/core/AppLanguage.kt index 46a164f1a..3ea69d565 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/AppLanguage.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/AppLanguage.kt @@ -6,22 +6,9 @@ import android.content.res.Configuration import android.os.LocaleList import java.util.Locale -/** - * The language the app shows itself in. - * - * Nothing chosen (the default) means the system's: Android's own language, or the per-app language - * Android 13+ lets a user set for DroidDeck in its settings, exactly as Android resolves it. Picking - * one in Setup overrides that until the user picks "System default" again. Each activity and service - * applies the choice to its own context ([wrap], [applyTo]), so it works on every Android version - * the app runs on rather than only where the platform's per-app language API exists. - * - * Steam follows the same choice: [SteamLanguage] turns [effective] into the client's language. - */ object AppLanguage { - /** The stored value meaning "follow the system". */ const val SYSTEM = "" - /** The languages the app ships, as BCP 47 tags, in the order the picker lists them. */ val supported: List<String> = listOf( "en", "id", "ms", "cs", "da", "de", "es", "es-419", "fr", "it", "hu", "nl", "nb", "pl", "pt-BR", "pt-PT", "ro", "fi", "sv", "vi", "tr", "el", "bg", "ru", "uk", "ar", "hi", "th", @@ -31,10 +18,8 @@ object AppLanguage { private const val PREFS = "language" private const val KEY = "app_language" - /** The locale Android gave the process before any choice here was applied. */ @Volatile private var systemLocale: Locale? = null - /** The stored choice: one of [supported], or [SYSTEM]. */ fun chosen(context: Context): String = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY, SYSTEM) ?.takeIf { it in supported } ?: SYSTEM @@ -44,7 +29,6 @@ object AppLanguage { context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(KEY, value).commit() val locale = value.takeIf { it.isNotEmpty() }?.let(Locale::forLanguageTag) ?: system(context) Locale.setDefault(locale) - // Code that reads strings through the application context sees the new language too. val app = context.applicationContext @Suppress("DEPRECATION") app.resources.updateConfiguration( @@ -53,24 +37,16 @@ object AppLanguage { ) } - /** The locale Android chose for the app, ignoring the choice made here. */ fun system(context: Context): Locale = systemLocale ?: context.resources.configuration.locales[0] ?: Locale.getDefault() - /** The locale the app is shown in: the chosen language, or else the system's. */ fun effective(context: Context): Locale = chosen(context).takeIf { it.isNotEmpty() }?.let(Locale::forLanguageTag) ?: system(context) - /** Remembers the system's locale; call with the untouched base context, before [wrap]. */ fun noteSystem(base: Context) { if (systemLocale == null) systemLocale = base.resources.configuration.locales[0] } - /** - * [base], showing the chosen language: for an Application's or a Service's attachBaseContext. - * Only the locale is overridden, so screen size, density and night mode keep following the - * device as they change. - */ fun wrap(base: Context): Context { noteSystem(base) val tag = chosen(base).ifEmpty { return base } @@ -79,11 +55,6 @@ object AppLanguage { return base.createConfigurationContext(override(locale)) } - /** - * Applies the chosen language to an activity; call from its attachBaseContext, after super. - * applyOverrideConfiguration keeps the locale through rotation and window resizes, which an - * activity here handles itself rather than being recreated. - */ fun applyTo(activity: Activity, base: Context) { noteSystem(base) val tag = chosen(base).ifEmpty { return } @@ -92,13 +63,10 @@ object AppLanguage { activity.applyOverrideConfiguration(override(locale)) } - /** Follows a change of the system's language while the process lives (Application.onConfigurationChanged). */ fun systemChanged(config: Configuration) { config.locales[0]?.let { systemLocale = it } } - // A new Configuration's fontScale is 1, which as an override would undo the user's font size; - // 0 leaves it undefined, so only the locale is changed. private fun override(locale: Locale) = Configuration().apply { fontScale = 0f; setLocales(LocaleList(locale)) } private val latinAmerica = setOf( @@ -106,7 +74,6 @@ object AppLanguage { "PE", "PR", "PY", "SV", "US", "UY", "VE", ) - /** Which of the app's languages [locale] reads as, or null when the app does not ship it. */ fun supportedTag(locale: Locale): String? = when (locale.language) { "en", "fr", "ru", "ja", "ko", "ms", "cs", "da", "de", "it", "hu", "nl", "nb", "pl", "ro", "fi", "sv", "vi", "tr", "el", "bg", "uk", "ar", "hi", "th" -> locale.language @@ -122,11 +89,9 @@ object AppLanguage { else -> null } - /** [locale]'s language named in itself, without its region unless the app tells them apart. */ fun displayName(locale: Locale): String = supportedTag(locale)?.let(::nativeName) ?: locale.getDisplayLanguage(locale).replaceFirstChar { it.titlecase(locale) } - /** A language's name in that language, for the picker: what a reader of it looks for. */ fun nativeName(tag: String): String = when (tag) { "en" -> "English" "id" -> "Bahasa Indonesia" diff --git a/app/src/main/java/com/droiddeck/launcher/core/ArchivePaths.java b/app/src/main/java/com/droiddeck/launcher/core/ArchivePaths.java index 3f9eaee44..8f6a8cb6e 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/ArchivePaths.java +++ b/app/src/main/java/com/droiddeck/launcher/core/ArchivePaths.java @@ -3,18 +3,9 @@ package com.droiddeck.launcher.core; import java.io.File; import java.io.IOException; -/** - * Where an archive entry may land. An entry name ("../x", "/etc/x") or a symlink already written - * by an earlier entry can point outside the directory being filled; the canonical path follows - * both, so anything that does not resolve inside it is refused. - */ public final class ArchivePaths { private ArchivePaths() {} - /** - * The file for {@code name} under {@code destination}, or null when it would resolve outside - * it. The destination itself counts as inside (a "./" entry). - */ public static File inside(File destination, String name) throws IOException { String base = destination.getCanonicalPath(); File file = new File(destination, name); diff --git a/app/src/main/java/com/droiddeck/launcher/core/CpuAllocationDefaults.kt b/app/src/main/java/com/droiddeck/launcher/core/CpuAllocationDefaults.kt index 08b0c59dd..adaa106a0 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/CpuAllocationDefaults.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/CpuAllocationDefaults.kt @@ -3,7 +3,6 @@ package com.droiddeck.launcher.core import com.droiddeck.launcher.gpu.GpuInfo import com.droiddeck.launcher.gpu.SocNames -/** Device defaults only: affinity does not reserve cores or lock their clocks. */ object CpuAllocationDefaults { data class Allocation(val client: String, val game: String) @@ -12,7 +11,6 @@ object CpuAllocationDefaults { CpuCores.all.associateWith(CpuCores::maxGhz)) } - /** Select by topology, including faster SM8550 variants; never guess core indices. */ fun forDevice(model: String, platform: String, frequencies: Map<Int, Double?>): Allocation? { val chip = model.trim().uppercase().substringBefore('-') val gen2 = chip in setOf("SM8550", "QCS8550") || diff --git a/app/src/main/java/com/droiddeck/launcher/core/CpuCores.kt b/app/src/main/java/com/droiddeck/launcher/core/CpuCores.kt index cf39812cf..b3dca67cc 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/CpuCores.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/CpuCores.kt @@ -2,17 +2,9 @@ package com.droiddeck.launcher.core import java.io.File -/** - * The device's cores as taskset names them, for the two masks a Steam session can carry. - * - * A list is stored the way taskset's `-c` takes it - comma-separated core numbers - so what the - * dialog produces is what the session script and the Proton wrappers consume unchanged. - */ object CpuCores { - /** Core numbers present on this device, in order. */ val all: List<Int> by lazy { (0 until Runtime.getRuntime().availableProcessors()).toList() } - /** The core's ceiling in GHz for the dialog's labels, or null where the kernel hides it. */ fun maxGhz(core: Int): Double? = FileUtils.readString(File("/sys/devices/system/cpu/cpu$core/cpufreq/cpuinfo_max_freq")) ?.trim()?.toLongOrNull()?.let { it / 1_000_000.0 } @@ -22,22 +14,14 @@ object CpuCores { fun format(cores: Set<Int>): String = cores.sorted().joinToString(",") - /** The stored list, or every core when nothing (or nothing valid) is stored. */ fun listOrAll(list: String): String { val cores = parse(list) return format(if (cores.isEmpty()) all.toSet() else cores) } - /** An all-core game must undo the affinity inherited from a pinned Steam client. */ fun gameLaunchList(list: String, clientPinned: Boolean): String = restrictionOrEmpty(list).ifEmpty { if (clientPinned) listOrAll("") else "" } - /** - * The list only when it is a real restriction, else "". Empty, absent, or naming every core - * all mean "no preference": pinning a process to all cores is what the scheduler does - * unaided, and passing it would only put a meaningless line in the session log. Bannerlator's - * rule, kept for the game mask. - */ fun restrictionOrEmpty(list: String): String { val cores = parse(list) return if (cores.isEmpty() || cores.size >= all.size) "" else format(cores) diff --git a/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt b/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt index 77ff35fb6..7d23f91fb 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt @@ -21,15 +21,6 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * `device.txt`: what this device is and what the session was told to do, written the moment a - * session starts so a log can be read months later without asking the reporter a single question. - * - * **Nothing identifying goes in here.** No serial number, no Android ID, no IMEI, no account name, - * no network names - only what the hardware is, what the ROM is, and which of our own settings - * were in effect. The one thing that comes close is the build fingerprint, which names the ROM and - * its build date and is the same string on every unit of that model. - */ object DeviceReport { private const val TAG = "DeviceReport" diff --git a/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt b/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt index 1af4095be..325ed7d43 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/DeviceSupport.kt @@ -5,26 +5,14 @@ import android.os.Build import com.droiddeck.launcher.R import java.io.File -/** - * Whether this device can draw a Linux session at all. - * - * The runtime draws with Turnip, an Adreno driver. On Mali, Xclipse and PowerVR the compositor - * gets no usable Vulkan device and a session comes up as sound over a black screen - a failure - * with nothing in it to read. The app cannot fix that, so it says so before the download rather - * than after it. Adreno is recognised by what only Qualcomm's stack has: the KGSL node, or the - * vendor's own Vulkan driver at its usual path. - */ object DeviceSupport { fun adreno(): Boolean = File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists() - /** The chip as the device names it, for the card that explains the refusal. */ fun gpuName(context: Context): String { val soc = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL.takeIf { it.isNotBlank() && it != Build.UNKNOWN } else null return soc?.let { "$it (${Build.HARDWARE})" } ?: Build.HARDWARE.ifBlank { context.getString(R.string.gpu_this_gpu) } } - /** False on the Odin 3 (CQ8725S): a low-latency Wi-Fi lock makes its WLAN firmware hang, and - * that firmware reboots the whole device instead of restarting Wi-Fi. */ fun lowLatencyWifiSafe(): Boolean = Build.VERSION.SDK_INT < 31 || Build.SOC_MODEL != "CQ8725S" } diff --git a/app/src/main/java/com/droiddeck/launcher/core/Downloader.java b/app/src/main/java/com/droiddeck/launcher/core/Downloader.java index 254341221..1e63ef982 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/Downloader.java +++ b/app/src/main/java/com/droiddeck/launcher/core/Downloader.java @@ -12,11 +12,6 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; -/** - * The two fetches this app makes: the runtime catalog (a few hundred bytes of JSON) and the - * runtime tarball itself (~790 MB). Plain HttpURLConnection - nothing here needs a client library, - * and one fewer dependency is one fewer thing that can fail a release build. - */ public final class Downloader { private static final String TAG = "Downloader"; private static final int TIMEOUT_MS = 30_000; @@ -44,11 +39,6 @@ public final class Downloader { } } - /** - * Downloads to {@code destination}, reporting a 0..1 fraction (or -1 when the server sends no - * length). {@code resume} continues a partial file with a Range request, which is what makes a - * 790 MB download survive a dropped connection instead of starting over. - */ public static boolean downloadFile(String url, File destination, boolean resume, java.util.function.Consumer<Float> progress) { HttpURLConnection connection = null; @@ -59,10 +49,8 @@ public final class Downloader { int code = connection.getResponseCode(); boolean appending = code == HttpURLConnection.HTTP_PARTIAL; if (code / 100 != 2) { - // The server ignored or refused the range: start the file again. if (have > 0) { connection.disconnect(); - //noinspection ResultOfMethodCallIgnored destination.delete(); return downloadFile(url, destination, false, progress); } @@ -73,7 +61,7 @@ public final class Downloader { long length = connection.getContentLengthLong(); long total = length < 0 ? -1 : length + have; File parent = destination.getParentFile(); - if (parent != null && !parent.isDirectory()) //noinspection ResultOfMethodCallIgnored + if (parent != null && !parent.isDirectory()) parent.mkdirs(); long written = have; try (InputStream in = new BufferedInputStream(connection.getInputStream(), 1 << 16); @@ -83,8 +71,6 @@ public final class Downloader { for (int read = in.read(buffer); read > 0; read = in.read(buffer)) { out.write(buffer, 0, read); written += read; - // A callback per 64 KB would be tens of thousands of UI hops; once per MB is - // enough to move a progress bar smoothly. if (progress != null && written - lastReport > (1 << 20)) { lastReport = written; progress.accept(total > 0 ? written / (float) total : -1f); diff --git a/app/src/main/java/com/droiddeck/launcher/core/FexPreset.kt b/app/src/main/java/com/droiddeck/launcher/core/FexPreset.kt index 832c9d039..6914bffa8 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/FexPreset.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/FexPreset.kt @@ -28,7 +28,6 @@ object FexPreset { fun byId(id: String): Preset = all.firstOrNull { it.id == id } ?: all[0] - /** Game-launch variables in KEY=VALUE form. */ fun env(id: String): List<String> = byId(id).env val environmentKeys: Set<String> by lazy { diff --git a/app/src/main/java/com/droiddeck/launcher/core/FileUtils.java b/app/src/main/java/com/droiddeck/launcher/core/FileUtils.java index 8190e2877..4cbba0897 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/FileUtils.java +++ b/app/src/main/java/com/droiddeck/launcher/core/FileUtils.java @@ -18,14 +18,12 @@ public final class FileUtils { private FileUtils() {} - /** Recursive delete that does not follow symlinks - a rootfs is full of them. */ public static void delete(File file) { if (file == null || !file.exists() && !isSymlink(file)) return; if (file.isDirectory() && !isSymlink(file)) { File[] children = file.listFiles(); if (children != null) for (File child : children) delete(child); } - //noinspection ResultOfMethodCallIgnored file.delete(); } @@ -37,7 +35,6 @@ public final class FileUtils { } } - /** Empties a directory without removing it. */ public static void clear(File dir) { if (dir == null || !dir.isDirectory()) return; File[] children = dir.listFiles(); @@ -47,7 +44,7 @@ public final class FileUtils { public static boolean writeString(File file, String content) { try { File parent = file.getParentFile(); - if (parent != null && !parent.isDirectory()) //noinspection ResultOfMethodCallIgnored + if (parent != null && !parent.isDirectory()) parent.mkdirs(); Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8)); return true; @@ -81,10 +78,9 @@ public final class FileUtils { } } - /** Copies an asset out of the apk, creating the parent directory. */ public static boolean copyAsset(Context context, String assetPath, File destination) { File parent = destination.getParentFile(); - if (parent != null && !parent.isDirectory()) //noinspection ResultOfMethodCallIgnored + if (parent != null && !parent.isDirectory()) parent.mkdirs(); try (InputStream in = context.getAssets().open(assetPath); OutputStream out = new FileOutputStream(destination)) { diff --git a/app/src/main/java/com/droiddeck/launcher/core/GameEnvironment.kt b/app/src/main/java/com/droiddeck/launcher/core/GameEnvironment.kt index dc6483118..045b52ed4 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/GameEnvironment.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/GameEnvironment.kt @@ -29,25 +29,15 @@ object GameEnvironment { "MESA_SHADER_CACHE_DISABLE" to "false", "VKD3D_FEATURE_LEVEL" to "12_2", "VKD3D_SHADER_MODEL" to "6_9", - // .NET 7 and later reserve 256 GB of address space for the GC heap at startup, more than an - // Android process has free, and the runtime fails to start. 32 GB is plenty for a game. - // Older NativeAOT runtimes require bare hex digits. CoreCLR accepts this form too. "DOTNET_GCRegionRange" to "800000000", - // .NET's W^X writes JIT code through a second mapping that FEX does not watch, so FEX keeps - // running the stale translation and the game hangs at startup. "DOTNET_EnableWriteXorExecute" to "0", ).apply { FexPreset.env(preset).forEach { put(it.substringBefore('='), it.substringAfter('=')) } - // Steam settings' Force SSBS: droiddeck-game-env preloads libssbs.so into the game's Wine. if (forceSsbs) put(FORCE_SSBS, "1") } const val FORCE_SSBS = "DROIDDECK_FORCE_SSBS" - /** - * A game's Start without Steam: droiddeck-game-env launches it without Proton's steam.exe (umu's - * launch where the Proton has it, else runinprefix), for games that close right away. - */ const val NO_STEAM = "DROIDDECK_NO_STEAM" fun gameNoSteam(config: Config, scope: String): Boolean = diff --git a/app/src/main/java/com/droiddeck/launcher/core/Hashes.kt b/app/src/main/java/com/droiddeck/launcher/core/Hashes.kt index 072874c87..0e4dce500 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/Hashes.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/Hashes.kt @@ -3,7 +3,6 @@ package com.droiddeck.launcher.core import java.io.File import java.security.MessageDigest -/** File checksums as lowercase hex, and GitHub's release-asset digests, in one place. */ object Hashes { private val GITHUB_SHA256 = Regex("(?i)sha256:[0-9a-f]{64}") @@ -13,11 +12,9 @@ object Hashes { @JvmStatic fun sha512(file: File): String = hex(file, "SHA-512") - /** True when [digest] is GitHub's asset digest form, "sha256:" and 64 hex digits. */ @JvmStatic fun isGithubSha256(digest: String?): Boolean = digest != null && digest.matches(GITHUB_SHA256) - /** The hex of a GitHub asset digest ("sha256:<hex>"), or null when [digest] is not one. */ @JvmStatic fun githubSha256(digest: String?): String? = digest?.takeIf { isGithubSha256(it) }?.substringAfter(':') diff --git a/app/src/main/java/com/droiddeck/launcher/core/HostEnvironment.kt b/app/src/main/java/com/droiddeck/launcher/core/HostEnvironment.kt index f40c93f82..e9b06990e 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/HostEnvironment.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/HostEnvironment.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.core -/** The variables a host process is started with, in the order they were set. */ class HostEnvironment { private val entries = LinkedHashMap<String, String>() @@ -8,6 +7,5 @@ class HostEnvironment { entries[name] = value } - /** `NAME=VALUE` lines, the shape ProcessBuilder and proot take. */ fun asArray(): Array<String> = entries.map { (name, value) -> "$name=$value" }.toTypedArray() } diff --git a/app/src/main/java/com/droiddeck/launcher/core/HostProcess.kt b/app/src/main/java/com/droiddeck/launcher/core/HostProcess.kt index 550d55a52..4a8b12934 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/HostProcess.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/HostProcess.kt @@ -4,27 +4,9 @@ import android.util.Log import java.io.File import java.util.function.Consumer -/** - * Starts a process on the Android side - proot, the audio daemon, logcat - and hands back its pid. - * - * The pid matters: stopping a session means killing that exact process (proot runs with - * --kill-on-exit, so the guest tree goes with it), and java.lang.Process does not expose it before - * API 33, so it is read from the implementation's private field. Output can be drained line by - * line to a callback; without one it goes to /dev/null, never to a pipe nobody reads, which would - * block the child the moment the pipe filled. - */ object HostProcess { private const val TAG = "HostProcess" - /** - * @param command the program and its arguments as one line; a backslash escapes a space, since - * paths under /data are full of them. - * @param env `NAME=VALUE` lines added to the app's own environment (proot needs nothing from - * the inherited set, and the guest starts with `env -i` anyway), or null. - * @param onExit told the exit status from a waiting thread, or null. - * @param onLine told each line of stdout and stderr, or null to discard both. - * @return the pid, or -1 when the process could not be started. - */ @JvmStatic fun start(command: String, env: Array<String>?, dir: File?, onExit: Consumer<Int>?, onLine: Consumer<String>?): Int { val argv = split(command) @@ -46,14 +28,13 @@ object HostProcess { try { process.inputStream.bufferedReader().useLines { lines -> lines.forEach { onLine.accept(it) } } } catch (e: Exception) { - // The process ended or the pipe closed; nothing more to read. } }, "proc-out-$pid").apply { priority = Thread.NORM_PRIORITY - 1; isDaemon = true; start() } } if (onExit != null) { Thread({ var status = -1 - try { status = process.waitFor() } catch (e: InterruptedException) { /* reported as -1 */ } + try { status = process.waitFor() } catch (e: InterruptedException) { } onExit.accept(status) }, "proc-wait-$pid").apply { priority = Thread.NORM_PRIORITY - 1; start() } } @@ -75,7 +56,6 @@ object HostProcess { -1 } - /** Splits a command line on spaces; a backslash keeps the space that follows it. */ @JvmStatic fun split(command: String): List<String> { val parts = ArrayList<String>() diff --git a/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt b/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt index 4406a7028..969442631 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/LogRedactor.kt @@ -1,64 +1,28 @@ package com.droiddeck.launcher.core -/** - * Scrubs credentials out of a log line before it is written somewhere a user will share. - * - * The Steam client's own logs are the reason this exists: they carry the account's session token - * (logged as a bare number after "Using JWT"), machine-auth GUIDs, `token=`/`sessionid=` pairs, - * Steam Guard codes, 32-hex WebAPI keys and the account's SteamID. A session folder from this app - * is meant to be attached to a bug report as it is, so none of that may be in it. - * - * Ported from Bannerlator's {@code SteamLogRedactor} (GPL-3.0), which was written against real - * client logs; the account-name registration is left out because this app never learns it. - * - * What is deliberately KEPT, because it is what makes a log worth reading: EResult codes, CM host - * names and Valve's server addresses, timings, token expiry dates, app IDs, pids, connection-state - * changes and file paths. A SteamID is masked rather than deleted - first four and last four digits - * survive - so a reader can still tell two accounts' lines apart without the value identifying either. - * - * The device's OWN addresses are not kept: the client's IPv6 check logs "external address <ours>" - * about twenty times a session, and a public IPv6 address is the user's. [learnOwnAddresses] reads - * the link the session was given (etc/droiddeck-net) and every address on the same /64 - privacy - * addresses rotate inside it - or an own public IPv4 is replaced wherever it appears. Likewise every - * Steam account and persona name on the device ([learnAccounts]), for users who sign in with an - * account name rather than an email. - */ object LogRedactor { private val EMAIL = Regex("[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}") - /** "Using JWT 25484942796017334" - the client logs its session token as digits, not base64. */ private val JWT_LABELLED = Regex("(?i)(\\bJWT[\\s=:]+)(\\d{8,})") - /** A 3-part base64url JWT logged verbatim (a refresh or access token). */ private val JWT_BASE64 = Regex("ey[A-Za-z0-9_\\-]{6,}\\.[A-Za-z0-9_\\-]{6,}\\.[A-Za-z0-9_\\-]{6,}") - /** Machine-auth and other GUIDs. */ private val GUID = Regex("\\b[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\\b") - /** - * key=value secrets. A bare `key` is excluded on purpose so "key=english" survives; a real - * WebAPI key is caught by [WEBAPI_KEY]. The value class excludes `<` and `>` so running this - * twice over an already-scrubbed line changes nothing. - */ private val SECRET_KV = Regex( "(?i)\\b(access[_-]?token|refresh[_-]?token|auth[_-]?token|authtoken|token|authcode|" + "auth[_-]?ticket|ticket|sessionid|steamloginsecure|webapikey|api[_-]?key|" + "machine[_-]?auth(?:[_-]?token)?|machineauth|password|passwd|pwd|secret)" + "(\\s*[=:]\\s*|=)([^\\s\"'<>&;,]{4,})" ) - /** A Steam Guard code, only where the text around it says that is what it is. */ private val GUARD_CODE = Regex( "(?i)((?:steam\\s*)?guard\\s*code[\\s:=]*|two[\\s-]?factor[\\s:=]*|2fa[\\s:=]*)([A-Za-z0-9]{5})" ) - /** Exactly 32 hex - a WebAPI key. Bounded so a 40-hex depot chunk id is left alone. */ private val WEBAPI_KEY = Regex("\\b[0-9A-Fa-f]{32}\\b") - /** A long opaque run after a sensitive word, for anything the rules above missed. */ private val RESIDUAL = Regex( "(?i)\\b(jwt|token|ticket|sessionid|steamloginsecure|machineauth)\\b[\\s=:]*([A-Za-z0-9+/=_\\-]{12,})" ) private val LONG_TOKEN = Regex("[A-Za-z0-9_\\-]{88,}") private val STEAMID64 = Regex("\\b(76561)(\\d{8})(\\d{4})\\b") private val STEAMID3 = Regex("\\[U:1:(\\d+)]") - /** "external address 2607:..." / "external IP: 203.0.113.9" - the client stating ours. */ private val EXTERNAL_ADDR = Regex("(?i)(external\\s+(?:ip\\s+)?address\\s*[:=]?\\s*|external\\s+ip\\s*[:=]?\\s*)([0-9A-Fa-f:.]{7,})") private val IPV4 = Regex("(?<![0-9.])(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})(?![0-9.])") - /** An IPv6 literal: "::" somewhere, or all eight groups (a clock's 10:03:05 is neither). */ private val IPV6 = Regex("(?<![0-9A-Za-z:])[0-9A-Fa-f]{0,4}(?::[0-9A-Fa-f]{0,4}){2,7}(?:%[A-Za-z0-9_.]+)?(?![0-9A-Za-z:])") private val MAC = Regex("(?i)(?<![0-9a-z:-])[0-9a-f]{2}([:-])(?:[0-9a-f]{2}\\1){4}[0-9a-f]{2}(?![0-9a-z:-])") private val MAC_FIELD = Regex( @@ -70,20 +34,9 @@ object LogRedactor { "[\"']?\\s*[:=]\\s*)(\"[^\"\\r\\n<>]+\"|'[^'\\r\\n<>]+'|[^\\s\"'<>&;,\\[\\]{}]+)" ) - /** - * The account a Steam UI login line names ("Login: OnLoginStateChange <account> 2 1 0 0"): a - * user who signs in with a plain account name, not an email, had it in every log. The single - * space then a non-space keeps "OnLoginStateChange 0 1 0 0" (no account yet) as it is. - */ private val LOGIN_STATE = Regex("(?i)(OnLoginStateChange:? )([^\\s<]\\S*)") private val LOGIN_USERS = Regex("(OnLoginUsersChanged )(\\S.*)$") - /** - * An account name as a field: `AccountName`, `account_name`, `username` (`"AccountName" - * "someone"` in a VDF, `account_name=...`, `"username": "..."`) and `login` - the last only - * as `login=`, a quoted key or a VDF pair, so "Login: OnLoginStateChange ..." keeps its label. - * A quoted value is blanked whole; one already blanked is left as it is. - */ private const val ACCOUNT_VALUE = "(\"[^\"<\\r\\n]+\"|'[^'<\\r\\n]+'|[^\\s\"'<>&;,}]+)" private val ACCOUNT_KV = Regex( "(?i)(?<![A-Za-z0-9_])(account[_-]?name|user[_-]?name|epicusername)" + @@ -93,18 +46,11 @@ object LogRedactor { "(?i)(?<![A-Za-z0-9_])(login)(\\s*=\\s*|[\"']\\s*[=:]\\s*|[\"']\\s+)" + ACCOUNT_VALUE ) - /** A learned pattern and the text (lowercase) a line must hold for it to match; null when none can be said. */ private class Learned(val regex: Regex, val needs: String?) - /** The device's Steam accounts and persona names as patterns (see [learnAccounts]). */ @Volatile private var accounts: List<Learned> = emptyList() - /** - * Learns every Steam account on the device from the client's loginusers.vdf - its AccountName - * and PersonaName - so they are replaced wherever a log mentions them, whoever the user is. - * Names under three characters are skipped: blanking every "a" would ruin a log. - */ fun learnAccounts(loginUsers: java.io.File) { val found = ArrayList<Learned>() try { @@ -121,21 +67,14 @@ object LogRedactor { accounts = found } - /** Everything the session's runtime can tell about whose logs these are: addresses and accounts. */ 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")) } - /** This device's public addresses as patterns (see [learnOwnAddresses]); empty until learned. */ @Volatile private var own: List<Learned> = emptyList() - /** - * Learns the device's addresses from the link file the app writes for the session - * (`addr <address> <prefix>` lines). Global IPv6 become their /64, public IPv4 stay exact; - * private, link-local and loopback addresses identify nobody and are left out. - */ fun learnOwnAddresses(linkFile: java.io.File) { val found = ArrayList<Learned>() try { @@ -144,7 +83,6 @@ object LogRedactor { when (kind(addr)) { "public IPv6" -> { val groups = expand6(addr)?.take(4) ?: return@forEachLine - // Any spelling of an address in this /64: leading zeros dropped, :: anywhere after. val prefix = groups.joinToString(":") { g -> "0{0,3}" + Regex.escape(g.trimStart('0').ifEmpty { "0" }) } found += Learned( Regex("(?i)(?<![0-9A-Fa-f:])$prefix(?::[0-9A-Fa-f]{0,4}){1,4}(?:%[A-Za-z0-9_.]+)?"), @@ -160,7 +98,6 @@ object LogRedactor { own = found } - /** "public IPv6", "private IPv4", "link-local IPv6", ... for an address literal; null if it is not one. */ fun kind(address: String): String? { val a = address.substringBefore('%') IPV4.matchEntire(a)?.let { m -> @@ -184,7 +121,6 @@ object LogRedactor { } } - /** The eight groups of an IPv6 literal, or null. */ private fun expand6(address: String): List<String>? { val a = address.substringBefore('%') if (!a.contains(':') || a.count { it == ':' } > 7 || a.contains(":::")) return null @@ -200,17 +136,12 @@ object LogRedactor { return parts.map { it.lowercase() } } - /** - * Every IP address in [text] replaced by what kind it is ("<public IPv6>"), for reports whose - * point is the shape of the network, not its numbers. - */ fun describeAddresses(text: String): String { var out = IPV6.replace(text) { m -> kind(m.value)?.let { "<$it>" } ?: m.value } out = IPV4.replace(out) { m -> kind(m.value)?.let { "<$it>" } ?: m.value } return out } - /** A file worth scrubbing: text (no NUL in its first 4 KB) and not huge. */ fun isText(file: java.io.File): Boolean = try { file.isFile && file.length() < 64L * 1024 * 1024 && file.inputStream().use { input -> val buf = ByteArray(4096) @@ -221,34 +152,16 @@ object LogRedactor { false } - /** - * Bump whenever a pattern is added or tightened. A session folder records the version it was - * scrubbed under (SessionArtifacts.SCRUBBED_TREE_MARKER), and a share copies a file as it is - * only when that matches, so a folder scrubbed under older rules is scrubbed again on the way - * out. - */ const val RULES_VERSION = 3 - /** - * A private IPv4 address (10/8, 172.16/12, 192.168/16, link-local 169.254/16): the home - * network's layout, e.g. its router as the resolver. Blanked in a shared zip only; public - * server addresses stay. Not part of a longer dotted number (a version like 10.0.19041.1). - */ private val LAN_IPV4 = Regex( "(?<![\\d.])(?:10\\.\\d{1,3}|192\\.168|172\\.(?:1[6-9]|2\\d|3[01])|169\\.254)\\.\\d{1,3}\\.\\d{1,3}(?!\\.?\\d)" ) - /** [src]'s lines, scrubbed, to [out]. */ fun scrubTo(src: java.io.File, out: java.io.Writer) { src.forEachLine { line -> out.write(redact(line)); out.write("\n") } } - /** - * The last pass for a file going into a shared zip: [redact], then the rules the stores' log - * uses too ([SecretScrub]) - URLs without query, fragment and user:password and with any - * token-bearing path segment blanked, token values and Authorization / Cookie headers blanked - * - and private IPv4 addresses as `<lan-address>`. A line already clean comes out unchanged. - */ fun redactForShare(line: String): String { if (line.isEmpty()) return line return try { @@ -259,23 +172,15 @@ object LogRedactor { } } - /** [src]'s lines through [redactForShare], to [out]: the pass every text file in a shared zip gets. */ fun scrubForShare(src: java.io.File, out: java.io.Writer) { src.forEachLine { line -> out.write(redactForShare(line)); out.write("\n") } } - /** `<redacted:account>`, inside [value]'s quotes when it had them. */ private fun quoted(value: String): String { val q = value.first().takeIf { it == '"' || it == '\'' }?.toString().orEmpty() return "$q<redacted:account>$q" } - /** - * A line on its way through [redact]. Each rule runs only on a line holding what it cannot match - * without - a word, enough separators, a long enough run: most of a Steam log is lines no rule - * touches, and a session's logs went through every rule at about a megabyte a second. A line with anything but ASCII in it goes through - * every case-insensitive rule, since Android's regex folds case more widely than lowercase(). - */ private class Line(var text: String) { private val unfolded = text.any { it.code >= 0x80 } private var lower: String? = null @@ -308,7 +213,6 @@ object LogRedactor { private fun isHex(c: Char) = c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F' - /** [line] with every credential shape replaced. Null- and exception-safe by construction. */ fun redact(line: String): String { if (line.isEmpty()) return line return try { @@ -328,7 +232,6 @@ object LogRedactor { } if (out.has("guard", "factor", "2fa")) out.set(GUARD_CODE.replace(out.text) { "${it.groupValues[1]}<redacted:code>" }) if (out.hasRun(32, ::isHex)) out.set(WEBAPI_KEY.replace(out.text, "<redacted:key>")) - // Mask, not delete: the last four digits let a reader correlate lines to one account. if ("76561" in out.text) out.set(STEAMID64.replace(out.text) { "${it.groupValues[1]}********${it.groupValues[3]}" }) if ("[U:1:" in out.text) out.set(STEAMID3.replace(out.text) { m -> val id = m.groupValues[1] @@ -352,8 +255,6 @@ object LogRedactor { } out.text } catch (t: Throwable) { - // A log line is never worth crashing a session over, but an unscrubbed one must not - // reach the file either. "<redaction failed; line withheld>" } } diff --git a/app/src/main/java/com/droiddeck/launcher/core/NetworkReport.kt b/app/src/main/java/com/droiddeck/launcher/core/NetworkReport.kt index b8a0e5dae..d06b5c593 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/NetworkReport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/NetworkReport.kt @@ -10,24 +10,11 @@ import java.io.File import java.net.Inet4Address import java.net.Inet6Address -/** - * `network.txt`: what the session was given to work with, for the reports that say "the client - * thinks WiFi is off" or "Firefox cannot resolve anything". - * - * The runtime's NetworkManager bridge reports Android's active link. Include whether the phone - * had a validated link and which DNS servers the app wrote into the runtime's `resolv.conf`. - * - * **No network names.** The SSID is identifying and is deliberately not collected; the transport - * type, the addresses' families and the DNS servers are what a diagnosis needs. - */ object NetworkReport { private const val TAG = "NetworkReport" fun write(context: Context, target: File) { try { - // Addresses as their kind ("<public IPv6>"), never the numbers: a public address is the - // user's, and this file goes into shared session logs. The session's made-up hardware - // address is stable per device (seeded from ANDROID_ID), so it is masked too. val text = LogRedactor.describeAddresses(build(context)) .replace(Regex("(?m)^(\\s*mac )\\S+"), "$1<masked>") target.writeText(text) @@ -82,8 +69,6 @@ object NetworkReport { append("\nWhat the runtime was given\n") append("--------------------------\n") - // The app writes this file from the live link at every change; it is what everything inside - // the session resolves with, so a mismatch with the system's list above is the bug itself. val resolv = File(LinuxRuntime.rootDir(context), "etc/resolv.conf") if (resolv.isFile) { append("etc/resolv.conf:\n") diff --git a/app/src/main/java/com/droiddeck/launcher/core/PhantomProcessLimit.kt b/app/src/main/java/com/droiddeck/launcher/core/PhantomProcessLimit.kt index 6a39746b7..cbad4349d 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/PhantomProcessLimit.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/PhantomProcessLimit.kt @@ -5,7 +5,6 @@ import android.os.Build import android.provider.Settings import com.droiddeck.launcher.R -/** Android's phantom process monitor can kill the many child processes used by a Steam session. */ enum class PhantomProcessStatus { NOT_APPLICABLE, DISABLED, @@ -68,12 +67,6 @@ object PhantomProcessLimit { return status(global, systemProperty(OVERRIDE_PROPERTY)) } - /** - * The value Android itself acts on, in FeatureFlagUtils.isEnabled's order: the global setting - * when it is set, otherwise the feature-flag override property. Developer options' "Disable - * child process restrictions" writes only the property, so a device switched off there has no - * global setting at all and must not be reported as unset. - */ fun status(global: String?, override: String?): PhantomProcessStatus { val value = global?.trim()?.lowercase()?.takeIf { it.isNotEmpty() } ?: override?.trim()?.lowercase()?.takeIf { it.isNotEmpty() } @@ -122,7 +115,6 @@ object PhantomProcessLimit { fun fixSentence(context: Context, sdk: Int = Build.VERSION.SDK_INT): String = context.getString(if (hasDeveloperToggle(sdk)) R.string.phantom_fix_toggle else R.string.phantom_fix_no_toggle) - /** English: for the device report. */ fun reportValue(status: PhantomProcessStatus): String = when (status) { PhantomProcessStatus.DISABLED -> "disabled (good - the OS will not kill the session's children)" PhantomProcessStatus.ENABLED -> "ENABLED (the OS may kill the session with no log; turn off Restrict child processes)" diff --git a/app/src/main/java/com/droiddeck/launcher/core/SecretScrub.kt b/app/src/main/java/com/droiddeck/launcher/core/SecretScrub.kt index c91ca60ef..7ac3f1801 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/SecretScrub.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/SecretScrub.kt @@ -1,21 +1,5 @@ package com.droiddeck.launcher.core -/** - * The one set of rules that keeps URLs and tokens out of anything the app writes or shares: the - * stores' log ([com.droiddeck.launcher.stores.StoreLog.redactLine]) and the last pass over every - * text file in a shared log zip ([LogRedactor.redactForShare]). - * - * - Every URL loses its query, fragment and `user:password@`. [Urls.SHORT] then keeps only the host - * and a plain first path segment (the stores' lines, where a GOG secure link carries its token in - * the path); [Urls.KEEP_PATH] keeps the path but blanks any segment that carries a token. - * - Outside URLs, the values of token-like keys (`token`, `__token__`, `f_token`, `hdnts`, access / - * refresh / id tokens; exchange and authorization codes of four characters or more, and a bare - * `code=` where it is one: in a query string or on a line with OAuth keys such as `client_id`, - * `redirect_uri` or `state`, never an 8-hex exception code like Proton's `code=c0000005`) and of - * Authorization / Cookie headers are blanked with [mark]. - * - * A line already scrubbed comes out unchanged. - */ object SecretScrub { enum class Urls { SHORT, KEEP_PATH } @@ -23,12 +7,9 @@ object SecretScrub { private val SAFE_SEGMENT = Regex("""[A-Za-z0-9._\-]{1,40}""") private val TOKEN_SEGMENT = Regex("""(?i)(token|hdnts|hmac|signature|sig|exp)=""") private val TOKEN = Regex("""(?i)(?<![A-Za-z0-9_])(__token__|f_token|hdnts|access_token|refresh_token|id_token|token)(["']?\s*[=:]\s*["']?)[^\s&;,"'<>}]+""") - /** Codes that are only ever OAuth grants. */ private val GRANT_CODE = Regex("""(?i)(?<![A-Za-z0-9_])(exchange_code|authorization_?code)(["']?\s*=\s*["']?)[^\s&;,"'<>}]{4,}""") - /** A bare `code=`: a grant only right after `?` / `&` (a query string) or beside OAuth keys ([OAUTH_CONTEXT]). */ private val CODE = Regex("""(?i)(?<![A-Za-z0-9_])(code)(["']?\s*=\s*["']?)([^\s&;,"'<>}]{4,})""") private val OAUTH_CONTEXT = Regex("""(?i)(?<![A-Za-z0-9_])(client_id|client_secret|redirect_uri|state|grant_type|response_type|access_token|refresh_token|id_token)["']?\s*[=:]""") - /** A Windows exception or status code (`code=c0000005`, `code=0x80000003`): never a grant. */ private val STATUS_CODE = Regex("""(?i)(0x)?[0-9a-f]{8}""") private val HEADER = Regex("""(?i)\b(authorization|cookie|set-cookie)(\s*[:=]\s*).+""") diff --git a/app/src/main/java/com/droiddeck/launcher/core/SessionLogCapture.kt b/app/src/main/java/com/droiddeck/launcher/core/SessionLogCapture.kt index 67b924ec2..965a4950d 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/SessionLogCapture.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/SessionLogCapture.kt @@ -5,24 +5,6 @@ import android.util.Log import java.io.BufferedWriter import java.io.File -/** - * `app.log`: what the app itself said while the session ran, and `crash.log`: what Android's crash - * buffer holds if something died hard. - * - * Everything this app decides is reported through `Log` - which driver was chosen and why, the - * audio line, a rival client being stopped, the session's exit status, a helper that was missing. - * None of it reaches a user's folder, so a report used to arrive with the settings visible in - * `device.txt` and no record of what actually happened. An app may always read back its own log - * entries, so the simplest complete answer is to run `logcat` filtered to our own pid and keep - * what it says. - * - * `logcat -b crash` earns its own file: proot once died before `main` over a missing library, and - * that appeared in the crash buffer and *nowhere else* - not in the session log, not in the main - * buffer. It is the first thing to read when a session "exits instantly". - * - * Both are best-effort. Where a ROM refuses to hand an app its own entries the files are written - * with a line saying so, which is better than their absence looking like silence. - */ object SessionLogCapture { private const val TAG = "SessionLogCapture" @@ -30,7 +12,6 @@ object SessionLogCapture { private var writer: BufferedWriter? = null private var target: File? = null - /** Start mirroring this process's log lines into [target]. Safe to call twice. */ @Synchronized fun start(target: File) { stop() @@ -41,8 +22,6 @@ object SessionLogCapture { out.flush() writer = out this.target = target - // -T 1 starts at the newest line rather than replaying the whole buffer; -v threadtime - // keeps the timestamps and thread ids that make two logs line up. pid = HostProcess.start( "/system/bin/logcat -v threadtime -T 1 --pid=" + Process.myPid(), null, target.parentFile, null, @@ -51,7 +30,6 @@ object SessionLogCapture { try { writer?.apply { write(line); newLine(); flush() } } catch (e: Exception) { - // A full or unmounted card must not take the session with it. } } } @@ -65,7 +43,6 @@ object SessionLogCapture { } } - /** Stops the capture only if it is the one writing into [dir]: a newer session's is left alone. */ @Synchronized fun stopFor(dir: File) { val t = target ?: return @@ -86,23 +63,14 @@ object SessionLogCapture { try { writer?.close() } catch (e: Exception) { - // nothing useful to do with a failure to close a log } writer = null } - /** - * Dump Android's crash buffer into [target]. Called at teardown: a session that was killed is - * exactly when this matters, and the buffer keeps its entries after the process is gone. - */ fun dumpCrashBuffer(target: File) { try { val lines = StringBuilder() val done = java.util.concurrent.CountDownLatch(1) - // Wait on the process ENDING, not on the first line arriving: the lines come through a - // callback on another thread, so a poll for "is it empty yet" declared an empty buffer - // every time and still cost the wait. exec() returns a pid, never an exit status - the - // first version printed that pid as "rc", which was nonsense in the file. val pid = HostProcess.start( "/system/bin/logcat -b crash -d -v threadtime -t 400", null, target.parentFile, { done.countDown() }, diff --git a/app/src/main/java/com/droiddeck/launcher/core/SessionPart.java b/app/src/main/java/com/droiddeck/launcher/core/SessionPart.java index 86f4380aa..bb5d13214 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/SessionPart.java +++ b/app/src/main/java/com/droiddeck/launcher/core/SessionPart.java @@ -2,12 +2,6 @@ package com.droiddeck.launcher.core; import android.content.Context; -/** - * One thing a running session depends on beside the guest process itself: the audio daemon, the - * rumble listener, the battery the client reads, the network-link file. The session starts its - * parts in order before the guest and stops them in reverse after it. A part is handed the - * application context, never an activity's, so nothing here can keep a screen alive. - */ public abstract class SessionPart { private Context app; @@ -15,7 +9,6 @@ public abstract class SessionPart { this.app = context.getApplicationContext(); } - /** The application context, valid after {@link #attach}. */ protected final Context app() { return app; } diff --git a/app/src/main/java/com/droiddeck/launcher/core/TarZst.java b/app/src/main/java/com/droiddeck/launcher/core/TarZst.java index 0a1110f23..c00030c01 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/TarZst.java +++ b/app/src/main/java/com/droiddeck/launcher/core/TarZst.java @@ -13,11 +13,6 @@ import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; -/** - * The small zstd tarballs that ship inside the apk (the Turnip driver, the PulseAudio modules). - * The runtime rootfs is not one of these - it needs hard links and symlinks preserved, which - * {@link com.droiddeck.launcher.runtime.LinuxRuntimeInstaller} handles itself. - */ public final class TarZst { private static final String TAG = "TarZst"; @@ -33,7 +28,6 @@ public final class TarZst { } public static boolean extract(InputStream source, File destination) { - //noinspection ResultOfMethodCallIgnored destination.mkdirs(); try (InputStream in = new ZstdCompressorInputStream(new BufferedInputStream(source, 1 << 16)); TarArchiveInputStream tar = new TarArchiveInputStream(in)) { @@ -42,15 +36,13 @@ public final class TarZst { File file = ArchivePaths.inside(destination, entry.getName()); if (file == null) continue; if (entry.isDirectory()) { - //noinspection ResultOfMethodCallIgnored file.mkdirs(); continue; } File parent = file.getParentFile(); - if (parent != null && !parent.isDirectory()) //noinspection ResultOfMethodCallIgnored + if (parent != null && !parent.isDirectory()) parent.mkdirs(); if (entry.isSymbolicLink()) { - //noinspection ResultOfMethodCallIgnored file.delete(); FileUtils.symlink(entry.getLinkName(), file.getAbsolutePath()); continue; @@ -59,7 +51,7 @@ public final class TarZst { try (OutputStream out = new FileOutputStream(file)) { FileUtils.copy(tar, out); } - if ((entry.getMode() & 0111) != 0) //noinspection ResultOfMethodCallIgnored + if ((entry.getMode() & 0111) != 0) file.setExecutable(true, false); } return true; diff --git a/app/src/main/java/com/droiddeck/launcher/core/TextureFiltering.kt b/app/src/main/java/com/droiddeck/launcher/core/TextureFiltering.kt index 97c07289f..66be28cb4 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/TextureFiltering.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/TextureFiltering.kt @@ -5,29 +5,15 @@ import kotlin.math.ln import kotlin.math.max import kotlin.math.min -/** - * Texture filtering for DirectX 9-11 games, as DXVK configuration options - * (d3d9/d3d11.samplerAnisotropy and samplerLodBias). VKD3D-Proton and WineD3D do not read them. - * Published into the game environment and read by DXVK when a game starts, so a change lands on - * the next launch, not the running game. - */ object TextureFiltering { - /** 0 = the game's own filtering; otherwise that anisotropy on every linearly filtered sampler. */ val ANISOTROPY = listOf(0, 2, 4, 8, 16) - /** Negative mip LOD bias added to the game's own: sharper textures at the cost of some shimmer. */ const val LOD_BIAS_OFF = "0" - /** Derived from how far the session is enlarged to the panel (see [autoLodBias]). */ const val LOD_BIAS_AUTO = "auto" val LOD_BIAS = listOf(LOD_BIAS_OFF, LOD_BIAS_AUTO, "-0.25", "-0.5", "-0.75", "-1.0") private const val LOD_BIAS_MIN = -2f - /** - * The mip LOD bias for a picture enlarged by [scale] (panel size over session size, long side - * to long side): -log2(scale), AMD's FSR 1 guidance (a 1.5x enlargement - 720p on a 1080p - * panel - gives -0.58). 0 when the session is not enlarged or the scale is unknown. - */ fun autoLodBias(scale: Float): Float { if (scale.isNaN() || scale <= 1f) return 0f return max((-ln(scale.toDouble()) / ln(2.0)).toFloat(), LOD_BIAS_MIN) @@ -39,7 +25,6 @@ object TextureFiltering { else -> choice.toFloatOrNull()?.let { max(min(it, 0f), LOD_BIAS_MIN) } ?: 0f } - /** The DXVK_CONFIG options for the two settings, "" when both are left to the game. */ fun dxvkOptions(anisotropy: Int, lodBiasChoice: String, autoBias: Float): String { val parts = ArrayList<String>(4) val aniso = if (anisotropy in ANISOTROPY) anisotropy else 0 @@ -49,7 +34,6 @@ object TextureFiltering { } val bias = lodBias(lodBiasChoice, autoBias) if (bias < 0f) { - // Locale.US: DXVK parses "-0.58", never "-0,58". val b = String.format(Locale.US, "%.2f", bias) parts += "d3d9.samplerLodBias = $b" parts += "d3d11.samplerLodBias = $b" diff --git a/app/src/main/java/com/droiddeck/launcher/core/WifiDiscovery.kt b/app/src/main/java/com/droiddeck/launcher/core/WifiDiscovery.kt index 40a6721dc..66cc77f91 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/WifiDiscovery.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/WifiDiscovery.kt @@ -7,11 +7,9 @@ import android.location.LocationManager import android.os.Build import com.droiddeck.launcher.session.SessionPrefs -/** Location is used only for Android's Wi-Fi names and scan results, never coordinates. */ object WifiDiscovery { val permissions = arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) - // targetSdk 28 accepts either grant for Wi-Fi scanning. fun permissionGranted(context: Context): Boolean = permissions.any { context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED } diff --git a/app/src/main/java/com/droiddeck/launcher/core/WirelessAdbFix.kt b/app/src/main/java/com/droiddeck/launcher/core/WirelessAdbFix.kt index 573aea35a..e3f432e18 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/WirelessAdbFix.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/WirelessAdbFix.kt @@ -16,7 +16,6 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.ArrayDeque -/** A narrowly scoped Wireless debugging client for the child-process setting repair. */ object WirelessAdbFix { private const val CERT_FILE = "wireless-adb-cert.pem" private const val KEY_FILE = "wireless-adb-key.pem" @@ -66,7 +65,6 @@ object WirelessAdbFix { return SavedConnection(host, port) } - /** A saved pairing skips the PIN; an unknown connection port can be filled in from Settings. */ fun savedConnectionAddress(context: Context): String? = savedConnection(context)?.let { saved -> val host = if (':' in saved.host) "[${saved.host}]" else saved.host "$host:${saved.port ?: ""}" @@ -80,7 +78,6 @@ object WirelessAdbFix { ) } - /** Finds this paired device's separate TLS connection port from Android's ADB mDNS record. */ fun isLocalAddress(address: InetAddress): Boolean = runCatching { if (address.isLoopbackAddress) return true NetworkInterface.getNetworkInterfaces()?.toList().orEmpty().any { network -> @@ -184,7 +181,6 @@ object WirelessAdbFix { saveConnection(context, host, port) } - /** Tries the app's saved pairing without asking for a new pairing code. */ fun setUsingSavedPairing(context: Context, enabled: Boolean) { val appContext = context.applicationContext val saved = savedConnection(appContext) diff --git a/app/src/main/java/com/droiddeck/launcher/core/ZramSupport.kt b/app/src/main/java/com/droiddeck/launcher/core/ZramSupport.kt index e81ed9681..51c3b2494 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/ZramSupport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/ZramSupport.kt @@ -5,7 +5,6 @@ import android.content.Context import android.os.Build import java.io.File -/** Whether the client's memory can be paged out to compressed swap: MADV_PAGEOUT (Linux 5.4) and a zram or other swap device. */ object ZramSupport { private const val GIB = 1L shl 30 diff --git a/app/src/main/java/com/droiddeck/launcher/files/AppFilesProvider.kt b/app/src/main/java/com/droiddeck/launcher/files/AppFilesProvider.kt index c5bbef88e..0f794ce0d 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/AppFilesProvider.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/AppFilesProvider.kt @@ -23,15 +23,6 @@ import java.nio.file.Path import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes -/** - * The app's own data folder (the Linux runtime, settings, logs) in Android's file picker, under the - * app's name beside the phone's storage - what MT Manager's "documents provider" injection does, as - * part of the app. Only the system's documents UI can bind it (MANAGE_DOCUMENTS); another app sees - * a file only when the user picks it there. - * - * A document id is the file's absolute path, and nothing outside the data folder is ever handed out - * or accepted: every id is resolved and checked to lie inside it. - */ class AppFilesProvider : DocumentsProvider() { private lateinit var base: File private lateinit var baseCanonical: String @@ -65,8 +56,6 @@ class AppFilesProvider : DocumentsProvider() { override fun queryChildDocuments(parentDocumentId: String, projection: Array<String>?, sortOrder: String?): Cursor { val result = MatrixCursor(projection ?: DOCUMENT_COLUMNS) - // A link in the runtime that leads out of the data folder (/etc/localtime and the like) is - // left out rather than listed as something that cannot be opened. fileFor(parentDocumentId).listFiles()?.filter { inside(it) }?.forEach { addRow(result, it) } return result } @@ -104,10 +93,6 @@ class AppFilesProvider : DocumentsProvider() { } } - /** - * Deletes a file or a folder and what is in it without ever following a link: the runtime holds - * links to places outside it, and deleting a folder must remove the link, not what it points at. - */ private fun deleteTree(root: Path) { if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS)) { Files.delete(root) @@ -158,7 +143,6 @@ class AppFilesProvider : DocumentsProvider() { val needle = query.lowercase() val pending = ArrayDeque(listOf(base)) var visited = 0 - // The runtime is tens of thousands of files: a bounded walk, not the whole tree. while (pending.isNotEmpty() && result.count < 50 && visited++ < 20000) { val f = pending.removeFirst() if (!inside(f)) continue @@ -168,7 +152,6 @@ class AppFilesProvider : DocumentsProvider() { return result } - /** The file an id names, when it exists and lies inside the data folder (links followed). */ private fun fileFor(documentId: String): File { val f = File(documentId) if (!f.isAbsolute || !inside(f)) throw FileNotFoundException("$documentId is outside the app's files") @@ -177,7 +160,6 @@ class AppFilesProvider : DocumentsProvider() { } private fun inside(f: File): Boolean { - // The folder's own entries are listed as they are; a link is judged by where it leads. val abs = f.absoluteFile.normalize().path if (abs != base.absolutePath && !abs.startsWith(base.absolutePath + "/")) return false val real = runCatching { f.canonicalPath }.getOrNull() ?: return false @@ -225,11 +207,6 @@ class AppFilesProvider : DocumentsProvider() { Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, ) - /** - * The name in the picker. The same app ships under other package names (for the performance - * modes some phones give only named apps); each says which it is, so four installs are not - * four identical "DroidDeck" entries. - */ fun titleFor(context: Context): String { val name = context.getString(R.string.app_name) return when (context.packageName) { diff --git a/app/src/main/java/com/droiddeck/launcher/files/CollapsibleRail.kt b/app/src/main/java/com/droiddeck/launcher/files/CollapsibleRail.kt index 30ae1537b..665d063fd 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/CollapsibleRail.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/CollapsibleRail.kt @@ -49,38 +49,26 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -/** One tappable entry in a [CollapsibleRail] (a container tab, a file-manager location, …). */ data class RailItem( val label: String, val icon: ImageVector, val selected: Boolean, - /** >0 shows a small accent count badge on the item's icon (expanded and collapsed). */ val badge: Int = 0, - /** The under-icon label when the rail is collapsed, for names too long to fit; null derives one. */ val shortLabel: String? = null, - // onClick stays LAST so existing call sites can pass it as a trailing lambda; badge is an - // optional named param before it (default 0). val onClick: () -> Unit, ) -/** A group of [RailItem]s under an optional small section header (STORAGE / QUICK / FAVORITES …). */ data class RailSection(val header: String?, val items: List<RailItem>) -/** - * The tiny under-icon label for a COLLAPSED rail item. The rail width never changes, so labels must - * fit the narrow (~58dp) column: multi-word names wrap to a second line; single words too long to - * fit are abbreviated; short ones pass through. (Explicit abbreviations first, then a length guard.) - */ private fun collapsedLabel(label: String): String { when (label) { - "ENVIROMENT" -> return "ENVIRON" // the app's existing (mis)spelling, kept uppercase + "ENVIROMENT" -> return "ENVIRON" "WIN COMPONENTS" -> return "WIN COMP" } - if (label.contains(' ')) return label // multi-word → wraps to 2 lines + if (label.contains(' ')) return label return if (label.length > 9) label.take(8) + "…" else label } -/** A rail item's 20dp icon, with a small accent count badge on its corner when [badge] > 0. */ @Composable private fun RailItemIcon(item: RailItem, tint: androidx.compose.ui.graphics.Color) { if (item.badge > 0) { @@ -99,21 +87,10 @@ private fun RailItemIcon(item: RailItem, tint: androidx.compose.ui.graphics.Colo } } -/** A secondary link shown under the header (e.g. "What is all this?", "Reset to app defaults"). */ data class RailLink(val label: String, val icon: ImageVector, val onClick: () -> Unit) -/** - * State for a [CollapsibleRail], with the app-wide unified rule: - * - PORTRAIT → always collapsed (icon-only), the toggle is HIDDEN and it can never expand. - * - LANDSCAPE → expanded by default; the user's collapse choice is remembered PER-SCREEN - * ([screenKey]) and is NOT overridden by rotation. portrait→landscape restores that choice. - * - * Backed by rememberSaveable (survives process death) and by the fact the app UI activity keeps - * orientation in configChanges (no recreate) so the selected item / fields / scroll are preserved. - */ class RailState internal constructor( val collapsed: Boolean, - /** True in portrait: the rail is locked collapsed and must not show an expand toggle. */ val portraitLocked: Boolean, val onToggle: () -> Unit, ) @@ -121,15 +98,12 @@ class RailState internal constructor( @Composable fun rememberRailState(screenKey: String): RailState { val context = LocalContext.current - // The same file the File Manager keeps its own browse settings in (no androidx.preference here). val prefs = remember { context.getSharedPreferences("file_manager", android.content.Context.MODE_PRIVATE) } val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE val choseKey = "rail_${screenKey}_userChose" val collapsedKey = "rail_${screenKey}_collapsed" - // Keep the landscape choice in saveable state regardless of orientation so portrait→landscape - // restores it. In portrait we simply report collapsed+locked and ignore it. var userChose by rememberSaveable(screenKey) { mutableStateOf(prefs.getBoolean(choseKey, false)) } var landscapeCollapsed by rememberSaveable(screenKey) { mutableStateOf(if (prefs.getBoolean(choseKey, false)) prefs.getBoolean(collapsedKey, false) else false) @@ -152,16 +126,6 @@ fun rememberRailState(screenKey: String): RailState { ) } -/** - * The shared collapsible left rail (mockup "Option 3"): app glyph + title, optional links, and the - * navigation items, replacing a top tab bar so content runs full height beside it. Width animates - * 190dp ↔ 58dp. When collapsed it is icon-only (the caller surfaces the active item's name over the - * content). Used by the container editors, File Manager and Save Manager. - * - * [footer] is an optional slot pinned to the bottom of the rail (a weighted spacer pushes it down), - * for a persistent per-screen summary (e.g. the Save Manager's "N games need syncing" line). The - * caller renders whatever it wants there and can key it off [RailState.collapsed] to go icon-only. - */ @Composable fun CollapsibleRail( state: RailState, @@ -170,12 +134,7 @@ fun CollapsibleRail( modifier: Modifier = Modifier, links: List<RailLink> = emptyList(), footer: (@Composable ColumnScope.() -> Unit)? = null, - // Optional header glyph (a themed drawable). When null the rail shows its plain solid square. - // Used by the container editors to show the wine-glass container icon; other rails leave it null. @DrawableRes headerIcon: Int? = null, - // When true each item gets a rounded outlined-button look (the selected one takes the accent - // border + accent-dim fill), matching the File Manager's "New Folder" button. Off by default so - // the container/save rails keep their lighter flat rows (5 stacked outlined tabs read too heavy). outlinedItems: Boolean = false, ) { val collapsed = state.collapsed @@ -187,7 +146,6 @@ fun CollapsibleRail( .fillMaxHeight() .background(MaterialTheme.colorScheme.surfaceContainerHigh) ) { - // ── Header: app glyph + title + collapse handle (handle hidden when portrait-locked) ── Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = if (collapsed) Arrangement.Center else Arrangement.Start, @@ -228,14 +186,12 @@ fun CollapsibleRail( } } } - // Collapsed (landscape only) → an expand handle; portrait is locked with no handle. if (collapsed && !state.portraitLocked) { IconButton(onClick = state.onToggle, modifier = Modifier.fillMaxWidth().height(24.dp)) { Text("››", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 15.sp) } } - // ── Links ── if (links.isNotEmpty()) { if (collapsed) { Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { @@ -258,8 +214,6 @@ fun CollapsibleRail( HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f)) - // ── Sections + items (SCROLLABLE so a tall rail - e.g. many File Manager favourites - is - // fully reachable in both orientations; the header above and footer below stay pinned) ── Column( modifier = Modifier .weight(1f) @@ -283,8 +237,6 @@ fun CollapsibleRail( } section.items.forEach { item -> val active = item.selected - // Outlined variant: each item is a rounded outlined button; selected = accent border + - // accent-dim fill. Flat variant (default): a simple accent-tinted highlight for selected. val shape = RoundedCornerShape(9.dp) val base = Modifier.fillMaxWidth() val styled = if (outlinedItems) { @@ -308,9 +260,6 @@ fun CollapsibleRail( val iconTint = if (active) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant val labelColor = if (active) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant if (collapsed) { - // Icon-only width is unchanged - a TINY label goes UNDER the icon (2 lines max, - // centre-aligned, wrapping/abbreviated) so every button stays identifiable without - // widening the rail. Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = styled, @@ -350,9 +299,8 @@ fun CollapsibleRail( } } - } // end scrollable section list (its weight fills the gap so the footer stays pinned) + } - // ── Optional pinned footer (e.g. the sync summary) ── if (footer != null) { HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f)) footer() @@ -360,16 +308,6 @@ fun CollapsibleRail( } } -/** - * Portrait counterpart to [CollapsibleRail]: the same navigation [items] laid out as a horizontal - * icon-over-label bar pinned across the top, with an accent underline under the selected one. Mirrors - * the container editor's top tab bar so every editor reads the same - pin this in portrait wherever a - * [CollapsibleRail] is used in landscape. Optional [links] (Help / Reset …) are appended after a - * faint separator, exactly like the rail's links. - * - * (The container editor still carries its own private copy of this bar; new screens should prefer - * this shared one, and the container editor can migrate to it later.) - */ @Composable fun RailTopTabs( items: List<RailItem>, @@ -391,7 +329,6 @@ fun RailTopTabs( ) } if (links.isNotEmpty()) { - // Faint separator: everything past here is an action button, not a tab. Box( modifier = Modifier .padding(horizontal = 2.dp) @@ -408,8 +345,6 @@ fun RailTopTabs( } } -/** One cell of [RailTopTabs] - icon over a small label, with an accent underline (zero-width when - * unselected, so every cell keeps the same height). Mirrors the container editor's TopCell. */ @Composable private fun TopTabCell( icon: ImageVector, diff --git a/app/src/main/java/com/droiddeck/launcher/files/FavoritesStore.kt b/app/src/main/java/com/droiddeck/launcher/files/FavoritesStore.kt index de38b7a4f..8255b561b 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FavoritesStore.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FavoritesStore.kt @@ -3,15 +3,6 @@ package com.droiddeck.launcher.files import android.content.Context import org.json.JSONArray -/** - * Persists the File Manager's favourite / bookmarked directories. - * - * Stores an ordered (insertion-order) list of absolute path strings as a JSON array - * under "file_manager_favorites" in SharedPreferences "file_manager_prefs". Only the - * absolute path is kept - everything shown on a favourite card (drive label, container - * name, display path) is derived live, so a renamed or deleted container resolves - * correctly for free. - */ object FavoritesStore { private const val PREFS_NAME = "file_manager_prefs" @@ -20,7 +11,6 @@ object FavoritesStore { private fun prefs(context: Context) = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - /** Ordered list of favourited absolute paths (insertion order). */ fun list(context: Context): List<String> { val raw = prefs(context).getString(KEY, null) ?: return emptyList() return runCatching { @@ -31,7 +21,6 @@ object FavoritesStore { fun isFavorite(context: Context, path: String): Boolean = list(context).contains(path) - /** Add [path] if not already present (deduped); keeps insertion order. */ fun add(context: Context, path: String) { val current = list(context) if (current.contains(path)) return @@ -44,7 +33,6 @@ object FavoritesStore { save(context, current.filterNot { it == path }) } - /** Toggle [path]; returns true if it is now a favourite, false if it was removed. */ fun toggle(context: Context, path: String): Boolean = if (isFavorite(context, path)) { remove(context, path); false diff --git a/app/src/main/java/com/droiddeck/launcher/files/FileAttributes.kt b/app/src/main/java/com/droiddeck/launcher/files/FileAttributes.kt index 60c359bf4..43fc55d71 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FileAttributes.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FileAttributes.kt @@ -4,30 +4,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import java.io.File -// Reading and setting a file's read-only and hidden attributes (the DOS attribute xattr). -// ── DOS file attributes (Read-only / Hidden) ── -// The in-container file manager (wfm.exe) exposes these in its Properties dialog; this mirrors the -// same two toggles for files browsed from the app side. No root - we only touch permission bits the -// app owns (container files) and the Wine DOS-attribute xattr. -// FILE_ATTRIBUTE_HIDDEN, as Wine encodes it in the user.DOSATTRIB extended attribute. private const val FILE_ATTRIBUTE_HIDDEN = 0x2 private const val DOSATTRIB_XATTR = "user.DOSATTRIB" -// Snapshot of a file's two toggleable attributes. hiddenSupported is false when the underlying -// filesystem can't store the DOSATTRIB xattr (the FUSE /storage volumes) - the Hidden toggle is then -// disabled while Read-only keeps working. internal data class FileAttrState( val readOnly: Boolean, val hidden: Boolean, val hiddenSupported: Boolean, ) -// Wine (and Samba) store user.DOSATTRIB as an ASCII hex string ("0x22"), sometimes with trailing -// Samba fields after a separator. We only need the leading DOS-attribute hex, so read from the "0x" -// prefix and stop at the first non-hex character. private fun parseDosAttrib(raw: ByteArray): Int { val s = String(raw, Charsets.US_ASCII).trim() val hex = if (s.startsWith("0x") || s.startsWith("0X")) s.substring(2) else s @@ -35,11 +23,8 @@ private fun parseDosAttrib(raw: ByteArray): Int { return digits.toIntOrNull(16) ?: 0 } -// Read the current read-only + Wine-hidden state. Never throws: an xattr-unsupported filesystem -// comes back hiddenSupported=false (Hidden toggle disabled), Read-only always resolves. internal fun readFileAttrs(file: File): FileAttrState { val path = file.absolutePath - // Read-only reflects the OWNER write bit - that's what Wine maps to FILE_ATTRIBUTE_READONLY. val readOnly = runCatching { (android.system.Os.stat(path).st_mode and android.system.OsConstants.S_IWUSR) == 0 }.getOrDefault(!file.canWrite()) @@ -49,8 +34,6 @@ internal fun readFileAttrs(file: File): FileAttrState { try { hidden = (parseDosAttrib(android.system.Os.getxattr(path, DOSATTRIB_XATTR)) and FILE_ATTRIBUTE_HIDDEN) != 0 } catch (e: android.system.ErrnoException) { - // ENODATA: xattr namespace works, the attribute just isn't set yet -> still toggleable. - // ENOTSUP/EOPNOTSUPP/anything else: the fs can't store it -> disable the Hidden toggle. supported = e.errno == android.system.OsConstants.ENODATA } catch (e: Exception) { supported = false @@ -58,10 +41,6 @@ internal fun readFileAttrs(file: File): FileAttrState { return FileAttrState(readOnly, hidden, supported) } -// Toggle read-only by flipping the write bits, preserving every other permission bit. Setting -// read-only clears owner/group/other write (so Wine sees FILE_ATTRIBUTE_READONLY regardless of which -// write bit it checks); clearing it restores owner write. Falls back to File.setWritable if chmod is -// somehow refused. Returns true on success. internal fun setReadOnly(file: File, readOnly: Boolean): Boolean = runCatching { val path = file.absolutePath val mode = android.system.Os.stat(path).st_mode @@ -73,10 +52,6 @@ internal fun setReadOnly(file: File, readOnly: Boolean): Boolean = runCatching { true }.getOrElse { file.setWritable(!readOnly, true) } -// Flip Wine's DOS hidden bit in user.DOSATTRIB, preserving the other DOS-attribute bits -// (archive/system/read-only) already encoded there, and synthesizing a minimal value when absent. -// Writes Wine's own "0x%x" format, which Wine reads back natively. Returns false when the fs can't -// store the xattr, so the caller can disable just the Hidden toggle. internal fun setHidden(file: File, hidden: Boolean): Boolean = try { val path = file.absolutePath var attr = try { diff --git a/app/src/main/java/com/droiddeck/launcher/files/FileManagerActivity.kt b/app/src/main/java/com/droiddeck/launcher/files/FileManagerActivity.kt index 2395d1c80..27cb9dbce 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FileManagerActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FileManagerActivity.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier import com.droiddeck.launcher.ui.DroidDeckTheme import java.io.File -/** The File Manager as a screen of its own, from the Files tile on the main screen. */ class FileManagerActivity : ComponentActivity() { override fun attachBaseContext(newBase: android.content.Context) { super.attachBaseContext(newBase) diff --git a/app/src/main/java/com/droiddeck/launcher/files/FileManagerItems.kt b/app/src/main/java/com/droiddeck/launcher/files/FileManagerItems.kt index 8efe2c6cd..45c8a92c8 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FileManagerItems.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FileManagerItems.kt @@ -77,23 +77,16 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -// The File Manager's rows, tiles, favourites and context menu. -// Semantic identity colours for the favourite-card drive badge. Intentionally NOT theme -// accent colours - they identify the storage source at a glance. Returns (background, foreground). private fun badgeColors(loc: FavLocation): Pair<Color, Color> { val white = Color(0xFFFFFFFF) return when { - loc.storage == FavStorage.INTERNAL -> Color(0xFF2E5FB0) to white // blue - loc.storage == FavStorage.SD -> Color(0xFF2E7D32) to white // green - else -> Color(0xFF555555) to white // grey + loc.storage == FavStorage.INTERNAL -> Color(0xFF2E5FB0) to white + loc.storage == FavStorage.SD -> Color(0xFF2E7D32) to white + else -> Color(0xFF555555) to white } } -// The context-menu item list shared by the list rows (FileItemRow) and the grid tiles -// (FileGridTile), so both open the identical menu. Gating (isDir/canRun/isInno/looksLikeArchive) is -// recomputed from [file] here - one source of truth for what each item shows. Every item dismisses -// the menu first, then runs its action. @Composable private fun FileContextMenuItems( file: File, @@ -114,15 +107,12 @@ private fun FileContextMenuItems( onClick = { onDismissMenu(); onSelect() }, ) MenuItemDivider() - // Properties: basic info + Read-only / Hidden toggles, for ANY file or folder (handy for config - // files like .txt/.cfg/.ini). Kept near the top since it's a common reason to open this menu. DropdownMenuItem( text = { Text(stringResource(R.string.fm_properties)) }, leadingIcon = { Icon(Icons.Filled.Info, null, tint = MaterialTheme.colorScheme.primary) }, onClick = { onDismissMenu(); onProperties() }, ) MenuItemDivider() - // Favorites are directories - only folders get the pin toggle. if (isDir) { DropdownMenuItem( text = { Text(if (isFavorite) stringResource(R.string.fm_favorite_remove) else stringResource(R.string.fm_favorite_add)) }, @@ -189,11 +179,8 @@ internal fun FileItemRow( val context = LocalContext.current val isDir = file.isDirectory val isExe = !isDir && file.name.lowercase().let { it.endsWith(".exe") || it.endsWith(".bat") || it.endsWith(".msi") || it.endsWith(".sh") } - // Image files show a real thumbnail instead of the generic file icon (handy when picking a - // wallpaper/icon). Coil sizes the decode to the 36dp slot and caches it, so scrolling stays smooth. val isImage = !isDir && file.extension.lowercase() in IMAGE_THUMB_EXTS - // For real PE executables, try to pull out the embedded application icon (async, off the main thread). var exeIcon by remember(file.absolutePath) { mutableStateOf<ImageBitmap?>(null) } if (!isDir && file.name.lowercase().endsWith(".exe")) { LaunchedEffect(file.absolutePath) { @@ -207,8 +194,6 @@ internal fun FileItemRow( .fillMaxWidth() .padding(horizontal = 12.dp, vertical = 3.dp) .combinedClickable( - // In selection mode a tap toggles instead of opening, so you can rattle through a - // folder without long-pressing every single row. onClick = { if (selectionMode) onToggleSelect() else onTap() }, onLongClick = onLongPress, ), @@ -231,7 +216,6 @@ internal fun FileItemRow( Spacer(Modifier.width(4.dp)) } when { - // Show the executable's own embedded icon when we managed to extract one. exeIcon != null -> Image( bitmap = exeIcon!!, contentDescription = null, @@ -249,7 +233,6 @@ internal fun FileItemRow( tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(if (compact) 24.dp else 36.dp), ) - // Real image preview. Falls back to the generic file icon while loading or on decode failure. isImage -> AsyncImage( model = file, contentDescription = null, @@ -313,8 +296,6 @@ internal fun FileItemRow( } } -// The ".." row at the top of a folder: one level up, styled like a folder row so it reads as part -// of the list and takes controller focus the same way. @Composable internal fun ParentFolderRow(compact: Boolean = false, onTap: () -> Unit) { Card( @@ -354,8 +335,6 @@ internal fun ParentFolderRow(compact: Boolean = false, onTap: () -> Unit) { } } -// Dedicated Favorites list that replaces the file list while the star toggle is on. -// Reads the store keyed on [favTick] so it recomposes after any pin/unpin. @Composable internal fun FavoritesList( currentDir: File, @@ -431,9 +410,6 @@ internal fun FavoritesList( } } -// A single favourite - matches the FileItemRow card style (surfaceContainer + outline + -// RoundedCornerShape(10.dp)). Shows the folder name, a coloured drive badge + origin text, -// and the full display path; tapping jumps into it, the filled star unpins. @Composable private fun FavoriteCard( file: File, @@ -472,7 +448,6 @@ private fun FavoriteCard( overflow = TextOverflow.Ellipsis, ) Spacer(Modifier.height(2.dp)) - // Origin line: coloured drive badge + source description. Row(verticalAlignment = Alignment.CenterVertically) { Text( text = stringResource(loc.driveLabel), @@ -517,13 +492,6 @@ private fun FavoriteCard( } } -/** - * One entry in the File Manager's grid view: a big thumbnail with the name under it. - * - * Deliberately drops size and date - at this width they truncate to noise. The grid is for - * recognising things by sight (screenshots, covers, game folders); the list stays the view for - * reading details. - */ @OptIn(ExperimentalFoundationApi::class) @Composable internal fun FileGridTile( @@ -554,7 +522,6 @@ internal fun FileGridTile( if (bmp != null) exeIcon = bmp.asImageBitmap() } } - // The tile has no ⋮ button - long-press opens this menu, anchored to the Box around the Card. Box { Card( modifier = Modifier @@ -608,8 +575,6 @@ internal fun FileGridTile( } } Spacer(Modifier.height(4.dp)) - // Always reserve two lines (issue #475): a one-line name used to make its card shorter than - // its neighbours, so grid rows had ragged heights. Fixed min/max keeps every tile the same size. Text( file.name, color = MaterialTheme.colorScheme.onSurface, diff --git a/app/src/main/java/com/droiddeck/launcher/files/FileManagerModel.kt b/app/src/main/java/com/droiddeck/launcher/files/FileManagerModel.kt index eac7dc463..c8bcf1ee7 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FileManagerModel.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FileManagerModel.kt @@ -12,33 +12,8 @@ import androidx.annotation.StringRes import com.droiddeck.launcher.R import java.io.File -// The File Manager's non-UI helpers: sorting, paths, conflict choices and favourite locations. - -/* - * Bannerlator's File Manager (ui/screens/FileManagerScreen.kt), carried over whole: the same - * browsing, grid and list, sorting, search, multi-select, copy/cut/paste with conflicts and - * progress, rename, delete, new folder, properties, favourites and the locations rail, and the - * same pick modes. What this app has no use for is not here: running a file in a Wine container, - * adding it to a container's shortcuts, the Drive C: / Drive Z: locations and the archive - * unpacker that installs into a prefix. Everything else is line for line the same so a fix in - * either app carries to the other. - */ - -/** - * What to do when a pasted item already exists at the destination. - * - * OVERWRITE and MERGE resolve to the same call - `copyWithProgress` recurses into an existing - * directory and truncates existing files - but they mean different things to the user, so both are - * offered and the wording is chosen per item type (files overwrite, folders merge). - */ enum class ConflictChoice { OVERWRITE, MERGE, KEEP_BOTH, SKIP } -/** - * Shortens a path from the LEFT, keeping whole segments. - * - * Compose's TextOverflow can only ellipsise the tail, which for a path throws away the part that - * matters - `/storage/emulated/0/Games/Racing/Dir…` tells you nothing about where you are. - */ internal fun elidePathStart(path: String, max: Int): String { if (path.length <= max) return path val parts = path.split('/').filter { it.isNotEmpty() } @@ -50,12 +25,9 @@ internal fun elidePathStart(path: String, max: Int): String { return if (out.isEmpty()) "…" + path.takeLast(max - 1) else "…$out" } -/** Folder-first ordering stays fixed across both sort directions. */ internal fun comparatorFor(sortBy: String, desc: Boolean): Comparator<File> { val inner: Comparator<File> = when (sortBy) { "date" -> compareBy { it.lastModified() } - // Directory length() is meaningless, so folders sort by name within the size ordering - // instead of pretending to have one. "size" -> compareBy { if (it.isDirectory) -1L else it.length() } else -> compareBy { it.name.lowercase() } } @@ -63,27 +35,23 @@ internal fun comparatorFor(sortBy: String, desc: Boolean): Comparator<File> { return compareBy<File> { if (it.isDirectory) 0 else 1 }.then(directed) } -// Image extensions that get a real thumbnail (via Coil) instead of the generic file icon. internal val IMAGE_THUMB_EXTS = setOf("jpg", "jpeg", "png", "webp", "bmp", "gif") -// True when [child] is [ancestor] itself or lives anywhere inside it. internal fun isWithin(child: File, ancestor: File): Boolean { val c = runCatching { child.canonicalPath }.getOrDefault(child.absolutePath) val a = runCatching { ancestor.canonicalPath }.getOrDefault(ancestor.absolutePath) return c == a || c.startsWith(a + File.separator) } -// ── Favorites: origin resolution ── enum class FavStorage { INTERNAL, SD, OTHER } data class FavLocation( val storage: FavStorage, - @StringRes val driveLabel: Int, // Internal, SD card, or Storage - val displayPath: String // the unix absolute path + @StringRes val driveLabel: Int, + val displayPath: String ) -// Resolve where [file] lives (storage source + a friendly label) by prefix-matching its path. fun describeLocation(file: File): FavLocation { val abs = file.absolutePath @@ -99,7 +67,6 @@ fun describeLocation(file: File): FavLocation { } } - // The Linux system's root (the app's files/linuxfs, the sessions' /) and anything under it. if (Regex("/files/linuxfs(/|$)").containsMatchIn(abs)) { return FavLocation(FavStorage.OTHER, R.string.fm_drive_linux, abs) } diff --git a/app/src/main/java/com/droiddeck/launcher/files/FileManagerScreen.kt b/app/src/main/java/com/droiddeck/launcher/files/FileManagerScreen.kt index 46f2f708b..9fbd251ff 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FileManagerScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FileManagerScreen.kt @@ -98,12 +98,7 @@ import java.util.Date @OptIn(ExperimentalMaterial3Api::class) @Composable fun FileManagerScreen( - // Pick mode (issue #73): reuse this File Manager as a themed file picker. When on, editing/run - // features are gated off and tapping a matching file returns it via [onPick]. Defaults keep the - // full-featured File Manager nav destination unchanged. pickMode: Boolean = false, - // Directory-pick mode (issue #70): only folders are listed, files are hidden, and a - // "Select this folder" action returns the current directory via [onPick]. Implies pickMode. pickDirMode: Boolean = false, pickExtensions: List<String> = emptyList(), initialDir: File? = null, @@ -113,7 +108,6 @@ fun FileManagerScreen( val context = LocalContext.current val scope = rememberCoroutineScope() - // Only matching files are shown in pick mode (directories are always shown). Empty = all files. val lowerExts = remember(pickExtensions) { pickExtensions.map { it.lowercase() } } fun matchesPickExt(file: File): Boolean { if (lowerExts.isEmpty()) return true @@ -124,18 +118,10 @@ fun FileManagerScreen( val pickPrefs = remember { context.getSharedPreferences("file_manager", android.content.Context.MODE_PRIVATE) } val browsePrefs = pickPrefs val rootDir = remember { - // Both modes: honour an explicit caller-supplied start dir (e.g. Log Manager's game-log - // folder), else open at the INTERNAL STORAGE ROOT. Selection screens (drive-folder pick, - // local component pick, imports) previously defaulted to Download which - combined with the - // currentRoot floor below - trapped users in Download with no way up (reported bug). initialDir?.takeIf { it.isDirectory } ?: File("/storage/emulated/0") } var currentDir by remember { mutableStateOf(rootDir) } - // The up/back FLOOR - back + the up-arrow are disabled while currentDir == currentRoot. It MUST be - // the VOLUME ROOT of the start dir (internal /storage/emulated/0, or an SD card /storage/XXXX-XXXX), - // NOT the start dir itself: otherwise opening at any subfolder disables up/back and traps the user - // there. (Mirrors the volume-root logic in favLocationOf above.) var currentRoot by remember { val abs = rootDir.absolutePath val internal = "/storage/emulated/0" @@ -145,8 +131,6 @@ fun FileManagerScreen( val name = abs.removePrefix("/storage/").substringBefore('/') if (name.isNotEmpty() && name != "emulated" && name != "self") File("/storage/$name") else rootDir } - // A game's own folder or Proton prefix lives in the app's private storage, which has no - // volume above it: floor at the app's files dir so up/back still reach the library roots. else -> { val appFiles = runCatching { context.filesDir.canonicalPath }.getOrNull() generateSequence(rootDir) { it.parentFile } @@ -159,40 +143,26 @@ fun FileManagerScreen( var entries by remember { mutableStateOf(listOf<File>()) } var selectedEntry by remember { mutableStateOf<File?>(null) } var showMenuFor by remember { mutableStateOf<File?>(null) } - // Clipboard holds a LIST so one paste can carry a whole selection. Cut/copy semantics are a - // flag on the batch rather than per item - mixing the two in one clipboard has no sane meaning. var clipboardFiles by remember { mutableStateOf<List<File>>(emptyList()) } var isCutOperation by remember { mutableStateOf(false) } - // Multi-select. Keyed by absolute path rather than File so a directory reload (which builds - // fresh File objects) doesn't silently drop the selection. var selectionMode by remember { mutableStateOf(false) } var selectedPaths by remember { mutableStateOf<Set<String>>(emptySet()) } - // Paste conflict resolution, surfaced from the IO coroutine and answered by the dialog. var pendingConflict by remember { mutableStateOf<File?>(null) } var conflictChoice by remember { mutableStateOf<ConflictChoice?>(null) } var conflictApplyToAll by remember { mutableStateOf(false) } - // Set while a copy/move runs so the progress UI can offer a cancel. var operationJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) } var pendingBulkDelete by remember { mutableStateOf<List<File>>(emptyList()) } - // Browse controls. Persisted so the list doesn't reset its order every time you open a folder. var searchQuery by remember { mutableStateOf("") } 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)) } var showHidden by remember { mutableStateOf(browsePrefs.getBoolean("fmShowHidden", true)) } var showSortMenu by remember { mutableStateOf(false) } - // View mode: list of cards (default) or a thumbnail grid. Density applies to the list only - - // a grid tile has no second line to compact. - // The grid/list toggle is the SOURCE OF TRUTH in BOTH orientations (it drives the view and its - // choice persists across rotation). Grid is the default - most useful in landscape, and in - // portrait GridCells.Adaptive naturally renders fewer columns (~2). Do NOT force portrait to list: - // that broke the toggle on-device (tapping it did nothing in portrait). var gridView by remember { mutableStateOf(browsePrefs.getBoolean("fmGridView", true)) } val showGrid = gridView var compactRows by remember { mutableStateOf(browsePrefs.getBoolean("fmCompactRows", false)) } var showNewFolderDialog by remember { mutableStateOf(false) } var renameTarget by remember { mutableStateOf<File?>(null) } - // Properties sheet (basic info + Read-only / Hidden toggles) target; null when closed. var propertiesTarget by remember { mutableStateOf<File?>(null) } var isOperationRunning by remember { mutableStateOf(false) } var operationLabel by remember { mutableStateOf("") } @@ -201,24 +171,16 @@ fun FileManagerScreen( val listState = rememberLazyListState() val pullState = rememberPullToRefreshState() - // Favorites view: when on, a dedicated bookmarks list replaces the file list. - // favTick is bumped on any add/remove/toggle so the favorites view + per-row star recompute. var showFavorites by remember { mutableStateOf(false) } var favTick by remember { mutableIntStateOf(0) } - // resetScroll: jump to the top of the list (true for navigation; false for in-place reloads - // after delete/paste/rename/refresh so the user keeps their scroll position). fun loadDirectory(dir: File, resetScroll: Boolean = true) { currentDir = dir - // Remember the browsed directory so the next pick resumes here. if (pickMode) pickPrefs.edit().putString("lastFilePickerDir", dir.absolutePath).apply() scope.launch { val list = withContext(Dispatchers.IO) { dir.listFiles()?.toList() - // Dir-pick mode: folders only. File-pick: folders + matching files. Else: all. ?.filter { if (pickDirMode) it.isDirectory else !pickMode || it.isDirectory || matchesPickExt(it) } - // Dotfiles are noise in a storage root (.aya, .$recycle_bin$) but occasionally - // the thing you came for, so it's a toggle rather than a permanent filter. ?.filter { showHidden || !it.name.startsWith(".") } ?.sortedWith(comparatorFor(sortBy, sortDesc)) ?: emptyList() } @@ -227,7 +189,6 @@ fun FileManagerScreen( } } - // Pull-to-refresh: re-list the current directory, keeping scroll position. if (pullState.isRefreshing) { LaunchedEffect(true) { loadDirectory(currentDir, resetScroll = false) @@ -235,19 +196,13 @@ fun FileManagerScreen( } } - // Jump to a drive's root; pins the Back boundary so we don't climb above it. fun openDrive(dir: File) { currentRoot = dir loadDirectory(dir) } - // Open the start dir without openDrive(): that pins the floor to the start dir itself, which - // greyed out up/back whenever the manager was opened at a game's folder. LaunchedEffect(Unit) { loadDirectory(rootDir) } - // System/gesture Back: while the Favorites view is open it closes that first; otherwise - // it goes up one directory. Only at the current drive's root with Favorites closed is it - // disabled, letting Back propagate to close the File Manager. BackHandler(enabled = showFavorites || currentDir != currentRoot) { if (showFavorites) { showFavorites = false @@ -257,15 +212,12 @@ fun FileManagerScreen( if (parent != null && parent.exists()) loadDirectory(parent) } - // One level up, never above the current drive's root: shared by the toolbar arrow and the - // ".." row at the top of the list. val canGoUp = currentDir != currentRoot && currentDir.parentFile?.exists() == true fun goUp() { val parent = currentDir.parentFile if (currentDir != currentRoot && parent != null && parent.exists()) loadDirectory(parent) } - // Resolve a non-colliding destination in [dir] for [name] (foo.txt -> "foo (1).txt"). fun uniqueDestination(dir: File, name: String): File { var candidate = File(dir, name) if (!candidate.exists()) return candidate @@ -291,12 +243,9 @@ fun FileManagerScreen( } } - /** Waits for the user to answer the conflict dialog for [file]; null if they cancelled it. */ suspend fun askConflict(file: File): ConflictChoice? { pendingConflict = file conflictChoice = null - // Poll rather than plumb a CompletableDeferred through Compose state - the dialog answers - // by setting conflictChoice, and this coroutine is already off the critical path. while (pendingConflict != null && conflictChoice == null) kotlinx.coroutines.delay(50) return conflictChoice } @@ -319,12 +268,10 @@ fun FileManagerScreen( var done = 0 for (src in sources) { - // Pasting a folder into itself or its own subtree would recurse forever. if (src.isDirectory && isWithin(dstDir, src)) { failed++ continue } - // Moving into the folder it already sits in is a no-op. if (cut && src.parentFile?.absolutePath == dstDir.absolutePath) { skipped++ continue @@ -336,16 +283,12 @@ fun FileManagerScreen( if (conflictApplyToAll) applyToAll = it } ?: run { skipped++; null } ?: continue when (choice) { - // Overwrite and Merge both paste onto the real destination: copyWithProgress - // recurses into an existing directory and truncates existing files, so the - // two differ only in what the user expects, not in what we call. ConflictChoice.OVERWRITE, ConflictChoice.MERGE -> Unit ConflictChoice.KEEP_BOTH -> dst = uniqueDestination(dstDir, src.name) ConflictChoice.SKIP -> { skipped++; continue } } } - // Progress is per item; with a batch the label carries the overall position. operationLabel = if (sources.size > 1) { context.getString(if (cut) R.string.fm_op_moving_batch else R.string.fm_op_copying_batch, done + 1, sources.size, src.name) } else { @@ -419,8 +362,6 @@ fun FileManagerScreen( } var showDriveMenu by remember { mutableStateOf(false) } - // Re-enumerated whenever we come back to the screen: returning from a container can leave this - // process on a stale storage view, and the volume set has to be re-read rather than cached. var storageTick by remember { mutableIntStateOf(0) } val lifecycleOwner = LocalLifecycleOwner.current DisposableEffect(lifecycleOwner) { @@ -435,7 +376,6 @@ fun FileManagerScreen( com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context).takeIf { File(it, "usr").isDirectory } } - // ── Dialogs ── if (showNewFolderDialog) { var folderName by remember { mutableStateOf("") } @@ -488,8 +428,6 @@ fun FileManagerScreen( FilePropertiesDialog( file = file, onDismiss = { propertiesTarget = null }, - // Attribute changes affect Hidden/read-only which the listing filters/sorts on, so refresh - // in place (keeping scroll) after any toggle applies. onChanged = { loadDirectory(currentDir, resetScroll = false) }, ) } @@ -519,7 +457,6 @@ fun FileManagerScreen( Column { Text(stringResource(R.string.fm_cannot_undo)) Spacer(Modifier.height(6.dp)) - // Name a few so an accidental Select-All is obvious before it's too late. victims.take(5).forEach { Text("• ${it.name}", color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp) } @@ -557,7 +494,6 @@ fun FileManagerScreen( ) } - // Paste conflict - one per colliding item, with "apply to all" for a long batch. pendingConflict?.let { conflict -> val isDir = conflict.isDirectory OutlinedAlertDialog( @@ -605,7 +541,6 @@ fun FileManagerScreen( } Column(modifier = Modifier.fillMaxSize()) { - // ── Pick-mode title ── if (pickMode && !pickerTitle.isNullOrEmpty()) { Text( text = pickerTitle, @@ -618,7 +553,6 @@ fun FileManagerScreen( .padding(horizontal = 16.dp, vertical = 10.dp), ) } - // ── Dir-pick action bar: confirm the currently-browsed folder ── if (pickDirMode) { Button( onClick = { onPick?.invoke(currentDir) }, @@ -631,7 +565,6 @@ fun FileManagerScreen( Text(stringResource(R.string.fm_select_this_folder)) } } - // ── Path bar ── Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -644,12 +577,8 @@ fun FileManagerScreen( } val currentDriveLabel = stringResource(describeLocation(currentDir).driveLabel) - // Dim the drive chip while the Favorites list is open (it's not the active context). val driveChipAlpha = if (showFavorites) 0.45f else 1f Box { - // The drive/location selector opens the drive dropdown, so give it the same outlined - // look as the "New Folder" button + the rail location items - it reads as a button, not - // plain text. Border uses the theme accent token; behaviour unchanged. val driveChipShape = RoundedCornerShape(8.dp) Text( text = " $currentDriveLabel ▾", @@ -720,13 +649,8 @@ fun FileManagerScreen( modifier = Modifier.weight(1f), ) } else if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { - // PORTRAIT: hide the current-folder name - it's redundant with the path bar directly - // below (which shows the full path). The spacer keeps the action icons right-aligned. Spacer(Modifier.weight(1f)) } else { - // LANDSCAPE: the CURRENT FOLDER, not the full path. A path ellipsised on the right - // hides its tail - the only part that says where you are ("…/Games/Racing/Dir…"). The - // full path moves to the line below, where it has room. Text( text = currentDir.name.ifBlank { currentDir.absolutePath }, color = MaterialTheme.colorScheme.onSurface, @@ -739,7 +663,6 @@ fun FileManagerScreen( } if (!showFavorites) { - // Keep New Folder in the toolbar so the file list uses the full height. if (!pickMode) { OutlinedButton( onClick = { showNewFolderDialog = true }, @@ -777,8 +700,6 @@ fun FileManagerScreen( Text(if (sortBy == key) "$label ${if (sortDesc) "↓" else "↑"}" else label) }, onClick = { - // Tapping the active field flips direction; a different - // field switches to it ascending. if (sortBy == key) sortDesc = !sortDesc else { sortBy = key; sortDesc = false } browsePrefs.edit().putString("fmSortBy", sortBy) .putBoolean("fmSortDesc", sortDesc).apply() @@ -809,7 +730,6 @@ fun FileManagerScreen( } } - // Star toggle: open/close the dedicated Favorites list. IconButton(onClick = { showFavorites = !showFavorites }) { if (showFavorites) { Icon(Icons.Filled.Star, stringResource(R.string.fm_hide_favorites), tint = MaterialTheme.colorScheme.primary) @@ -819,7 +739,6 @@ fun FileManagerScreen( } } - // ── Search field ── filters the current folder only; it is not a recursive search. if (showSearch && !showFavorites) { OutlinedTextField( value = searchQuery, @@ -830,7 +749,6 @@ fun FileManagerScreen( ) } - // Free space on the volume being browsed - worth knowing before starting a 60 GB copy. val freeSpace = remember(currentDir.absolutePath, entries) { runCatching { currentDir.usableSpace }.getOrDefault(0L) } @@ -840,14 +758,10 @@ fun FileManagerScreen( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 2.dp), ) { Text( - // Elided from the LEFT: the deepest part of a path is the informative part, so - // when it doesn't fit we drop the /storage/emulated/0 prefix, not the tail. text = elidePathStart(currentDir.absolutePath, 52), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 12.sp, maxLines = 1, - // fill = true: the path takes all remaining width, so the free-space figure - // is pinned to the right edge instead of sliding around with the path length. modifier = Modifier.weight(1f), ) if (freeSpace > 0) { @@ -863,7 +777,6 @@ fun FileManagerScreen( HorizontalDivider(color = MaterialTheme.colorScheme.outline) - // ── Selection bar ── replaces the paste banner while picking items. if (selectionMode) { Row( verticalAlignment = Alignment.CenterVertically, @@ -878,7 +791,6 @@ fun FileManagerScreen( fontSize = 13.sp, modifier = Modifier.weight(1f), ) - // Compact outlined buttons so all five fit one row alongside the count. val selBarPadding = PaddingValues(horizontal = 10.dp, vertical = 2.dp) OutlinedButton( onClick = { @@ -924,7 +836,6 @@ fun FileManagerScreen( } } - // ── Paste banner ── if (clipboardFiles.isNotEmpty()) { Row( verticalAlignment = Alignment.CenterVertically, @@ -948,7 +859,6 @@ fun FileManagerScreen( } } - // ── Progress overlay ── if (isOperationRunning) { Column( modifier = Modifier @@ -966,8 +876,6 @@ fun FileManagerScreen( overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f), ) - // A multi-gigabyte copy onto a slow card is exactly when you discover you - // picked the wrong folder; without this the only way out was killing the app. if (operationJob != null) { TextButton(onClick = { operationJob?.cancel() @@ -996,10 +904,6 @@ fun FileManagerScreen( } } - // ── Left locations rail (mockup Option 2) + content ── - // Shared collapsible rail: landscape expanded by default, portrait collapsed icon-only. Not - // shown in pick mode (the themed picker keeps its slim layout). Built each recompose (cheap) - // so it tracks the current drive/favourites without stale click lambdas. val fmRailState = rememberRailState("filemanager") fun locItem(label: String, icon: androidx.compose.ui.graphics.vector.ImageVector, dir: File, shortLabel: String? = null) = RailItem(label, icon, !showFavorites && currentRoot.absolutePath == dir.absolutePath, shortLabel = shortLabel) { @@ -1012,15 +916,12 @@ fun FileManagerScreen( showFavorites = false; if (d.readable) openDrive(d.dir) }) } - // The Linux system the sessions run in: its own / (the desktop's root), once installed. linuxRoot?.let { add(locItem(stringResource(R.string.fm_drive_linux), Icons.Filled.Terminal, it)) } } val quickItems = buildList { File("/storage/emulated/0/Download").takeIf { it.isDirectory }?.let { add(locItem(stringResource(R.string.fm_rail_downloads), Icons.Filled.Download, it, stringResource(R.string.fm_rail_downloads_short))) } - // The ROMs folder chosen on the main screen: what the session shows as /root/ROMs. SessionPrefs.romsDir(context).takeIf { it.isNotEmpty() }?.let(::File)?.takeIf { it.isDirectory } ?.let { add(locItem(stringResource(R.string.fm_rail_roms), Icons.Filled.SportsEsports, it)) } - // The app's own logs folder: private, which this file manager (the app itself) can read. com.droiddeck.launcher.runtime.LinuxRuntime.logDir(context).takeIf { it.isDirectory }?.let { add(locItem(stringResource(R.string.fm_rail_session_logs), Icons.Filled.Description, it)) } File("/storage/emulated/0/Pictures").takeIf { it.isDirectory }?.let { add(locItem(stringResource(R.string.fm_rail_pictures), Icons.Filled.Image, it)) } } @@ -1033,12 +934,10 @@ fun FileManagerScreen( } Row(modifier = Modifier.weight(1f).fillMaxWidth()) { - // The slim picker keeps its layout: no rail. if (!pickMode) { CollapsibleRail(state = fmRailState, title = stringResource(R.string.fm_rail_title), sections = locationSections, outlinedItems = true) } Box(modifier = Modifier.weight(1f).fillMaxSize()) { - // ── Favorites list OR file list ── if (showFavorites) { FavoritesList( currentDir = currentDir, @@ -1060,7 +959,6 @@ fun FileManagerScreen( modifier = Modifier.fillMaxSize(), ) } else { - // ── File list (pull down to refresh) ── Box( modifier = Modifier .fillMaxSize() @@ -1088,8 +986,6 @@ fun FileManagerScreen( selected = file.absolutePath in selectedPaths, onLongPress = { if (!pickMode) { - // In selection mode a long-press toggles; otherwise it opens the - // same context menu the list rows show (the tile has no ⋮ button). if (selectionMode) { selectedPaths = if (file.absolutePath in selectedPaths) selectedPaths - file.absolutePath @@ -1168,8 +1064,6 @@ fun FileManagerScreen( selectionMode = selectionMode, selected = file.absolutePath in selectedPaths, onLongPress = { - // In selection mode a long-press toggles; otherwise it opens the - // per-item context menu (matching the grid tiles). if (!pickMode) { if (selectionMode) { selectedPaths = if (file.absolutePath in selectedPaths) @@ -1223,8 +1117,6 @@ fun FileManagerScreen( } } } - // material3 1.2.0's PullToRefreshContainer draws its indicator even at rest; - // only show it while the user is actively pulling or a refresh is running. if (pullState.verticalOffset > 0.5f || pullState.isRefreshing) { PullToRefreshContainer( state = pullState, @@ -1233,7 +1125,7 @@ fun FileManagerScreen( } } } - } // end content Box (beside the rail) - } // end rail + content Row + } + } } } diff --git a/app/src/main/java/com/droiddeck/launcher/files/FileOps.kt b/app/src/main/java/com/droiddeck/launcher/files/FileOps.kt index e363a3acc..04b83abaa 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FileOps.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FileOps.kt @@ -10,15 +10,9 @@ import java.io.IOException import java.nio.file.Files import java.util.Locale -/** - * The file operations the File Manager needs, as Bannerlator's FileUtils has them: a delete that - * says whether it worked, a copy and a move that report bytes as they go, and the size format - * the rows use. Kept apart from core/FileUtils, whose callers want a quiet delete. - */ object FileOps { private const val TAG = "FileOps" - /** Byte-accurate copy progress (copied/total bytes), reported as the copy proceeds. */ fun interface ProgressCallback { fun onProgress(copiedBytes: Long, totalBytes: Long) } fun formatBytes(context: Context, bytes: Long): String { @@ -30,7 +24,6 @@ object FileOps { fun isSymlink(file: File): Boolean = try { Files.isSymbolicLink(file.toPath()) } catch (e: Exception) { false } - /** Recursive delete that never follows a symlink; true when everything went. */ fun delete(target: File?): Boolean { if (target == null) return false if (target.isDirectory && !isSymlink(target)) { @@ -46,20 +39,12 @@ object FileOps { return ok } - /** Recursive total size in bytes of files under [f] (skips symlinks). Used to size a progress bar. */ fun totalSize(f: File?): Long { if (f == null || !f.exists() || isSymlink(f)) return 0 if (f.isDirectory) return f.listFiles()?.sumOf { totalSize(it) } ?: 0 return f.length() } - /** - * Moves [src] to [dst], relinking instead of copying whenever possible. A rename is instant - * regardless of size but only works within one filesystem; renameTo returning false is the - * signal that a boundary was crossed, so it doubles as the check, and the fallback is - * copy-then-delete. Only attempted when the destination does not exist: renaming onto an - * existing directory does not merge. - */ fun moveWithProgress(src: File, dst: File, progress: ProgressCallback?): Boolean { if (sameFile(src, dst)) return true if (!dst.exists() && src.renameTo(dst)) { @@ -70,7 +55,6 @@ object FileOps { return delete(src) } - /** A copy that reports byte progress, smooth even within one large file. */ fun copyWithProgress(src: File, dst: File, progress: ProgressCallback?): Boolean { val total = maxOf(totalSize(src), 1L) return copyWithProgress(src, dst, longArrayOf(0), total, progress) @@ -96,7 +80,7 @@ object FileOps { } val parent = dst.parentFile if (!src.exists() || (parent != null && !parent.exists() && !parent.mkdirs())) return false - val chunk = 4L * 1024 * 1024 // report progress every 4 MiB + val chunk = 4L * 1024 * 1024 try { FileInputStream(src).channel.use { input -> FileOutputStream(dst).channel.use { output -> @@ -123,7 +107,6 @@ object FileOps { private fun sameFile(a: File, b: File): Boolean = try { a.canonicalPath == b.canonicalPath } catch (e: IOException) { a.absolutePath == b.absolutePath } - /** True when [child] is [ancestor] itself or lives anywhere inside it. */ private fun isWithin(child: File, ancestor: File): Boolean { val c = try { child.canonicalPath } catch (e: IOException) { child.absolutePath } val a = try { ancestor.canonicalPath } catch (e: IOException) { ancestor.absolutePath } diff --git a/app/src/main/java/com/droiddeck/launcher/files/FilePickerActivity.kt b/app/src/main/java/com/droiddeck/launcher/files/FilePickerActivity.kt index 8f8782719..6fd89fa99 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FilePickerActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FilePickerActivity.kt @@ -16,16 +16,6 @@ import com.droiddeck.launcher.R import com.droiddeck.launcher.ui.DroidDeckTheme import java.io.File -/** - * Themed host for [FileManagerScreen] in pick mode. Launched through [InAppFilePicker] and answers with the - * picked path in [EXTRA_SELECTED_FILE] on RESULT_OK. - * - * Extras: - * - [EXTRA_EXTENSIONS] allowed lowercase extensions, no dot; absent or empty = all files - * - [EXTRA_PICK_DIRECTORY] true to pick a folder instead of a file - * - [EXTRA_INITIAL_DIRECTORY] where to open; absent = the last folder picked from, then internal storage - * - [EXTRA_PICKER_TITLE] the header - */ class FilePickerActivity : ComponentActivity() { override fun attachBaseContext(newBase: android.content.Context) { super.attachBaseContext(newBase) @@ -36,7 +26,6 @@ class FilePickerActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requestedOrientation = com.droiddeck.launcher.session.SessionPrefs.orientation(this) - // targetSdk 28: the old storage permission is exactly what lets us list the card. if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 1) } @@ -44,7 +33,6 @@ class FilePickerActivity : ComponentActivity() { val extensions = intent.getStringArrayExtra(EXTRA_EXTENSIONS)?.toList() ?: emptyList() val pickDir = intent.getBooleanExtra(EXTRA_PICK_DIRECTORY, false) val title = intent.getStringExtra(EXTRA_PICKER_TITLE) - // The screen remembers the folder it last picked from; an explicit start wins over it. val prefs = getSharedPreferences("file_manager", MODE_PRIVATE) val initialDir = (intent.getStringExtra(EXTRA_INITIAL_DIRECTORY) ?: prefs.getString("lastFilePickerDir", null)) ?.let { File(it) }?.takeIf { it.isDirectory } @@ -70,7 +58,6 @@ class FilePickerActivity : ComponentActivity() { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) - // The screen re-lists on its own when the grant lands; nothing to relay. if (requestCode == 1 && grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) recreate() } diff --git a/app/src/main/java/com/droiddeck/launcher/files/FilePropertiesDialog.kt b/app/src/main/java/com/droiddeck/launcher/files/FilePropertiesDialog.kt index fff674dc9..f4f8d80fb 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/FilePropertiesDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/FilePropertiesDialog.kt @@ -39,17 +39,7 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -// The File Manager's properties dialog. -/** - * Properties sheet for a single file or folder: basic info plus the two Windows/Wine file attributes - * the in-container file manager (wfm.exe) also exposes - Read-only and Hidden. Each toggle applies - * immediately (off the main thread) and refreshes the listing via [onChanged]. - * - * State is keyed on the file so it always reflects the entry it was opened for. On a filesystem that - * can't store Wine's DOSATTRIB xattr (the FUSE /storage volumes) only the Hidden toggle is disabled; - * Read-only keeps working everywhere. - */ @Composable internal fun FilePropertiesDialog( file: File, @@ -60,7 +50,6 @@ internal fun FilePropertiesDialog( val scope = rememberCoroutineScope() val dateFormat = remember { SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) } var attrs by remember(file.absolutePath) { mutableStateOf<FileAttrState?>(null) } - // Guards against a second toggle landing while the first is still being applied off-thread. var busy by remember(file.absolutePath) { mutableStateOf(false) } LaunchedEffect(file.absolutePath) { attrs = withContext(Dispatchers.IO) { readFileAttrs(file) } @@ -71,7 +60,6 @@ internal fun FilePropertiesDialog( title = { Text(stringResource(R.string.fm_properties)) }, text = { Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - // ── Basic info ── Text( text = file.name, color = MaterialTheme.colorScheme.onBackground, @@ -95,7 +83,6 @@ internal fun FilePropertiesDialog( Spacer(Modifier.height(6.dp)) val state = attrs - // ── Read-only ── checked when the owner can't write (Wine's FILE_ATTRIBUTE_READONLY). AttributeToggleRow( label = stringResource(R.string.fm_read_only), description = stringResource(R.string.fm_read_only_hint), @@ -116,7 +103,6 @@ internal fun FilePropertiesDialog( }, ) Spacer(Modifier.height(2.dp)) - // ── Hidden ── Wine's DOS hidden bit in the user.DOSATTRIB xattr. val hiddenSupported = state?.hiddenSupported == true AttributeToggleRow( label = stringResource(R.string.fm_hidden), @@ -134,7 +120,6 @@ internal fun FilePropertiesDialog( attrs = attrs?.copy(hidden = want) onChanged() } else { - // The write failed after all - disable the toggle rather than lie. attrs = attrs?.copy(hiddenSupported = false) Toast.makeText(context, R.string.fm_hidden_failed, Toast.LENGTH_SHORT).show() } @@ -147,7 +132,6 @@ internal fun FilePropertiesDialog( ) } -// One "label: value" line in the Properties info block. @Composable private fun PropertyLine(label: String, value: String) { Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) { @@ -168,7 +152,6 @@ private fun PropertyLine(label: String, value: String) { } } -// A labelled attribute switch with a one-line description, used by the Properties sheet. @Composable private fun AttributeToggleRow( label: String, diff --git a/app/src/main/java/com/droiddeck/launcher/files/InAppFilePicker.kt b/app/src/main/java/com/droiddeck/launcher/files/InAppFilePicker.kt index f53ffd26f..1ce9aca1f 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/InAppFilePicker.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/InAppFilePicker.kt @@ -5,14 +5,7 @@ import android.content.Intent import android.net.Uri import java.io.File -/** - * The app's own file picker, the way Bannerlator's File Manager offers its pick mode: one intent - * builder per kind of pick, and the picked path read back from the result. Every import on the - * main screen goes through here rather than Android's document picker, which cannot show an SD - * card as a card, remembers nothing, and hands back a content URI the runtime cannot bind. - */ object InAppFilePicker { - /** Pick one file. [extensions] are lowercase, without the dot; empty = any file. */ fun buildIntent( context: Context, extensions: List<String> = emptyList(), @@ -24,7 +17,6 @@ object InAppFilePicker { initialDir?.let { putExtra(FilePickerActivity.EXTRA_INITIAL_DIRECTORY, it) } } - /** Pick a folder: only folders are listed, and "Use this folder" returns the one open. */ fun buildDirIntent( context: Context, title: String? = null, @@ -39,6 +31,5 @@ object InAppFilePicker { fun pickedFile(data: Intent?): File? = pickedPath(data)?.let { File(it) } - /** The picked file as the `file:` URI the import code already accepts. */ fun pickedUri(data: Intent?): Uri? = pickedFile(data)?.let { Uri.fromFile(it) } } diff --git a/app/src/main/java/com/droiddeck/launcher/files/MenuStyle.kt b/app/src/main/java/com/droiddeck/launcher/files/MenuStyle.kt index 4435c3795..81f138b20 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/MenuStyle.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/MenuStyle.kt @@ -10,9 +10,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp -// Shared outlined-card look for the app's popup menus, matching the FileManager / CommunityCard -// idiom (surfaceContainer fill, 1dp outline, rounded 10dp). Applied to a DropdownMenu's modifier so -// the popup reads as an outlined card. Keep all three menu call-sites on this so they stay identical. @Composable internal fun Modifier.outlinedMenuCard(): Modifier { val cs = MaterialTheme.colorScheme @@ -23,7 +20,6 @@ internal fun Modifier.outlinedMenuCard(): Modifier { .border(1.dp, cs.outline, shape) } -// The thin low-alpha separator that sits between items inside an outlined menu card. @Composable internal fun MenuItemDivider() { HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)) diff --git a/app/src/main/java/com/droiddeck/launcher/files/OutlinedAlertDialog.kt b/app/src/main/java/com/droiddeck/launcher/files/OutlinedAlertDialog.kt index 3159d361c..3919e89b0 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/OutlinedAlertDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/OutlinedAlertDialog.kt @@ -12,12 +12,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties -// Single source of truth for the app's AlertDialogs: a drop-in wrapper around the Material3 -// slot-based AlertDialog that adds a 1dp outline around the whole popup box - matching the -// outline that Modifier.outlinedMenuCard() (MenuStyle.kt) puts on the app's menus. Signature -// mirrors androidx.compose.material3.AlertDialog exactly (compose-bom 2024.02.00 → material3 -// 1.2.0), so every call site can swap AlertDialog( → OutlinedAlertDialog( unchanged. The -// border shares the dialog's shape so its corner radius follows the rounded dialog. @Composable internal fun OutlinedAlertDialog( onDismissRequest: () -> Unit, diff --git a/app/src/main/java/com/droiddeck/launcher/files/PeIconExtractor.kt b/app/src/main/java/com/droiddeck/launcher/files/PeIconExtractor.kt index dc65b5e0c..621a06118 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/PeIconExtractor.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/PeIconExtractor.kt @@ -6,21 +6,11 @@ import java.io.File import java.io.RandomAccessFile import kotlin.math.max -/** - * Extracts the embedded application icon from a Windows PE executable (.exe). - * - * Walks the PE resource directory to the first RT_GROUP_ICON, picks the largest / - * deepest-colour entry it references, then decodes that RT_ICON image. Icon images - * are either PNG (Vista+) - decoded directly - or a classic DIB (BITMAPINFOHEADER + - * XOR pixels + 1-bpp AND mask) - decoded by hand here. - * - * Everything is best-effort: any malformed/oversized/unsupported input returns null. - */ object PeIconExtractor { private const val RT_ICON = 3 private const val RT_GROUP_ICON = 14 - private const val MAX_RES_SECTION = 64 * 1024 * 1024 // don't slurp absurd resource sections + private const val MAX_RES_SECTION = 64 * 1024 * 1024 private const val MAX_ICON_DIM = 1024 fun extract(file: File): Bitmap? = try { @@ -31,11 +21,11 @@ object PeIconExtractor { private fun parse(raf: RandomAccessFile): Bitmap? { val dos = read(raf, 0, 0x40) ?: return null - if (u8(dos, 0) != 0x4D || u8(dos, 1) != 0x5A) return null // "MZ" + if (u8(dos, 0) != 0x4D || u8(dos, 1) != 0x5A) return null val peOff = u32(dos, 0x3C).toInt() val coff = read(raf, peOff.toLong(), 24) ?: return null - if (u8(coff, 0) != 0x50 || u8(coff, 1) != 0x45 || u8(coff, 2) != 0 || u8(coff, 3) != 0) return null // "PE\0\0" + if (u8(coff, 0) != 0x50 || u8(coff, 1) != 0x45 || u8(coff, 2) != 0 || u8(coff, 3) != 0) return null val numSections = u16(coff, 6) val sizeOpt = u16(coff, 20) if (numSections <= 0 || numSections > 96 || sizeOpt <= 0) return null @@ -43,8 +33,8 @@ object PeIconExtractor { val optOff = peOff + 24L val opt = read(raf, optOff, sizeOpt) ?: return null val magic = u16(opt, 0) - val ddOff = if (magic == 0x20B) 112 else 96 // PE32+ vs PE32 data-directory offset - val resDirEntry = ddOff + 2 * 8 // data directory index 2 = resource table + val ddOff = if (magic == 0x20B) 112 else 96 + val resDirEntry = ddOff + 2 * 8 if (resDirEntry + 8 > opt.size) return null val resRVA = u32(opt, resDirEntry).toInt() val resSize = u32(opt, resDirEntry + 4).toInt() @@ -75,7 +65,6 @@ object PeIconExtractor { val resDirStart = resRVA - secVA if (resDirStart < 0 || resDirStart + 16 > res.size) return null - // RT_GROUP_ICON -> first group's data entry -> GRPICONDIR val grpTypeDir = findIdSubdir(res, resDirStart, RT_GROUP_ICON, resDirStart) ?: return null val grpData = firstDataEntry(res, grpTypeDir, resDirStart) ?: return null val grp = dataEntryBytes(res, grpData, secVA) ?: return null @@ -97,7 +86,6 @@ object PeIconExtractor { } if (bestId < 0) return null - // RT_ICON with the chosen id -> image bytes val iconTypeDir = findIdSubdir(res, resDirStart, RT_ICON, resDirStart) ?: return null val iconIdDir = findIdSubdir(res, iconTypeDir, bestId, resDirStart) ?: return null val iconData = firstDataEntry(res, iconIdDir, resDirStart) ?: return null @@ -106,9 +94,7 @@ object PeIconExtractor { return decodeIcon(img) } - // ── resource-directory navigation ── - /** Resolve the sub-directory under [dirIdx] whose id matches [wantId]; null if absent or not a directory. */ private fun findIdSubdir(res: ByteArray, dirIdx: Int, wantId: Int, resDirStart: Int): Int? { if (dirIdx + 16 > res.size) return null val total = u16(res, dirIdx + 12) + u16(res, dirIdx + 14) @@ -126,7 +112,6 @@ object PeIconExtractor { return null } - /** Descend the first entry of each level until an IMAGE_RESOURCE_DATA_ENTRY; returns its index. */ private fun firstDataEntry(res: ByteArray, dirIdx: Int, resDirStart: Int): Int? { if (dirIdx + 16 > res.size) return null if (u16(res, dirIdx + 12) + u16(res, dirIdx + 14) == 0) return null @@ -140,7 +125,6 @@ object PeIconExtractor { } } - /** Read the bytes an IMAGE_RESOURCE_DATA_ENTRY points at (its OffsetToData is an RVA). */ private fun dataEntryBytes(res: ByteArray, entryIdx: Int, secVA: Int): ByteArray? { if (entryIdx < 0 || entryIdx + 8 > res.size) return null val rva = u32(res, entryIdx).toInt() @@ -150,7 +134,6 @@ object PeIconExtractor { return res.copyOfRange(start, start + size) } - // ── icon image decoding ── private fun decodeIcon(b: ByteArray): Bitmap? { if (b.size >= 8 && u8(b, 0) == 0x89 && u8(b, 1) == 0x50 && u8(b, 2) == 0x4E && u8(b, 3) == 0x47) { @@ -164,7 +147,7 @@ object PeIconExtractor { val headerSize = u32(b, 0).toInt() val width = s32(b, 4) val heightFull = s32(b, 8) - val height = heightFull / 2 // DIB icon height counts XOR + AND mask + val height = heightFull / 2 val bitCount = u16(b, 14) if (width <= 0 || height <= 0 || width > MAX_ICON_DIM || height > MAX_ICON_DIM) return null @@ -183,7 +166,6 @@ object PeIconExtractor { val andRowSize = ((width + 31) / 32) * 4 val andStart = xorStart + rowSize * height - // 32-bit icons sometimes ship an all-zero alpha channel; fall back to the AND mask then. var hasAlpha = false if (bitCount == 32) { var y = 0 @@ -245,7 +227,6 @@ object PeIconExtractor { return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888) } - /** AND-mask alpha for a pixel: returns 0xFF000000 (opaque) or 0 (transparent), shifted into place. */ private fun maskAlpha(b: ByteArray, andStart: Int, andRowSize: Int, srcRow: Int, x: Int): Int { val o = andStart + srcRow * andRowSize + x / 8 if (o >= b.size) return 0xFF shl 24 @@ -253,7 +234,6 @@ object PeIconExtractor { return if (bit == 1) 0 else (0xFF shl 24) } - // ── little-endian readers ── private fun u8(b: ByteArray, off: Int): Int = b[off].toInt() and 0xFF private fun u16(b: ByteArray, off: Int): Int = u8(b, off) or (u8(b, off + 1) shl 8) diff --git a/app/src/main/java/com/droiddeck/launcher/files/StorageRoots.kt b/app/src/main/java/com/droiddeck/launcher/files/StorageRoots.kt index a53e792ce..99e940157 100644 --- a/app/src/main/java/com/droiddeck/launcher/files/StorageRoots.kt +++ b/app/src/main/java/com/droiddeck/launcher/files/StorageRoots.kt @@ -8,52 +8,24 @@ import android.os.storage.StorageVolume import com.droiddeck.launcher.R import java.io.File -/** - * A storage volume offered in the file manager's drive menu. - * - * [dir] is the best path we found for the volume: its root when that is listable, otherwise the - * deepest directory we can actually read on the way down to the app-specific directory. A volume - * always gets exactly one entry, even if several paths lead to it. - */ data class StorageRoot( val label: String, val dir: File, val removable: Boolean, - /** False when no path on the volume could be listed; the entry is still shown. */ val readable: Boolean, ) -/** - * Enumerates mounted storage volumes. - * - * Listing `/storage` is not enough on its own: a volume can be absent from this process's mount - * view (notably after a container exits and the app process is restarted onto a stale storage - * sandbox) even though it is mounted and healthy. The framework knows about it regardless, so the - * volume set is built from several independent sources: - * - * 1. [StorageManager.getStorageVolumes] - the authoritative list, read over Binder. - * 2. [Context.getExternalFilesDirs] - per-app directories, granted separately from shared storage. - * 3. `/storage` and `/mnt/media_rw` directory listings - the filesystem view, as a backstop. - * - * Those sources overlap heavily: one SD card is reachable as `/storage/<uuid>`, as - * `/mnt/media_rw/<uuid>`, and via its app-specific directory, and typically only some of them are - * readable. So paths are grouped by the *volume* they belong to rather than by path, and each - * volume contributes a single entry pointing at the best path available for it. - */ object StorageRoots { private const val EMULATED_PREFIX = "/storage/emulated" private const val INTERNAL_PATH = "/storage/emulated/0" - /** Identifies the physical volume a path belongs to; paths sharing a key are the same card. */ private const val PRIMARY_KEY = "primary" private class Volume(val key: String) { var label: String? = null var removable: Boolean = key != PRIMARY_KEY - /** Candidate directories, best-guess first. */ val candidates = LinkedHashSet<File>() - /** App-specific directories, used to find a readable path inside an unreadable root. */ val seeds = LinkedHashSet<File>() } @@ -62,7 +34,6 @@ object StorageRoots { fun volumeOf(key: String) = volumes.getOrPut(key) { Volume(key) } - // Internal storage is never removable and never absent; seed it first so it heads the menu. volumeOf(PRIMARY_KEY).apply { label = context.getString(R.string.fm_drive_internal) removable = false @@ -72,7 +43,6 @@ object StorageRoots { val appDirs = appSpecificDirs(context) val storageManager = context.getSystemService(StorageManager::class.java) - // 1 + 2 - framework-reported volumes. Authoritative, and unaffected by our mount view. storageManager?.storageVolumes.orEmpty() .filter { isMounted(it.state) } .forEach { volume -> @@ -86,14 +56,11 @@ object StorageRoots { } if (uuid != null) { entry.candidates += File("/storage/$uuid") - // Last resort only: the raw mount is normally unreadable to an app, so it must - // never outrank /storage/<uuid> and must never become an entry of its own. entry.candidates += File("/mnt/media_rw/$uuid") } appDirs.filter { belongsTo(storageManager!!, it, volume) }.forEach { entry.seeds += it } } - // 3 - filesystem backstop, for any volume the framework did not report. File("/storage").listFiles().orEmpty().forEach { child -> if (!child.isDirectory || child.name == "self" || child.name == "emulated") return@forEach volumeOf(child.name).candidates += child @@ -102,7 +69,6 @@ object StorageRoots { if (child.isDirectory) volumeOf(child.name).candidates += child } - // App-specific directories, which stay reachable when shared storage is not. appDirs.forEach { dir -> val root = volumeRootOf(dir) ?: return@forEach val entry = volumeOf(if (isEmulated(root)) PRIMARY_KEY else root.name) @@ -117,11 +83,9 @@ object StorageRoots { } private fun Volume.toRoot(context: Context): StorageRoot { - // Prefer a path we can actually list; a readable child beats an unreadable root. val dir = candidates.firstOrNull(::canBrowse) ?: candidates.firstNotNullOfOrNull { root -> seeds.firstNotNullOfOrNull { deepestReadable(root, it) } } ?: seeds.firstOrNull(::canBrowse) - // Report the volume anyway; an entry that lists empty beats one that silently vanished. ?: candidates.firstOrNull() ?: File("/storage/$key") @@ -135,7 +99,6 @@ object StorageRoots { private fun Volume.defaultLabel(context: Context): String = if (removable) context.getString(R.string.fm_drive_sd) else key - /** Appends the volume id to any label used by more than one volume, so entries stay tellable apart. */ private fun disambiguate(roots: List<StorageRoot>): List<StorageRoot> { val counts = roots.groupingBy { it.label }.eachCount() return roots.map { root -> @@ -143,7 +106,6 @@ object StorageRoots { } } - /** Walks up from [seed] towards [root] and returns the highest directory we can list. */ private fun deepestReadable(root: File, seed: File): File? { var current: File? = seed.absoluteFile var best: File? = null @@ -154,7 +116,6 @@ object StorageRoots { return best } - /** `/storage/ABCD-1234/Android/data/<pkg>/files` -> `/storage/ABCD-1234`. */ private fun volumeRootOf(appDir: File): File? = generateSequence(appDir.absoluteFile) { it.parentFile } .firstOrNull { it.name.equals("Android", ignoreCase = true) } @@ -178,11 +139,6 @@ object StorageRoots { } } - /** - * Names a volume the way the rest of the file manager does. The framework description is only - * used when it says something more useful than "SD card" - on some devices it is a bare disk - * name like "android", which tells the user nothing about which card they are looking at. - */ private fun labelFor(context: Context, volume: StorageVolume, uuid: String?): String { if (volume.isPrimary) return context.getString(R.string.fm_drive_internal) if (volume.isRemovable) return context.getString(R.string.fm_drive_sd) diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AddedExes.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AddedExes.kt index 62afb0b8a..a9f18ac06 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedExes.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedExes.kt @@ -5,15 +5,7 @@ import org.json.JSONArray import org.json.JSONObject import java.io.File -/** - * Games added from the Games tab's + rather than a Games folder: one picked .exe, or each game - * "Add all games in this folder" found. Kept by the game's folder in `files/added-exes.json`, - * which [AddedGames.scan] merges in. The exe chosen later, the shortcut appid, the edits and the - * removal live where every added game keeps them ([com.droiddeck.launcher.session.SessionPrefs]); - * this holds which folders are games and the exe they came with. - */ object AddedExes { - /** [picked]: the user chose [exe]; false = [GameExePicker]'s best guess, which a scan ranks again. */ class Entry(val folder: String, val exe: String, val picked: Boolean = true) private fun file(context: Context) = File(context.filesDir, "added-exes.json") @@ -30,7 +22,6 @@ object AddedExes { emptyList() } - /** Adds [entry], replacing one for the same folder. */ @Synchronized fun put(context: Context, entry: Entry) = write(context, list(context).filter { it.folder != entry.folder } + entry) diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameArt.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameArt.kt index 1fb5bb272..0dd76260b 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameArt.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameArt.kt @@ -11,14 +11,6 @@ import java.net.HttpURLConnection import java.net.URL import java.net.URLEncoder -/** - * Artwork for an added game: what the user put in the game's folder first, else what Steam has - * for it - by the Steam appid its files name, else by a store search on its title - else what - * SteamGridDB has ([SteamGridDb]), fetched once into the app's cache. Four pieces, named the way - * the client names them in its grid folder: the portrait capsule, the wide header, the hero - * background and the logo; plus an icon pulled out of the game's own .exe. The session hands - * the paths to the runtime's shortcuts writer, which copies them into the client's grid. - */ object AddedGameArt { private const val TAG = "AddedGameArt" private const val STORE_SEARCH = "https://store.steampowered.com/api/storesearch/?l=english&cc=US&term=" @@ -29,15 +21,10 @@ object AddedGameArt { val any get() = portrait != null || header != null || hero != null || logo != null } - /** - * The pieces a user can choose, as the client takes them: [assetType] is its custom-artwork - * number (null: the icon, set by path); [cacheName] the automatic file in the game's cache. - */ enum class Slot(val key: String, val cacheName: String, val assetType: Int?) { COVER("p", "p.jpg", 0), BACKGROUND("hero", "hero.jpg", 1), LOGO("logo", "logo.png", 2), ICON("icon", "icon.png", null), } - /** Where a piece came from; [AUTO] = nothing found yet. */ const val FOLDER = "folder" const val STEAM = "steam" const val SGDB = "sgdb" @@ -45,10 +32,8 @@ object AddedGameArt { const val EXE = "exe" const val AUTO = "" - /** A slot as it stands: the file shown and where it came from (one of [FOLDER].. [AUTO]); [chosen] = the user picked it. */ class SlotState(val slot: Slot, val file: File?, val source: String, val chosen: Boolean) - /** One image the chooser offers: its group ([FOLDER], [STEAM], [SGDB]), a thumbnail and the full image (URL or path). */ class Option(val group: String, val thumb: String, val full: String) private val CHOSEN = Regex("""chosen-(\w+)\.(\w+)""") @@ -64,7 +49,6 @@ object AddedGameArt { internal fun cacheDir(context: Context, game: AddedGames.Game) = File(context.filesDir, "added-art/${game.appId}") - /** An image in the folder (or its `art` subfolder) under one of the given names, any image extension. */ private fun find(folder: File, names: List<String>): File? { val dirs = listOf(folder, File(folder, "art"), File(folder, ".art")) for (dir in dirs) for (name in names) for (ext in IMAGE) { @@ -74,7 +58,6 @@ object AddedGameArt { return null } - /** What the folder itself offers; the file named after the folder counts as the portrait. */ private fun local(game: AddedGames.Game): Art { val folder = game.folder return Art( @@ -86,7 +69,6 @@ object AddedGameArt { ) } - /** The user's choice for each piece, else the folder's art, else the cached art; no network. */ fun resolve(context: Context, game: AddedGames.Game): Art { val own = local(game) val cache = cacheDir(context, game) @@ -100,7 +82,6 @@ object AddedGameArt { ) } - /** Each choosable piece with its file and where it came from, for the game's editor; no network. */ fun describe(context: Context, game: AddedGames.Game): List<SlotState> { val own = local(game) val cache = cacheDir(context, game) @@ -122,17 +103,11 @@ object AddedGameArt { private val CHOOSABLE = setOf("png", "jpg", "jpeg", "webp") - /** Images in the game's folder (and its art folders) that could stand for a piece. */ internal fun folderImages(folder: File): List<File> = listOf(folder, File(folder, "art"), File(folder, ".art")).flatMap { dir -> dir.listFiles { f -> f.isFile && f.extension.lowercase() in CHOOSABLE }?.sortedBy { it.name.lowercase() }.orEmpty() }.take(12) - /** - * What the chooser offers for [slot]: the game folder's images, Steam's official piece (when the - * game has a Steam appid and Steam has the piece), and SteamGridDB's best few (with a key). - * Blocking: it asks the network. - */ fun options(context: Context, game: AddedGames.Game, slot: Slot): List<Option> { val out = ArrayList<Option>() folderImages(game.folder).forEach { out.add(Option(FOLDER, it.path, it.path)) } @@ -155,7 +130,6 @@ object AddedGameArt { return out } - /** Sets [slot] to [option] (a file is copied, a URL fetched) and records where it came from. False when it could not be had. Blocking. */ fun choose(context: Context, game: AddedGames.Game, slot: Slot, option: Option): Boolean { val bytes = if (option.full.startsWith("https://")) get(option.full) else runCatching { File(option.full).readBytes() }.getOrNull() bytes ?: return false @@ -170,13 +144,11 @@ object AddedGameArt { return true } - /** Where the user's choice for [slot] came from and which image it was ("" both when automatic). */ fun chosenSource(context: Context, game: AddedGames.Game, slot: Slot): Pair<String, String> { val value = SessionPrefs.addedGameArtSource(context, game.folder.path, slot.key) return value.substringBefore('|') to value.substringAfter('|', "") } - /** [slot] back to what is found by itself. */ fun reset(context: Context, game: AddedGames.Game, slot: Slot) { chosen(cacheDir(context, game), slot)?.delete() SessionPrefs.setAddedGameArtSource(context, game.folder.path, slot.key, "") @@ -191,7 +163,6 @@ object AddedGameArt { try { c.responseCode == 200 } finally { c.disconnect() } }.getOrDefault(false) - /** The first icon inside the game's .exe, kept as a PNG beside the cached art. */ private fun exeIcon(context: Context, game: AddedGames.Game): File? { val cache = cacheDir(context, game) val png = File(cache, "icon.png") @@ -206,12 +177,6 @@ object AddedGameArt { return if (tmp.renameTo(png)) png else null } - /** - * Fetches the art that is missing: Steam first (the appid the game's files name, else a store - * search on its title whose result carries the same name), then SteamGridDB when a key is - * set. Each lookup runs once per game; a miss is retried after a week. Blocking: run it off - * the main thread. Returns true when anything new arrived. - */ fun fetchMissing(context: Context, games: List<AddedGames.Game>): Boolean { if (!SessionPrefs.addedGamesArt(context)) return false val sgdbKey = SteamGridDb.key(context) @@ -236,17 +201,12 @@ object AddedGameArt { return changed } - /** - * The game's Steam app, the one record its Windows components use too ([SteamMatch]): what its - * files name, else a store search on its title ([search]) whose result has the same name. - */ internal fun steamMatch(context: Context, game: AddedGames.Game, search: (String) -> Int?): SteamMatch.Match { val match = SteamMatch.resolve(context, game.folder.path, game.exe, game.name, legacy = File(cacheDir(context, game), "steam-appid"), search = search) match.appId?.let { Log.i(TAG, "${game.name}: Steam app $it (${match.certainty.name.lowercase()})") } return match } - /** What [marker] remembers (an id, or "none" for a week), else [find]'s answer written to it. */ private fun remembered(marker: File, find: () -> String?): String? { if (marker.isFile) { val value = marker.readText().trim() @@ -262,11 +222,6 @@ object AddedGameArt { private val EDITION = Regex("(?i)\\b(complete|definitive|legendary|goty|game of the year|deluxe|ultimate|remastered)( edition)?\\b") - /** - * How well a result's name [got] matches the game's [name]: 3 the same, 2 the same once - * edition words are dropped, 1 one starts with the other; 0 no match. Nothing is taken for - * being the first result. - */ internal fun nameMatch(name: String, got: String): Int { val want = normalize(name) val wantShort = normalize(name.replace(EDITION, " ").trim().ifEmpty { name }) @@ -280,13 +235,11 @@ object AddedGameArt { } } - /** The id of the store result whose name matches [name] best, or null when none does. */ internal fun pickStoreResult(name: String, items: List<Pair<String, String>>): String? = items.map { (id, got) -> id to nameMatch(name, got) } .filter { it.second > 0 && it.first.isNotEmpty() } .maxByOrNull { it.second }?.first - /** The Steam appid whose store name matches [name], or null for no match. */ private fun search(name: String): String? { val term = name.replace(EDITION, " ").trim().ifEmpty { name } val json = get(STORE_SEARCH + URLEncoder.encode(term, "UTF-8"))?.toString(Charsets.UTF_8) ?: return null @@ -309,10 +262,6 @@ object AddedGameArt { return got } - /** - * Newer Steam games keep their art under hashed paths (store_item_assets/steam/apps/<id>/<hash>/…), - * where the fixed CDN names above 404. The store's item API names those paths; once per appid. - */ private fun steamAssetUrl(id: String, remote: String): String? { val assets = steamAssets.getOrPut(id) { runCatching { @@ -336,11 +285,6 @@ object AddedGameArt { private val steamAssets = java.util.concurrent.ConcurrentHashMap<String, Map<String, String>>() - /** - * When nothing else gave a cover (or header), one made from the exe's icon: the icon centred on - * its own colour, darkened toward the bottom, so the game still has a face in the list and in - * Steam. Made once per game; real art found later takes over because it is looked at first. - */ private fun exeArt(context: Context, game: AddedGames.Game, cover: Boolean): File? { val cache = cacheDir(context, game) val out = File(cache, if (cover) "p-exe.png" else "header-exe.png") diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameEdits.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameEdits.kt index df3a22f9b..5690d00fe 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameEdits.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameEdits.kt @@ -5,27 +5,16 @@ import com.droiddeck.launcher.session.SessionPrefs import com.droiddeck.launcher.stores.SteamLiveShortcuts import java.io.File -/** - * The game editor's writes for an added game, Steam's non-Steam Properties in the app: name, - * target, Start in, launch options and art. Each goes through the same shortcut - the listing the - * shortcuts writer reads, and the running client - on the appid the game already has, so an edit - * never makes a second shortcut. Each call is quick (the game alone, [AddedGames.single]); the - * client and the listing follow on [AddedGamesPublisher], in order. Call off the main thread. - */ object AddedGameEdits { - /** The added game in [folder] as it stands now, or null when it is not listed. */ fun game(context: Context, folder: String): AddedGames.Game? = AddedGames.single(context, folder) - /** The client told ([live]), then the listing rewritten, on [AddedGamesPublisher]. */ private fun publish(context: Context, live: () -> Unit) = AddedGamesPublisher.later(context, live) - /** [name] as the game's name; "" goes back to the name found by itself. */ fun setName(context: Context, folder: String, name: String): AddedGames.Game? { SessionPrefs.setAddedGameName(context, folder, name) return apply(context, folder) } - /** [exe] as the game's target; false when the session cannot see it. */ fun setExe(context: Context, folder: String, exe: File): Boolean { if (!exe.isFile || AddedGames.guestPath(context, exe) == null) return false SessionPrefs.setAddedGameExe(context, folder, exe.path) @@ -33,7 +22,6 @@ object AddedGameEdits { return true } - /** [dir] (a host folder) as the game's Start in; "" = the exe's own folder. False when the session cannot see it. */ fun setStartIn(context: Context, folder: String, dir: String): Boolean { val guest = if (dir.isBlank()) "" else AddedGames.guestPath(context, File(dir.trim())) ?: return false SessionPrefs.setAddedGameStartIn(context, folder, guest) @@ -46,7 +34,6 @@ object AddedGameEdits { return apply(context, folder) } - /** [slot] set to [option], then handed to the shortcut. False when the image could not be had. */ fun setArt(context: Context, folder: String, slot: AddedGameArt.Slot, option: AddedGameArt.Option): Boolean { val game = game(context, folder) ?: return false if (!AddedGameArt.choose(context, game, slot, option)) return false @@ -54,7 +41,6 @@ object AddedGameEdits { return true } - /** [slot] back to automatic, then handed to the shortcut. */ fun resetArt(context: Context, folder: String, slot: AddedGameArt.Slot) { val game = game(context, folder) ?: return AddedGameArt.reset(context, game, slot) diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt index a4ce68acb..a4a2e80ab 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt @@ -11,7 +11,6 @@ import com.droiddeck.launcher.stores.StoreInstallRoot import java.io.File import java.util.zip.CRC32 -/** Windows game folders shared with the Steam session. */ object AddedGames { private const val TAG = "AddedGames" private const val LIBRARY = "/mnt/droiddeck-sd" @@ -19,38 +18,23 @@ object AddedGames { class Game( val folder: File, val name: String, - /** The game's own .exe: what the icon is read from and what the settings page lists. */ val exe: File, - /** The guest path the shortcut runs: [exe], or a store install's launcher .bat in front of it. */ val guestExe: String, val guestDir: String, - /** The client's 32-bit appid for this shortcut, as an unsigned value. */ val appId: Long, - /** What steam://rungameid/ takes for a shortcut. */ val gameId: Long, val candidates: List<File>, val steamAppId: Int? = null, - /** Where the game came from: a store's id, or [Library.ADDED] for a folder the user added. */ val source: String = Library.ADDED, - /** The store's own id for the game, from its sidecar; null for a plain added folder. */ val storeId: String? = null, - /** The exe is the picker's guess and nothing clearly won ([GameExePicker.Ranking.uncertain]). */ val exeUncertain: Boolean = false, - /** Steam's launch options for the shortcut; "" for none. */ val launchOptions: String = "", - /** Added from the Games tab (an entry of its own), not found in a Games folder. */ val single: Boolean = false, ) { fun folderName(): String = folder.name } - /** A Games folder an earlier build had, and where the session sees it. */ class Root(val host: File, val guest: String) - /** - * The Games folders earlier builds had, bound into the session at the guest paths they had - * then (/root/Games/<name>), so the games in them (entries now, [GamesFolderMigration]) keep - * their paths and shortcuts. Nothing adds one any more. - */ fun roots(context: Context): List<Root> { GamesFolderMigration.run(context) return SessionPrefs.gameFolderBinds(context).map { (host, guest) -> Root(File(host), guest) } @@ -61,14 +45,10 @@ object AddedGames { "|.*installer.*|.*uninstall.*|.*updater?.*|.*config(ur.*)?|.*settings.*|.*editor.*|.*server.*|.*benchmark.*|.*helper.*|.*eac.*|.*easyanticheat.*|.*battleye.*)\\.exe$", ) - /** Under here the chosen Games folders are bound inside the session, one each. */ const val GUEST_DIR = "/root/Games" - /** Where a host path appears inside the session, or null when the session cannot see it. */ fun guestPath(context: Context, host: File): String? { val path = host.absolutePath - // The Games folders are bound on their own, so a folder anywhere - an SD card, a USB - // drive - works without being inside one of the other binds. for (root in roots(context)) { val dir = root.host.absolutePath if (path == dir) return root.guest @@ -77,26 +57,19 @@ object AddedGames { SessionPrefs.romsDir(context).takeIf { it.isNotEmpty() }?.let { roms -> if (path.startsWith("$roms/")) return "/root/ROMs/" + path.removePrefix("$roms/") } - // A store root on a card has a bind of its own, before the library rule: the same folder - // keeps the same guest path (and appid) whether or not that card is also the Steam library. StoreInstallRoot.externalGuestPath(context, host)?.let { return it } GameStorage.effective(context)?.let { lib -> if (path.startsWith("${lib.path}/")) return "$LIBRARY/" + path.removePrefix("${lib.path}/") } - // Inside the runtime's own tree (store installs on internal storage live at - // root/Games/Stores): the tree is the session's / and needs no bind. val runtime = LinuxRuntime.rootDir(context).absolutePath if (path.startsWith("$runtime/")) return "/" + path.removePrefix("$runtime/") - // Internal storage, bound at /root/Storage in every session: an exe picked anywhere on it. val storage = android.os.Environment.getExternalStorageDirectory().absolutePath if (path.startsWith("$storage/")) return "/root/Storage/" + path.removePrefix("$storage/") return null } - /** The .exe files a game folder offers, best first ([GameExePicker]); declared Store exes lead. */ fun candidates(folder: File): List<File> = rank(folder).exes - /** [GameExePicker]'s ranking, with a Store-declared executable taking precedence when present. */ fun rank(folder: File): GameExePicker.Ranking { val key = "${folder.path}|${folder.lastModified()}" val now = System.currentTimeMillis() @@ -115,20 +88,11 @@ object AddedGames { return result } - /** A folder's ranking for a minute, so one add (rank, then the game built from it) walks it once. */ private val ranked = java.util.concurrent.ConcurrentHashMap<String, Pair<Long, GameExePicker.Ranking>>() private const val RANK_FRESH_MS = 60_000L - /** - * Names identified from an exe (Steam manifest, GOG info, version resource), by the exe's path, - * size and time and its game folder's time (a GOG info file dropped in changes the latter). - */ private val identified = java.util.concurrent.ConcurrentHashMap<String, String>() - /** - * The game's own title for [exe] when the disk names one ([GameIdentifier]: a Steam manifest, - * a GOG info file, the exe's version resource), else null (the folder name stands). - */ internal fun identifiedName(folder: File, exe: File): String? { val key = "${exe.path}|${exe.length()}|${exe.lastModified()}|${folder.lastModified()}" identified[key]?.let { return it.ifEmpty { null } } @@ -141,13 +105,6 @@ object AddedGames { return name } - /** - * The exe's version resource often spells the title the way a programmer typed it ("Insane2") - * while the folder has the real spacing ("Insane 2"). When both spell the same letters and - * digits, the one with more word breaks wins; a tie keeps mixed case over all one case - * ("DiRT 3" over "dirt 3"), else the exe's spelling. Different titles keep the exe's. - */ - /** "Watch_Dogs" → "Watch Dogs": an underscore in a version-resource title is a programmer's space. */ internal fun underscoresToSpaces(name: String): String = name.replace('_', ' ').replace(Regex("\\s+"), " ").trim().ifEmpty { name } @@ -163,7 +120,6 @@ object AddedGames { else -> peName } } - /** The executables MicrosoftGame.config or appxmanifest.xml declares that exist inside [folder]. */ internal fun declaredExes(folder: File): List<File> { val names = DECLARED.flatMap { (file, pattern) -> runCatching { pattern.findAll(File(folder, file).readText()).map { it.groupValues[1] }.toList() }.getOrDefault(emptyList()) @@ -195,30 +151,20 @@ object AddedGames { val steamInstalls = steamInstallDirs(dir) for (folder in dir.listFiles { f -> f.isDirectory }?.sortedBy { it.name.lowercase() } ?: emptyList()) { if (folder.name.lowercase() in steamInstalls || folder.name.equals("steamapps", ignoreCase = true)) continue - // Removed from the list by the user (the folder stays on disk); restored from the settings page. if (folder.path in removed) continue - // The store install root sits inside the Games storage; its games are scanned below, per store. if (folder.name == "Games" && StoreInstallRoot.roots(context).any { it.absolutePath == folder.absolutePath }) continue scanGame(context, folder, out) } } - // Games added as one picked exe (the Games tab's +), by that exe's folder. for (entry in AddedExes.list(context)) { val folder = File(entry.folder) if (folder.path in removed || !folder.isDirectory) continue scanGame(context, folder, out, single = entry) } - // Games the Stores section installed: one folder each under <root>/<Store>/, with a sidecar. - // An unfinished one (sidecar still `installing`, or none at all) is neither a game nor Custom. for (folder in StoreInstallRoot.gameFolders(context)) if (StoreInstallRoot.isFinished(folder)) scanGame(context, folder, out) return out.distinctBy { it.folder.canonicalPath } } - /** - * The game in [folderPath] alone, built by the same rules as [scan] (store sidecar, an entry - * from the Games tab, the chosen exe, the edits) without walking every other game: what the - * editor needs for one game. Null when that folder is not a game, or was removed. - */ fun single(context: Context, folderPath: String): Game? { val folder = File(folderPath) if (!folder.isDirectory || folderPath in SessionPrefs.removedAddedGames(context)) return null @@ -227,12 +173,6 @@ object AddedGames { return out.firstOrNull() } - /** - * Stops listing the added game in [folder] at once: marked removed and dropped from the - * library cache; the running client loses its shortcut ([appId], else the one stored for the - * folder) and the listing is rewritten afterwards ([AddedGamesPublisher]). Nothing on disk is - * deleted. Quick; call off the main thread (preferences and a small file). - */ fun remove(context: Context, folder: File, appId: Long? = null, name: String = folder.name) { val app = context.applicationContext val started = android.os.SystemClock.elapsedRealtime() @@ -243,13 +183,10 @@ object AddedGames { AddedGamesPublisher.later(app) { if (id != null) com.droiddeck.launcher.stores.SteamLiveShortcuts.remove(app, id) } } - /** What [addExe] did with a picked exe. */ sealed class AddResult { - /** A new game, or the existing one the exe already is, or the listed game whose exe it now is. */ class Added(val game: Game) : AddResult() class Existing(val game: Game) : AddResult() class Switched(val game: Game) : AddResult() - /** The session cannot see the exe's path. */ object Unreachable : AddResult() object NotFound : AddResult() } @@ -258,11 +195,6 @@ object AddedGames { private fun inside(path: String, folder: String) = path.startsWith(folder.trimEnd('/') + "/") - /** - * The game folder [exe] belongs to, without a scan: an entry from the Games tab (Games folders - * of earlier builds included), a removed game, a store install, or the first-level folder under - * the Steam library that holds it. Null when it is in none. - */ private fun ownerFolder(context: Context, exe: File): File? { GamesFolderMigration.run(context) val path = canonical(exe) @@ -270,7 +202,6 @@ object AddedGames { 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 } - // Games folders of earlier builds are entries now, found above. val dirs = listOfNotNull( GameStorage.effective(context)?.let { File(it.path, "steamapps/common") }, GameStorage.effective(context)?.let { File(it.path) }, @@ -284,12 +215,6 @@ object AddedGames { return null } - /** - * Adds the game [exe] is: the game itself when it is listed already, that game's exe switched - * when [exe] sits in a listed (or removed) game's folder, else a new game for [exe]'s folder. - * Only that one game is read; Steam (the running client, then the listing) follows on - * [AddedGamesPublisher]. Nothing is added when the session cannot see the path. - */ fun addExe(context: Context, exe: File): AddResult { val app = context.applicationContext if (!exe.isFile) return AddResult.NotFound @@ -309,15 +234,8 @@ object AddedGames { return if (owner == null) AddResult.Added(game) else AddResult.Switched(game) } - /** What [addFolder] did: the games it added, and how many of the folder's games were listed already. */ class FolderResult(val added: List<Game>, val already: Int) - /** - * "Add all games in this folder": every subfolder of [dir] with an .exe becomes a game with - * [GameExePicker]'s best one (marked uncertain where nothing clearly won), as an entry of its - * own, not a Games folder. A subfolder that is, holds or sits in a listed game counts as - * already there; a removed one comes back. Steam follows once, on [AddedGamesPublisher]. Blocking. - */ fun addFolder(context: Context, dir: File, onGame: (Game) -> Unit = {}, onChecking: (String?) -> Unit = {}): FolderResult { val app = context.applicationContext val removed = SessionPrefs.removedAddedGames(app).toSet() @@ -343,11 +261,6 @@ object AddedGames { return FolderResult(added, already) } - /** - * The game folders already listed, without building any game: the first-level folders of the - * Steam library (as [scan] walks them), the Games tab's entries (Games folders of earlier builds - * included) and the store installs. Canonical paths; removed games are left out. - */ private fun listedFolders(context: Context, removed: Set<String>): List<String> { val out = ArrayList<String>() GamesFolderMigration.run(context) @@ -367,11 +280,6 @@ object AddedGames { return out } - /** - * The folders under a library's steamapps/common that a manifest beside it already claims. - * Steam lists those itself, so a shortcut would only add a non-Steam copy of the game. Steam - * may lowercase installdir on Android's case-insensitive storage, so names compare lowercased. - */ internal fun steamInstallDirs(dir: File): Set<String> { val steamapps = dir.parentFile?.takeIf { dir.name == "common" && it.name == "steamapps" } ?: return emptySet() return steamapps.listFiles { f -> f.isFile && f.name.startsWith("appmanifest_") && f.name.endsWith(".acf") } @@ -385,33 +293,24 @@ object AddedGames { private val INSTALL_DIR = Regex("\"installdir\"\\s*\"([^\"]*)\"", RegexOption.IGNORE_CASE) - /** [single]: the entry a game added from the Games tab has; its exe stands in when no other is chosen and the user picked it. */ private fun scanGame(context: Context, folder: File, out: MutableList<Game>, single: AddedExes.Entry? = null) { run { - // A store install carries a sidecar; every one of them is a shortcut. val sidecar = StoreGameSidecar.read(folder) 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 } - // A store install says which exe it is; the folder's guess only when that file is gone. val found = chosen ?: sidecar?.exeFile(folder)?.takeIf { it.isFile } ?: candidates.firstOrNull() ?: return - // The store's launcher .bat stands in front of the exe when the game needs arguments or - // environment; it is what the shortcut runs. Dropped when the user picked another exe. 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 } - // A store install's title; else the title the disk gives (the shortcut's appid is kept - // per folder from its first scan, so a name found later does not move it); else the folder. val autoName = sidecar?.title ?: identifiedName(folder, found) ?: folder.name - // Keyed by the pre-rename path so shortcut ids, and the prefixes and saves under them, stay put. 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(File(LinuxRuntime.rootDir(context), "root/.local/share/Steam")) val record = config?.let { shortcutRecord(it, appId) } val rev = record?.optInt("rev", 0) ?: 0 - // Name, Start in and launch options edited in Steam's own Properties become the app's. if (sidecar == null && record != null) adoptSteamEdits(context, folder, record) val name = SessionPrefs.addedGameName(context, folder.path).takeIf { sidecar == null && it.isNotEmpty() } ?: autoName val adopted = guestPath(context, folder) @@ -460,12 +359,6 @@ object AddedGames { private fun unquote(value: String?): String = value.orEmpty().trim().trim('"') - /** - * What the user changed in Steam's Properties since the app last wrote the shortcut ([record]: - * what the app asked for, and what Steam had): a new name, launch options, or a Start in other - * than the exe's folder, taken over as the app's own. Each Steam value is taken once, so an app - * edit made after it is not undone before the shortcut is written again. - */ internal fun adoptSteamEdits(context: Context, folder: File, record: JSONObject) { val app = record.optJSONObject("app") ?: return val steam = record.optJSONObject("steam") ?: return @@ -489,7 +382,6 @@ object AddedGames { } } - /** The list the session hands the runtime's shortcuts writer; one file per session start. */ fun writeListing(context: Context, games: List<Game>): File { val file = File(context.filesDir, "session/added-games.json").apply { parentFile?.mkdirs() } val json = StringBuilder("[") @@ -500,8 +392,6 @@ object AddedGames { .append(",\"dir\":").append(quote(g.guestDir)).append(",\"appid\":").append(g.appId) .append(",\"seen\":").append(SessionPrefs.addedGameExeSeen(context, g.folder.path)) if (g.launchOptions.isNotEmpty()) json.append(",\"launch\":").append(quote(g.launchOptions)) - // The art, as the session sees it: the app's cache is bound at its own path, a file in - // the game's folder at the folder's guest path. val art = AddedGameArt.resolve(context, g) val pieces = listOf("p" to art.portrait, "header" to art.header, "hero" to art.hero, "logo" to art.logo, "icon" to art.icon) .mapNotNull { (k, f) -> f?.let { file -> artGuestPath(context, file)?.let { k to it } } } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGamesPublisher.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGamesPublisher.kt index 19243b8e8..7fc51558f 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGamesPublisher.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGamesPublisher.kt @@ -5,23 +5,15 @@ import android.util.Log import androidx.annotation.VisibleForTesting import java.util.concurrent.Executors -/** - * The slow part of an added-game change, off the caller's path and in order: the running client - * told ([later]'s work), then the listing the shortcuts writer reads at the next session start and - * the library cache, both from one scan ([republish]). An add, an edit or a removal shows at once; - * this follows. - */ object AddedGamesPublisher { private const val TAG = "AddedGamesPublisher" - /** Tests publish at once, to read the listing right after a change. */ @VisibleForTesting @Volatile internal var now = false private val worker = Executors.newSingleThreadExecutor { r -> Thread(r, "added-games-publish") } - /** [live] (a DevTools call or nothing), then [republish], on the worker. */ fun later(context: Context, live: () -> Unit = {}) { val app = context.applicationContext val work = Runnable { @@ -31,7 +23,6 @@ object AddedGamesPublisher { if (now) work.run() else worker.execute(work) } - /** The listing and the library cache from one scan of the added games. Blocking. */ fun republish(context: Context) { val games = AddedGames.scan(context) AddedGames.writeListing(context, games) diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AutoComponents.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AutoComponents.kt index b71c8c1a4..ced1abfbe 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AutoComponents.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AutoComponents.kt @@ -16,29 +16,12 @@ import org.json.JSONObject import java.io.File import java.util.concurrent.PriorityBlockingQueue -/** - * Turns on, per game, the Windows components its source's list names ([WinCompSources]). Only the - * findings marked auto are turned on, only the ones that install here, and never one the user - * switched off: the user's switches are kept apart ([WinComponents.Selection.user]) and win. - * - * All the work is done by one background thread (low priority, so are the runtime helpers it - * starts), one job at a time from one queue: working out a game's list (kept, so a page opens on - * it) and downloading a component (each once, whichever games want it). The queue waits while a - * session runs; jobs nobody asked for (the start-up sweep, a game being launched) also wait on - * battery saver, a warm device or a metered network. Nothing here runs on the caller's thread. - */ object AutoComponents { private const val TAG = "AutoComponents" - /** The key a game's picks are kept under: the appid its prefix is named after. */ fun appKey(game: Library.SteamGame): String? = if (game.library == Library.ADDED) Integer.toUnsignedString(game.appId) else game.appId.takeIf { it > 0 }?.toString() - /** - * What turns on by itself for [findings], given [catalog] and what is [installed]: the catalog - * name each auto finding installs as, with its reason. A component that needs a Windows - * installer, or is not in the catalog, stays out (the editor lists it as a suggestion). - */ fun autoPicks(findings: List<WinCompSources.Finding>, catalog: Map<String, WinComponents.Component>): Map<String, WinComponents.AutoReason> { val out = LinkedHashMap<String, WinComponents.AutoReason>() for (f in findings) { @@ -51,30 +34,22 @@ object AutoComponents { return out } - // ---- seams for tests ---- @VisibleForTesting internal var catalogOf: (Context, Boolean) -> List<WinComponents.Component>? = { c, network -> if (network) WinComponents.fetch(c) else WinComponents.cached(c) } @VisibleForTesting internal var installedOf: (Context) -> Set<String> = { WinComponents.installedIds(it).toSet() } @VisibleForTesting internal var installer: (Context, WinComponents.Component, Map<String, WinComponents.Component>, (WinComponents.Progress) -> Unit) -> String? = { c, comp, all, progress -> WinComponents.install(c, comp, all, progress) } @VisibleForTesting internal var sessionRunning: () -> Boolean = { SessionState.running } - /** Battery saver, a warm device or a metered network: no work nobody asked for. */ @VisibleForTesting internal var unfavourable: (Context) -> Boolean = { c -> constrained(c) } - /** A usable network: a download waits for one. */ @VisibleForTesting internal var online: (Context) -> Boolean = { c -> isOnline(c) } - /** Tests run the queue on the caller's thread ([drain]) instead of the worker. */ @VisibleForTesting @Volatile internal var runNow = false - /** Bumped whenever a game's list or automatic components change, so an open page reads them again. */ val changes = MutableStateFlow(0) - /** The components downloading for an automatic pick right now, with their progress. */ val downloading = MutableStateFlow<Map<String, WinComponents.Progress>>(emptyMap()) - /** Games (by [appKey]) whose list is queued or being worked out: a page waits on it. */ val pendingGames = MutableStateFlow<Set<String>>(emptySet()) - /** Components queued for an automatic pick, not yet downloaded. */ val pendingComponents = MutableStateFlow<Set<String>>(emptySet()) private fun mark(job: Job, on: Boolean) { @@ -84,10 +59,8 @@ object AutoComponents { flow.value = if (on) flow.value + id else flow.value - id } - /** One piece of work: a game's list ([game]) or one component's download ([component]). [asked]: the user is waiting on it. */ internal class Job( val game: Library.SteamGame?, val component: String?, val asked: Boolean, val seq: Long, val pics: List<Int>? = null, - /** For a game Steam installed during the running session: may run in it, unless a game is running. */ val inSession: Boolean = false, ) { val key get() = when { @@ -95,24 +68,17 @@ object AutoComponents { component != null -> "c:$component" else -> "g:" + (game?.let { appKey(it) } ?: "") } - /** Needs the network: a download, or Steam asked anonymously. */ val online get() = component != null || pics != null } private val seq = java.util.concurrent.atomic.AtomicLong() - /** What the user asked for first, then in the order queued. */ private val jobs = PriorityBlockingQueue<Job>(16, compareByDescending<Job> { it.asked }.thenBy { it.seq }) @Volatile private var draining = false private val queuedKeys = java.util.Collections.synchronizedSet(HashSet<String>()) - /** The games waiting on a component's download, to turn it on for when it lands. */ private val waiting = HashMap<String, MutableList<Library.SteamGame>>() @Volatile private var worker: Thread? = null private const val PAUSE_MS = 15_000L - /** - * [game]'s list worked out again on the worker, and its components queued: [asked] when the - * user is waiting (its page is open, or they installed or added it). - */ fun queueGame(context: Context, game: Library.SteamGame, asked: Boolean) { if (appKey(game) == null) return enqueue(context, Job(game, null, asked, seq.incrementAndGet())) @@ -121,10 +87,8 @@ object AutoComponents { private fun queueDownload(context: Context, id: String, asked: Boolean, inSession: Boolean = false) = enqueue(context, Job(null, id, asked, seq.incrementAndGet(), inSession = inSession)) - /** Adds [job] unless the same one is queued (a game or a component, once each). */ private fun enqueue(context: Context, job: Job) { if (!queuedKeys.add(job.key)) { - // Asked for now: ahead of what nobody asked for, so the open page does not wait behind the sweep. if (job.asked && jobs.removeIf { it.key == job.key && !it.asked }) jobs.add(job) if (runNow && !draining) drain(context) return @@ -135,12 +99,10 @@ object AutoComponents { if (runNow) { if (!draining) drain(context) } else ensureWorker(context.applicationContext) } - /** Whether [job] has to wait: always while a session runs; one nobody asked for also when it is unfavourable. */ internal fun mustWait(context: Context, job: Job): Boolean = if (job.inSession && sessionRunning()) gameRunning() || (job.online && !online(context)) else sessionRunning() || (job.online && !online(context)) || (!job.asked && unfavourable(context)) - /** A game is running in the session (Steam's reaper, or Proton waiting on its game). */ @VisibleForTesting internal var gameRunning: () -> Boolean = { gameProcessRunning() } private fun gameProcessRunning(): Boolean = File("/proc").listFiles()?.any { p -> @@ -148,17 +110,11 @@ object AutoComponents { ?.let { it.contains("SteamLaunch AppId=") || it.contains("proton waitforexitandrun") } == true } == true - /** - * A game Steam installed during the running session: its list worked out and its components - * readied in the session (one job, low priority), so a launch right after finds them; it - * waits while a game runs. All other work stays paused until the session ends. - */ fun queueInSession(context: Context, game: Library.SteamGame) { if (appKey(game) == null) return enqueue(context, Job(game, null, true, seq.incrementAndGet(), inSession = true)) } - /** Runs the queued jobs on this thread until the queue is empty or the next one has to wait. Tests. */ @VisibleForTesting internal fun drain(context: Context) { draining = true @@ -173,7 +129,6 @@ object AutoComponents { } } - /** The first job in order that may run now, taken off the queue; null when none may. */ private fun next(context: Context): Job? = synchronized(jobs) { val job = jobs.sortedWith(order).firstOrNull { !mustWait(context, it) } ?: return null jobs.remove(job) @@ -190,8 +145,6 @@ object AutoComponents { worker = Thread({ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND) while (true) { - // Something queued, then the first that may run (one for a game installed in the - // session goes ahead of paused work); else a look again in a while. try { jobs.put(jobs.take()) } catch (e: InterruptedException) { return@Thread } val job = next(app) if (job == null) { @@ -219,21 +172,14 @@ object AutoComponents { } } - /** - * [game]'s list (worked out again only when what it is read from changed), its automatic - * components that are downloaded turned on, and the rest queued for download. - */ private fun apply(context: Context, game: Library.SteamGame, asked: Boolean, inSession: Boolean = false) { val key = appKey(game) ?: return val folder = game.gameFiles if (game.library == Library.ADDED && game.source == Library.ADDED && folder != null && SteamMatch.get(context, folder.path) == null) { AddedGames.single(context, folder.path)?.let { SteamMatch.resolve(context, folder.path, it.exe, it.name) } } - // A GOG or Epic game installed before its sidecar kept the store's list: read it once from the store. val store = com.droiddeck.launcher.stores.Store.byId(game.source) if (folder != null && (store == com.droiddeck.launcher.stores.Store.GOG || store == com.droiddeck.launcher.stores.Store.EPIC)) StoreListBackfill.fill(context, folder) - // Its Steam app, when the client never saw it: Steam asked anonymously now if the user is - // waiting on this game (a pass over every game asks for all of them in one go, [sweep]). if (asked && online(context)) steamAppOf(context, game)?.let { SteamAppInfo.lookup(context, listOf(it)) } val findings = findings(context, game) val catalog = (catalogOf(context, false) ?: catalogOf(context, true))?.associateBy { it.name } ?: return @@ -249,7 +195,6 @@ object AutoComponents { queueDownload(context, id, asked, inSession) missing.add(id) } - // In a session the launch waits a little for these (droiddeck-wincomponents reads the list). if (inSession) Pending.set(context, key, missing) val before = WinComponents.selection(context, key).auto if (before.keys != kept.keys) { @@ -259,7 +204,6 @@ object AutoComponents { changes.value = changes.value + 1 } - /** One component downloaded and installed, then turned on for every game waiting on it. */ private fun download(context: Context, id: String, asked: Boolean, inSession: Boolean = false) { val catalog = catalogOf(context, true)?.associateBy { it.name } ?: run { Pending.drop(context, id); return } if (id !in installedOf(context)) { @@ -279,10 +223,6 @@ object AutoComponents { Pending.drop(context, id) } - /** - * The components still being readied for games installed in the session, where the launch - * reads them (~/.config/droiddeck/wincomponents-pending.json in the runtime). - */ internal object Pending { private fun file(context: Context) = File(com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context), "root/.config/droiddeck/wincomponents-pending.json") @@ -315,11 +255,9 @@ object AutoComponents { } } - // ---- the list, kept per game ---- private const val FINDINGS = "wincomp-findings.json" - /** What the list is read from, by modification time: a change means it is worked out again. */ internal fun signature(context: Context, game: Library.SteamGame): String { val folder = game.gameFiles val parts = listOfNotNull( @@ -334,7 +272,6 @@ object AutoComponents { return parts.joinToString("|") } - /** [game]'s list: the one kept when nothing it is read from changed, else worked out again and kept. Worker only. */ internal fun findings(context: Context, game: Library.SteamGame): List<WinCompSources.Finding> { val key = appKey(game) ?: return emptyList() val sig = signature(context, game) @@ -345,7 +282,6 @@ object AutoComponents { return fresh } - /** The list kept for [appKey], without working anything out; null before the first time. */ fun cachedFindings(context: Context, appKey: String): List<WinCompSources.Finding>? = readFindings(context).optJSONObject(appKey)?.let { decode(it.optJSONArray("findings")) } @@ -374,34 +310,21 @@ object AutoComponents { WinCompSources.Finding(o.optString("component").takeIf { it.isNotEmpty() }, o.optString("original"), origin, o.optBoolean("auto"), args) } - // ---- when ---- - /** Before a launch from DroidDeck: nothing but a queued look (it waits for the session to end). */ fun beforeLaunch(context: Context, game: Library.SteamGame) = queueGame(context, game, asked = false) - /** After an install or an add: the user did it, so it does not wait on a metered network. */ fun refreshLater(context: Context, game: Library.SteamGame) = queueGame(context, game, asked = true) - /** - * Every game, whatever its source, at app start and after each session (Steam installs its - * games without a word to DroidDeck; older GOG and Epic installs get their list read once): - * only queued, one game at a time, behind anything asked for, under the same conditions. A - * game whose files did not change costs a look at their times. - */ fun sweep(context: Context, games: List<Library.SteamGame>) { - // First every Steam app these games are that the client never saw, asked of Steam in one - // connection; then each game's list. val apps = games.mapNotNull { steamAppOf(context, it) }.distinct() if (apps.isNotEmpty() && SteamAppInfo.needLookup(context, apps).isNotEmpty()) enqueue(context, Job(null, null, false, seq.incrementAndGet(), pics = apps)) games.forEach { queueGame(context, it, asked = false) } } - /** The Steam app whose list a game takes: a Steam game's own, or an added game's match. */ private fun steamAppOf(context: Context, game: Library.SteamGame): Int? = if (game.library != Library.ADDED) game.appId.takeIf { it > 0 } else game.gameFiles?.let { SteamMatch.get(context, it.path) }?.appId - /** The default network is there and validated (reaches the internet). */ fun isOnline(context: Context): Boolean { val cm = context.getSystemService(ConnectivityManager::class.java) ?: return false val caps = cm.getNetworkCapabilities(cm.activeNetwork ?: return false) ?: return false @@ -409,7 +332,6 @@ object AutoComponents { caps.hasCapability(android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED) } - /** Battery saver, thermal status moderate or worse, or a metered network. */ private fun constrained(context: Context): Boolean { val power = context.getSystemService(PowerManager::class.java) if (power?.isPowerSaveMode == true) return true diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/CoverArt.kt b/app/src/main/java/com/droiddeck/launcher/frontend/CoverArt.kt index cf7f151aa..8bb83d15a 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/CoverArt.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/CoverArt.kt @@ -11,27 +11,12 @@ import java.net.HttpURLConnection import java.net.URL import java.net.URLEncoder -/** - * Box art for the emulators' games, found and kept on the device: the front end shows a disc image - * with nothing but its emulator's logo otherwise. - * - * In order: the name of the file against RetroArch's thumbnail collection for the system - * (libretro-thumbnails, named after the standard disc-dump names - "Need for Speed - Underground 2 - * (USA)"), first exactly and then loosely (tags dropped, the same region preferred) against the - * collection's list of names; then the disc's own ID, read from the image - PS2 serials against - * xlenore/ps2-covers (what PCSX2 and ARMSX2 download from), GameCube/Wii IDs against GameTDB (what - * Dolphin uses). A PS3 disc image gives its own PS3_GAME/ICON0.PNG first: the libretro PS3 - * collection is small. A cover is downloaded once into files/covers; a game none of them has is not - * asked about again for a few days. - */ object CoverArt { private const val TAG = "CoverArt" private const val MISS_RETRY_MS = 3L * 24 * 3600 * 1000 private const val INDEX_TTL_MS = 14L * 24 * 3600 * 1000 - /** Downloads in one pass, so a first visit to a big library does not hold the network for long. */ private const val MAX_PER_PASS = 40 - /** The emulator's systems in libretro-thumbnails, in the order they are tried. */ private val collections = mapOf( "rpcs3" to listOf("Sony_-_PlayStation_3"), "armsx2" to listOf("Sony_-_PlayStation_2"), @@ -44,14 +29,12 @@ object CoverArt { private fun root(context: Context) = File(context.filesDir, "covers") - /** The file name, as the cover is kept under: a game keeps its art when its folder moves. */ private fun keyOf(rom: Library.Rom): String = rom.hostPath.name.replace(Regex("[^A-Za-z0-9._() -]"), "_").take(150) private fun coverFile(context: Context, rom: Library.Rom) = File(root(context), "${rom.emulatorId}/${keyOf(rom)}.img") private fun missFile(context: Context, rom: Library.Rom) = File(root(context), "${rom.emulatorId}/${keyOf(rom)}.miss") - /** A cover already on the device for this game, or null. */ fun cached(context: Context, rom: Library.Rom): File? = coverFile(context, rom).takeIf { it.isFile && it.length() > 0 } private fun wanted(context: Context, rom: Library.Rom): Boolean { @@ -60,10 +43,6 @@ object CoverArt { return !(miss.isFile && System.currentTimeMillis() - miss.lastModified() < MISS_RETRY_MS) } - /** - * Looks for covers for the games that have none, on the calling (background) thread. True when - * at least one was found, so the caller rebuilds the list to show it. - */ fun fetchMissing(context: Context, roms: List<Library.Rom>): Boolean { var found = false var tries = 0 @@ -71,7 +50,6 @@ object CoverArt { if (!wanted(context, rom)) continue if (++tries > MAX_PER_PASS) break val target = coverFile(context, rom) - // A PS3 disc carries its own cover (PS3_GAME/ICON0.PNG, not encrypted): no download. if (rom.emulatorId == "rpcs3" && try { copyPs3Icon(rom.hostPath, target) } catch (e: Exception) { false }) { Log.i(TAG, "${rom.name}: ICON0.PNG from the disc") missFile(context, rom).delete() @@ -93,13 +71,10 @@ object CoverArt { private fun findUrl(context: Context, rom: Library.Rom): String? { val name = rom.hostPath.nameWithoutExtension.removeSuffix(".dec") for (collection in collections.getValue(rom.emulatorId)) { - // Exactly the dump name first: no listing needed. val exact = thumbnailUrl(collection, name) if (exists(exact)) return exact - // Then loosely, against the collection's names. nearest(name, names(context, collection))?.let { return thumbnailUrl(collection, it) } } - // The disc's own ID. return when (rom.emulatorId) { "armsx2" -> ps2Serial(rom.hostPath)?.let { "https://raw.githubusercontent.com/xlenore/ps2-covers/main/covers/default/$it.jpg" } "dolphin" -> nintendoDiscId(rom.hostPath)?.let { id -> @@ -110,18 +85,15 @@ object CoverArt { }?.takeIf { exists(it) } } - /** libretro-thumbnails replaces these in a title's file name. */ private fun thumbName(title: String) = title.replace(Regex("[&*/:`<>?\\\\|\"]"), "_") private fun thumbnailUrl(collection: String, title: String) = "https://raw.githubusercontent.com/libretro-thumbnails/$collection/master/Named_Boxarts/" + URLEncoder.encode("${thumbName(title)}.png", "UTF-8").replace("+", "%20") - // ---- loose matching ------------------------------------------------------------------------ private fun tags(name: String): List<String> = Regex("[(\\[]([^)\\]]*)[)\\]]").findAll(name).map { it.groupValues[1].lowercase() }.toList() - /** A title without its tags, articles and punctuation: "Legend of Zelda, The (USA)" = "legend of zelda". */ private fun base(name: String): String = name .replace(Regex("\\s*[(\\[][^)\\]]*[)\\]]"), " ") .lowercase() @@ -139,7 +111,6 @@ object CoverArt { return same.firstOrNull { region in tags(it) } ?: same.firstOrNull { "usa" in tags(it) } ?: same.first() } - /** The titles in a collection's Named_Boxarts (no extension), kept for a fortnight. */ private fun names(context: Context, collection: String): List<String> { val cache = File(root(context), "index-$collection.txt") if (cache.isFile && System.currentTimeMillis() - cache.lastModified() < INDEX_TTL_MS) return cache.readLines() @@ -161,12 +132,7 @@ object CoverArt { private fun cachedOr(cache: File): List<String> = if (cache.isFile) cache.readLines() else emptyList() - // ---- disc IDs ------------------------------------------------------------------------------ - /** - * A file's bytes from an ISO 9660 image by path ("PS3_GAME/ICON0.PNG"), or null. Primary volume - * names only (upper case, ";1" suffix), which is what PS2 and PS3 discs use. - */ private fun isoFile(file: File, path: String, maxSize: Int): ByteArray? { if (!file.name.endsWith(".iso", ignoreCase = true)) return null RandomAccessFile(file, "r").use { f -> @@ -208,14 +174,12 @@ object CoverArt { return if (bounds.outWidth > 0 && tmp.renameTo(target)) true else { tmp.delete(); false } } - /** "SLUS-21065" from a PS2 .iso's SYSTEM.CNF (BOOT2 = cdrom0:\SLUS_210.65;1), or null. */ private fun ps2Serial(file: File): String? { val cnf = String(isoFile(file, "SYSTEM.CNF", 4096) ?: return null, Charsets.US_ASCII) val m = Regex("BOOT2\\s*=\\s*cdrom0:\\\\?([A-Z]{4})_(\\d{3})\\.(\\d{2})").find(cnf) ?: return null return "${m.groupValues[1]}-${m.groupValues[2]}${m.groupValues[3]}" } - /** The six-character ID of a GameCube/Wii disc image (.iso/.gcm, or .wbfs), or null. */ private fun nintendoDiscId(file: File): String? { val offset = when (file.extension.lowercase()) { "iso", "gcm" -> 0L; "wbfs" -> 0x200L; else -> return null } RandomAccessFile(file, "r").use { f -> @@ -225,7 +189,6 @@ object CoverArt { } } - // ---- network ------------------------------------------------------------------------------- private fun open(url: String, method: String = "GET"): HttpURLConnection = (URL(url).openConnection() as HttpURLConnection).apply { @@ -245,7 +208,6 @@ object CoverArt { if (c.responseCode != 200) { c.disconnect(); null } else c.inputStream.bufferedReader().use { it.readText() } } catch (e: Exception) { Log.w(TAG, "$url: $e"); null } - /** Saves the image at [url] to [target] if it really is an image. */ private fun download(url: String, target: File): Boolean = try { val c = open(url) if (c.responseCode != 200) { c.disconnect(); false } else { diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/DependencyDetector.kt b/app/src/main/java/com/droiddeck/launcher/frontend/DependencyDetector.kt index 3b977bc4b..0acb02d37 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/DependencyDetector.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/DependencyDetector.kt @@ -3,49 +3,25 @@ package com.droiddeck.launcher.frontend import java.io.File import java.util.Locale -/** - * Best-effort detection of the Windows redistributables a game bundles, by scanning its install - * folder for the redist installers Steam/Epic/GOG ship (`_CommonRedist/`, `_Redist/`, - * `Redistributables/`, loose `vcredist_x64.exe`, …) and mapping them to entries in our component - * catalog (WinComponents, Bannerlator's components.json `name` keys). The result drives the - * Recommended part of a game's Windows components; the user still switches each one on. - * Bannerlator's detector, unchanged but for this note and its package. - * - * Wine-aware: keeps the redists that actually matter under Wine/DXVK (VC++ runtimes, .NET, PhysX, - * OpenAL, XACT, XNA, GFWL) and SKIPS the ones DXVK/VKD3D already provide (DX12 Agility SDK) or that - * aren't our components (anti-cheat, Rockstar/Social-Club/launcher setups). Never throws; returns an - * empty list on anything unexpected. Pure filesystem — no network, no Android deps (unit-testable). - */ object DependencyDetector { - /** How a recommendation was found — drives the UI's "recommended vs optional" split. */ enum class Kind { - /** The game bundles the redist installer (in _CommonRedist etc.) — meant to be installed. */ BUNDLED, - /** A runtime DLL the game ships loose — usually already works from the game folder; install - * only if the game misbehaves. */ SHIPPED, - /** Steam installs it with the game from Steamworks Shared ([SteamRedists]). */ STEAM, } - /** A recommended component: the catalog [componentName] to install, a display [label], the on-disk - * redist that triggered it ([reason]), and [kind] (bundled installer vs shipped DLL). */ data class Recommendation(val componentName: String, val label: String, val reason: String, val kind: Kind = Kind.BUNDLED) private const val MAX_DEPTH = 5 - private const val MAX_ENTRIES = 8000 // cost guard against giant game trees + private const val MAX_ENTRIES = 8000 - /** A loose VC++ installer with no year in its path — resolved to "latest" only if no specific - * vcredistYYYY was found anywhere (else it's just a duplicate of the versioned one). */ private const val VCREDIST_LOOSE = "__vcredist_loose" - /** Directory names that are redist containers worth descending into. */ private val REDIST_DIR_RE = Regex( """(?i)^(_?commonredist|_?redist(ributables)?|__?installer|vcredist|vc_redist|directx|direct_x|physx|openal|dotnet(fx)?|\.net|xna|xact|redist|support)$""" ) - /** Skip whole subtrees that are never our components. */ private val SKIP_DIR_RE = Regex("""(?i)^(d3d12.*|.*agility.*|dxvk|vkd3d|battleye|easyanticheat|eac|anticheat)$""") private val YEAR_TO_VCREDIST = mapOf( @@ -70,7 +46,6 @@ object DependencyDetector { "dotnetcoredesktop8" to ".NET Desktop 8", ) - /** Preferred install order (runtimes others may depend on first). */ private val INSTALL_ORDER = listOf( "vcredist2005", "vcredist2008", "vcredist2010", "vcredist2012", "vcredist2013", "vcredist2015", "vcredist2019", "vcredist2022", @@ -79,13 +54,6 @@ object DependencyDetector { "d3dx9", "d3dx11", "xact", "xact_x64", "xna31", "xna40", "physx", "oalinst", "XLiveRedist", ) - /** - * Detect from the .exe the user picked, resolving the game root first: walk up from the exe's - * folder (bounded) to the nearest ancestor that holds a redist container (`_CommonRedist`, …) at - * its top level, and scan from there — so a nested exe (`…/Game/bin/x.exe`) still finds a root-level - * `_CommonRedist`, without over-reaching into a shared `steamapps/common` (which has no redist - * container of its own). Falls back to scanning the exe's own folder. - */ fun detectForExe(exeFile: File): List<Recommendation> { val start = exeFile.absoluteFile.parentFile ?: return emptyList() var d: File? = start @@ -101,19 +69,14 @@ object DependencyDetector { return detect(start) } - /** - * Scan [gameDir] and return the recommended components, de-duplicated (first reason wins), in a - * stable install-friendly order. Empty when nothing is detected or [gameDir] isn't a directory. - */ fun detect(gameDir: File): List<Recommendation> { if (!gameDir.isDirectory) return emptyList() - val hits = LinkedHashMap<String, Recommendation>() // componentName -> first recommendation + val hits = LinkedHashMap<String, Recommendation>() var visited = 0 fun consider(f: File, isDir: Boolean, insideRedist: Boolean) { val name = f.name.lowercase(Locale.US) val path = f.path.replace('\\', '/').lowercase(Locale.US) - // Bundled = in a redist container or an installer (exe/msi/cab); shipped = a loose runtime DLL. val kind = if (insideRedist || isDir || name.endsWith(".exe") || name.endsWith(".msi") || name.endsWith(".cab")) Kind.BUNDLED else Kind.SHIPPED for (comp in classify(path, name, isDir)) { @@ -121,7 +84,7 @@ object DependencyDetector { if (existing == null) { hits[comp] = Recommendation(comp, LABELS[comp] ?: comp, relativeReason(gameDir, f), kind) } else if (existing.kind == Kind.SHIPPED && kind == Kind.BUNDLED) { - hits[comp] = existing.copy(kind = Kind.BUNDLED) // a bundled installer outranks a shipped DLL + hits[comp] = existing.copy(kind = Kind.BUNDLED) } } } @@ -134,17 +97,11 @@ object DependencyDetector { val lower = f.name.lowercase(Locale.US) if (f.isDirectory) { if (SKIP_DIR_RE.matches(lower)) continue - // Unity's <Game>_Data/Managed holds the game's own Mono assemblies, not a .NET - // Framework to install. if (lower == "managed" && dir.name.endsWith("_Data", ignoreCase = true)) continue val isRedistDir = insideRedist || REDIST_DIR_RE.matches(lower) if (isRedistDir) consider(f, isDir = true, insideRedist = true) - // Aggressive coverage: descend redist containers fully, and otherwise sweep the game - // tree a few levels deep so runtime libraries the game ships (in bin/, etc.) are seen. if (isRedistDir || depth < 3) walk(f, depth + 1, isRedistDir) } else { - // Classify installers + runtime libraries (.dll/.exe/.msi) anywhere in the scanned - // tree; other files are ignored. Recommendations are advisory, so cast a wide net. val ext = lower.substringAfterLast('.', "") if (insideRedist || ext == "dll" || ext == "exe" || ext == "msi") consider(f, isDir = false, insideRedist = insideRedist) } @@ -160,20 +117,13 @@ object DependencyDetector { } } - /** - * The catalog components one installer stands for, by its path ("_CommonRedist/vcredist/2015/ - * vc_redist.x64.exe", "DXSETUP.exe"): what a store's install list names. A VC++ installer - * with no year is the latest; empty for one that is none of ours. - */ fun componentsFor(path: String): List<String> { val p = path.replace('\\', '/').lowercase(Locale.US) return classify(p, p.substringAfterLast('/'), isDir = false).map { if (it == VCREDIST_LOOSE) "vcredist2022" else it } } - /** The name the catalog component [name] goes by, for a reason line. */ fun label(name: String): String = LABELS[name] ?: name - /** A loose VC++ installer resolves to the latest 14.x only if no specific vcredistYYYY was found. */ private fun resolveLooseVcredist(hits: LinkedHashMap<String, Recommendation>) { val loose = hits.remove(VCREDIST_LOOSE) ?: return val hasSpecific = hits.keys.any { it in YEAR_TO_VCREDIST.values } @@ -186,52 +136,38 @@ object DependencyDetector { return if (p.startsWith("$g/")) p.substring(g.length + 1) else f.name } - /** - * Map a redist file/dir (its full lowercased [path], leaf [name], and whether it [isDir]) to - * catalog component name(s). Empty for things we deliberately skip. - */ private fun classify(path: String, name: String, isDir: Boolean): List<String> { - // Hard skips first — DX12 is provided by DXVK/VKD3D; these aren't our components. if (path.contains("d3d12") || path.contains("agility") || path.contains("battleye") || path.contains("anticheat") || path.contains("easyanticheat") || name.contains("socialclub") || name.contains("social-club") || name.contains("rockstar") || name == "setup.exe" ) return emptyList() - // Visual C++ runtimes — most important. Year comes from the path context. if (path.contains("vcredist") || path.contains("vc_redist") || name.startsWith("vcredist") || name.startsWith("vc_redist")) { val year = YEAR_TO_VCREDIST.keys.firstOrNull { path.contains(it) } return when { year != null -> listOf(YEAR_TO_VCREDIST.getValue(year)) - isDir -> emptyList() // bare "vcredist" container — wait for the year subfolder / installer - else -> listOf(VCREDIST_LOOSE) // loose installer, no year → resolved post-walk + isDir -> emptyList() + else -> listOf(VCREDIST_LOOSE) } } - // VC++ runtime DLL the game ships (msvcp140.dll → 2015-2022, msvcp120.dll → 2013, …). VCRUNTIME_DLL.entries.firstOrNull { name.startsWith(it.key) }?.let { return listOf(it.value) } if (path.contains("openal") || name.startsWith("oalinst") || name == "openal32.dll" || name == "wrap_oal.dll") return listOf("oalinst") if (path.contains("physx") || name.startsWith("physx")) return listOf("physx") if (path.contains("xact") || name.startsWith("xactengine") || name.startsWith("x3daudio")) return listOf("xact", "xact_x64") - // Games for Windows Live — the redist folder, OR a GFWL library the game ships (xlive.dll, - // Codemasters' dbxLive32/64, gfwl*): GFWL titles need XLiveRedist to run even when they don't - // bundle the redist itself. if (path.contains("xliveredist") || name.contains("xlive") || name.startsWith("gfwl") || name.startsWith("dbxlive")) { return listOf("XLiveRedist") } if (path.contains("xna")) return listOf(if (path.contains("3.1") || path.contains("3_1") || path.contains("v31")) "xna31" else "xna40") - // .NET — Framework and Core/Desktop runtimes. if (path.contains("dotnet") || path.contains(".net") || name.startsWith("ndp") || name.startsWith("dotnetfx") || name.contains("windowsdesktop-runtime")) { val comp = dotnetComponent("$path $name") return when { comp != null -> listOf(comp) - isDir -> emptyList() // bare ".NET" container — wait for the versioned installer - else -> listOf("dotnet48") // unlabeled .NET Framework installer file → sensible default + isDir -> emptyList() + else -> listOf("dotnet48") } } - // DirectX 11 helper DLLs the game ships (d3dx11_43.dll). if (D3DX11_RE.matches(name)) return listOf("d3dx11") - // DirectX 9 helper DLLs / d3dcompiler / June-2010 DX redist (DXVK provides core d3d9 but NOT - // these helper DLLs). if (name.startsWith("dxsetup") || name.startsWith("dxwebsetup") || D3DX9_RE.matches(name) || D3DCOMPILER_RE.matches(name) || (path.contains("directx") && name.endsWith(".exe")) @@ -239,7 +175,6 @@ object DependencyDetector { return emptyList() } - /** Runtime DLL prefix → the VC++ redistributable that provides it (checked with startsWith). */ private val VCRUNTIME_DLL = linkedMapOf( "vcruntime140" to "vcredist2022", "msvcp140" to "vcredist2022", "msvcr140" to "vcredist2022", "concrt140" to "vcredist2022", "msvcp120" to "vcredist2013", "msvcr120" to "vcredist2013", @@ -252,7 +187,6 @@ object DependencyDetector { private val D3DX11_RE = Regex("""d3dx11_\d+\.dll""") private val D3DCOMPILER_RE = Regex("""d3dcompiler_\d+\.dll""") - /** Resolve a .NET component from installer/path text, or null when no version is discernible. */ private fun dotnetComponent(s: String): String? = when { s.contains("desktop-runtime-8") || Regex("""\b8\.\d""").containsMatchIn(s) -> "dotnetcoredesktop8" s.contains("desktop-runtime-7") || Regex("""\b7\.\d""").containsMatchIn(s) -> "dotnetcoredesktop7" @@ -264,6 +198,6 @@ object DependencyDetector { s.contains("dotnetfx45") || s.contains("ndp45") || s.contains("4.5") -> "dotnet45" s.contains("dotnetfx40") || s.contains("ndp40") || s.contains("4.0") -> "dotnet40" s.contains("dotnetfx35") || s.contains("3.5") -> "dotnet35" - else -> null // no version signal (e.g. a bare "DotNet" container dir) + else -> null } } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GameExePicker.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GameExePicker.kt index 1ceadbfa4..f692c7523 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GameExePicker.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GameExePicker.kt @@ -2,26 +2,15 @@ package com.droiddeck.launcher.frontend import java.io.File -/** - * Which `.exe` in a game folder is the game. A real install holds installers, crash handlers, - * anti-cheat stubs and launchers beside the game, and the game itself may sit several levels down - * (`Game/Binaries/Win64/Game-Win64-Shipping.exe`), so candidates are collected with a bounded walk - * and ranked rather than guessed by position. - */ object GameExePicker { - /** How far below a game folder to look: Unreal puts the game three down, repacks deeper. */ const val MAX_DEPTH = 5 - /** Folders and .exe files looked at per game before the walk stops, so a huge game stays quick. */ const val WALK_BUDGET = 400 - /** Below this the pick is marked uncertain rather than trusted silently. */ const val CONFIDENT_SCORE = 60 - /** The best and the runner-up this close together: nothing clearly won. */ const val AMBIGUOUS_MARGIN = 15 - /** Folders that never hold the game itself. */ private val SKIP_DIRS = setOf( "_commonredist", "commonredist", "redist", "redistributable", "redistributables", "directx", "dotnet", "dotnetfx", "vcredist", "vc_redist", "prerequisites", "prereq", @@ -32,7 +21,6 @@ object GameExePicker { private val SKIP_DIR_PREFIX = listOf("dotnet", "vcredist", "$") private val SKIP_PATHS = listOf("engine/extras", "engine/binaries/thirdparty") - /** Exe names that are never the game. */ private val JUNK_EXE_RE = Regex( """(?i)^(unins\w*|setup|install\w*|vc_?redist.*|vcredist.*|dxsetup|dxwebsetup|directx.*|""" + """oalinst|openal.*|dotnetfx.*|ndp\d.*|unitycrashhandler\d*|crashreport\w*|crashpad\w*|""" + @@ -41,10 +29,8 @@ object GameExePicker { """activation\w*|register\w*|readme|report\w*|.*_debug|.*-debug)$""", ) - /** A folder's exes best first, with their scores, and whether the first clearly won. */ class Ranking(val exes: List<File>, val scores: List<Int>, val uncertain: Boolean) - /** Ranks the .exe files under [folder]; [keep] drops names the caller never offers. */ fun rank(folder: File, keep: (File) -> Boolean = { true }): Ranking { val scored = collect(folder).filter { keep(it.first) }.map { (exe, depth) -> exe to score(exe, folder, depth) } .sortedWith(compareByDescending<Pair<File, Int>> { it.second }.thenBy { it.first.path.lowercase() }) @@ -54,11 +40,6 @@ object GameExePicker { return Ranking(scored.map { it.first }, scored.map { it.second }, uncertain) } - /** - * Every plausible exe under [folder] with its depth (0 = in the folder itself): up to - * [MAX_DEPTH] folders down, nearest levels first, at most [WALK_BUDGET] entries, never through a - * symlink, past folders that never hold the game. - */ private fun collect(folder: File): List<Pair<File, Int>> { val found = ArrayList<Pair<File, Int>>() var visited = 0 @@ -86,31 +67,23 @@ object GameExePicker { return found } - /** - * How likely [exe] is to be the game in [folder]. Relative only: the numbers rank candidates - * against each other and decide whether anything won clearly enough to trust. - */ internal fun score(exe: File, folder: File, depth: Int): Int { var score = 0 val base = exe.nameWithoutExtension val exeKey = key(base) val folderKey = key(folder.name) - // The exe named after its folder is the single strongest signal. if (exeKey.isNotEmpty() && folderKey.isNotEmpty()) { when { exeKey == folderKey -> score += 100 folderKey.contains(exeKey) || exeKey.contains(folderKey) -> score += 60 } } - // Unreal ships as <Game>-Win64-Shipping.exe under Binaries/Win64. if (base.endsWith("-Win64-Shipping", true) || base.endsWith("-Win32-Shipping", true)) score += 70 val path = exe.absolutePath.lowercase() if (path.contains("/binaries/win64") || path.contains("/binaries/win32")) score += 30 if (path.contains("/bin/") || path.contains("/bin64/")) score += 10 - // A launcher is a real entry point, so it stays a candidate, just not the preferred one. if (GameIdentifier.isLauncherExeName(base)) score -= 40 if (GameIdentifier.isJunkPeName(base)) score -= 30 - // Shallower is more likely the entry point; big binaries beat small helpers. score -= depth * 8 score += (exe.length() / (8L * 1024 * 1024)).toInt().coerceAtMost(30) return score @@ -125,6 +98,5 @@ object GameExePicker { private fun isLink(f: File): Boolean = runCatching { java.nio.file.Files.isSymbolicLink(f.toPath()) }.getOrDefault(false) - /** Comparison key: letters and digits only, so "GTA IV" and "gta-iv" match. */ private fun key(s: String) = s.lowercase().filter { it.isLetterOrDigit() } } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GameFileSync.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GameFileSync.kt index 3bef04f5a..0196797cf 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GameFileSync.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GameFileSync.kt @@ -10,7 +10,6 @@ import java.io.File import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -/** Runs while DroidDeck's process is alive, including downloads in a background Steam session. */ object GameFileSync { private val worker = Executors.newSingleThreadScheduledExecutor { task -> Thread(task, "game-file-sync").apply { isDaemon = true } } private fun prefs(context: Context) = context.getSharedPreferences("game_files", Context.MODE_PRIVATE) @@ -39,7 +38,6 @@ object GameFileSync { @Synchronized private fun sync(context: Context, folder: File) { check(LinuxRuntime.isInstalled(context)) { context.getString(R.string.game_sync_err_runtime) } - // A disconnected library must not be interpreted as a mass uninstall. 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) diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GameFiles.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GameFiles.kt index 6b39093c0..8e0b55def 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GameFiles.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GameFiles.kt @@ -11,14 +11,12 @@ import org.json.JSONObject import java.io.File import java.io.InputStream -/** Small, portable launch files for frontends that scan ROM folders. No guest paths or commands. */ object GameFiles { const val EXTENSION = "steam" private const val MAX_BYTES = 128 private const val INVENTORY = ".droiddeck-sync.json" private const val FORMAT = "droiddeck-game-files-v1" - /** Called on a worker: content providers and shared storage may block. */ fun readIntent(context: Context, request: Intent): String? = runCatching { if (request.action != Intent.ACTION_VIEW) return null val uri = request.data ?: return null @@ -36,7 +34,6 @@ object GameFiles { }.getOrNull() fun read(stream: InputStream): String? { - // An untrusted URI must not make us buffer an arbitrary ROM or parse a command. val bytes = ByteArray(MAX_BYTES + 1) var count = 0 while (count < bytes.size) { @@ -84,7 +81,6 @@ object GameFiles { } else target } - /** Cocoon matches icons in its data folder's images directory. Never replace custom art. */ private fun exportArt(context: Context, folder: File, game: Library.SteamGame) { val art = listOfNotNull(game.icon, game.art).firstOrNull { it.isFile && it.extension.lowercase() in listOf("jpg", "jpeg", "png", "webp") @@ -100,7 +96,6 @@ object GameFiles { if (!target.exists()) AtomicFile(target).writeBytes(art.readBytes()) } - /** Only remove files in our inventory whose content is still exactly what we wrote. */ @Synchronized fun sync(context: Context, folder: File, games: List<Library.SteamGame>) { require(folder.isDirectory) { context.getString(R.string.gfiles_folder_unavailable) } val inventory = child(context, folder, INVENTORY) @@ -119,21 +114,17 @@ object GameFiles { else exportTarget(context, folder, game, name) val text = contents(game.gameIdString) val existed = target.exists() - // Keep an edited export without letting it block the rest of the library sync. val owned = inventoryOwns(context, folder, previous, target, game.gameIdString) val file = if (owned && target.exists() && !matches(target, text)) target else exportNamed(context, folder, game, target.name) if (!existed || owned) next.put(file.name, game.gameIdString) file } - // Write new exports before pruning; a failed write must not remove old launch files. for (name in previous.keys()) { val id = previous.getString(name) if (!next.has(name) && name.endsWith(".$EXTENSION") && GameLaunchLink.validId(id)) { val file = child(context, folder, name) if (matches(file, contents(id))) { - // Android shared storage is often case insensitive. A case-only rename may - // still resolve to the live export, which must not be pruned as an old file. val retained = exports.any { java.nio.file.Files.isSameFile(it.toPath(), file.toPath()) } if (!retained) check(file.delete()) { context.getString(R.string.gfiles_remove_failed, name) } } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GameIdentifier.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GameIdentifier.kt index d3b88b2a9..4b385d896 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GameIdentifier.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GameIdentifier.kt @@ -2,65 +2,35 @@ package com.droiddeck.launcher.frontend import java.io.File -/** - * Best-effort identification of a game from its on-disk footprint, given its .exe: a Steam appId - * and/or a display name, so an added game is not just named after its folder or exe. Pure - * filesystem and PE parsing, no network, never throws; it reports only what the disk tells. - * - * Signal chain (appId preferred; best name chosen across all hits): - * 1. steam_appid.txt → appId (Goldberg-style installs) - * 2. ColdClientLoader / emu ini → appId (AppId= key) - * 3. appmanifest_<id>.acf → appId + name (real Steam install under steamapps/) - * 4. goggame-<id>.info → name (+ gogId) (GOG installs) - * 5. PE VS_VERSIONINFO → name (ProductName / FileDescription) - * 6. cleaned folder / exe name → name (weak fallback) - */ object GameIdentifier { - /** How far up the directory tree to look for marker files (games nest deep, e.g. Game/Binaries/Win64/x.exe). */ private const val MAX_PARENT_DEPTH = 6 enum class Source { STEAM_APPID_TXT, STEAM_MANIFEST_ACF, STEAM_EMU_INI, GOG_INFO, PE_VERSION, FILENAME, NONE } enum class Confidence { HIGH, MEDIUM, LOW } - /** - * @param appId Steam appId if one was found on disk, else null. - * @param gogId GOG product id if a goggame info file was found, else null. - * @param name Best display name found locally; may be null when only an appId was - * found (caller resolves the name from the appId over the network). - * @param source Which signal produced the primary identity. - */ data class GameIdentity( val appId: Int? = null, val gogId: String? = null, val name: String? = null, val source: Source = Source.NONE, val confidence: Confidence = Confidence.LOW, - /** Which signal gave [name]: [Source.FILENAME] means only a cleaned folder or exe name. */ val nameSource: Source = Source.NONE, ) { val hasStrongId: Boolean get() = appId != null } - /** Identify the game owning [exeFile]. Never throws; returns an empty identity if nothing matched. */ fun identify(exeFile: File): GameIdentity { val dirs = ancestorDirs(exeFile) - // Steam appId from a bare steam_appid.txt (in the exe dir, a steam_settings/ subdir, or an ancestor). val steamAppId = dirs.firstNotNullOfOrNull { readSteamAppIdTxt(it) } - // AppId= out of a ColdClientLoader.ini / steam_emu.ini next to the game. val emuAppId = dirs.firstNotNullOfOrNull { readEmuIniAppId(it) } - // Real Steam install: appmanifest_<id>.acf up under steamapps/ — carries both appId and name. val acf = resolveSteamManifest(exeFile, dirs) - // GOG: goggame-<id>.info JSON with the real title. val gog = dirs.firstNotNullOfOrNull { readGogInfo(it) } - // PE ProductName / FileDescription — but not when it merely names a launcher/loader - // ("Rockstar Games Launcher Redirector", "GSE", …), which must not become the game name. val peName = PeVersionInfo.bestName(PeVersionInfo.read(exeFile))?.takeUnless { isJunkPeName(it) } val appId = acf?.appId ?: steamAppId ?: emuAppId - // Best name, most-trustworthy first. val nameHit: Pair<String, Source>? = when { acf?.name != null -> acf.name to Source.STEAM_MANIFEST_ACF gog?.name != null -> gog.name to Source.GOG_INFO @@ -68,7 +38,6 @@ object GameIdentifier { else -> cleanedFallbackName(exeFile)?.let { it to Source.FILENAME } } - // Primary source = whatever gave us the appId (strongest), else the name source. val primary: Source = when { acf?.appId != null -> Source.STEAM_MANIFEST_ACF steamAppId != null -> Source.STEAM_APPID_TXT @@ -93,27 +62,19 @@ object GameIdentifier { ) } - /** - * Make an identified title readable and filesystem-safe (a Steam shortcut's name also names - * files): "DARK SOULS: REMASTERED" becomes "DARK SOULS - REMASTERED". Strips trademark marks, - * turns title punctuation into safe equivalents, and collapses whitespace. - */ fun normalizeName(raw: String): String { var s = raw s = s.replace("™", "").replace("®", "").replace("©", "") - s = s.replace(Regex("""(?i)\(\s*(tm|r|c)\s*\)"""), "") // "(TM)" / "(R)" / "(C)" - s = s.replace(":", " - ") // colon → dash separator (safe + readable) - s = s.replace(Regex("""[\\/]"""), " ") // path separators → space - s = s.replace(Regex("""["*?<>|]"""), "") // other illegal chars → drop - // Drop trailing generic descriptors a PE FileDescription tacks on ("DiRT 3 Executable" → "DiRT 3"). + s = s.replace(Regex("""(?i)\(\s*(tm|r|c)\s*\)"""), "") + s = s.replace(":", " - ") + s = s.replace(Regex("""[\\/]"""), " ") + s = s.replace(Regex("""["*?<>|]"""), "") s = s.replace(Regex("""(?i)[ \-]+(executable|application|launcher|redistributable|installer|setup)$"""), "") - s = s.replace(Regex("""\s+"""), " ").trim() // collapse whitespace - return s.trim(' ', '-').trim() // tidy stray leading/trailing dashes + s = s.replace(Regex("""\s+"""), " ").trim() + return s.trim(' ', '-').trim() } - // ── directory walk ── - /** The exe's directory plus its ancestors, nearest-first, bounded by [MAX_PARENT_DEPTH]. */ private fun ancestorDirs(exeFile: File): List<File> { val out = ArrayList<File>(MAX_PARENT_DEPTH + 1) var d: File? = exeFile.absoluteFile.parentFile @@ -126,12 +87,9 @@ object GameIdentifier { return out } - // ── signal readers (all best-effort; return null on anything unexpected) ── - /** steam_appid.txt directly in [dir] or in a steam_settings/ subdir. */ private fun readSteamAppIdTxt(dir: File): Int? { for (candidate in arrayOf(File(dir, "steam_appid.txt"), File(dir, "steam_settings/steam_appid.txt"))) { - // Strip a leading UTF-8 BOM (trim() doesn't) before reading the digits. val id = readSmallTextFile(candidate)?.removePrefix("\uFEFF")?.trim() ?.takeWhile { it.isDigit() }?.toIntOrNull() if (id != null && id > 0) return id @@ -139,12 +97,6 @@ object GameIdentifier { return null } - /** - * First `AppId=<n>` in a crack/emu config .ini next to the game (ColdClientLoader, steam_emu, - * flt, hlm, …). Known names are tried first, then any other `.ini` in the dir — only crack - * configs carry an `AppId=` key, game settings .ini files (settings.ini, graphics.ini) don't, - * so the broad scan is safe. - */ private fun readEmuIniAppId(dir: File): Int? { val re = Regex("""(?im)^\s*appid\s*=\s*(\d+)""") val named = arrayOf( @@ -166,13 +118,6 @@ object GameIdentifier { private data class Acf(val appId: Int?, val name: String?) - /** - * Resolve the Steam appmanifest for this game: find the `common` ancestor on the exe's - * path, read the sibling `steamapps/appmanifest_*.acf`, and match by installdir == the - * game folder under common. No match, no manifest: a folder in steamapps/common that no - * manifest claims is exactly the kind added games list, and the library's one other game is - * not its name. - */ private fun resolveSteamManifest(exeFile: File, dirs: List<File>): Acf? { val commonIdx = dirs.indexOfFirst { it.name.equals("common", ignoreCase = true) } if (commonIdx < 0) return null @@ -183,7 +128,6 @@ object GameIdentifier { } ?: return null if (manifests.isEmpty()) return null - // The game's install folder is the child of `common` on our descent path. val gameFolder = if (commonIdx > 0) dirs[commonIdx - 1].name else null val match = manifests.firstOrNull { m -> @@ -200,7 +144,6 @@ object GameIdentifier { private data class Gog(val id: String?, val name: String?) - /** goggame-<id>.info JSON: pull `name` (and the id from the filename). */ private fun readGogInfo(dir: File): Gog? { val files = dir.listFiles { f -> f.isFile && f.name.startsWith("goggame-") && f.name.endsWith(".info") @@ -214,75 +157,42 @@ object GameIdentifier { return null } - // ── filename fallback ── private val REPACK_TOKENS = setOf( "fitgirl", "dodi", "codex", "plaza", "skidrow", "rune", "empress", "flt", "tenoke", "reloaded", "elamigos", "razor1911", "hoodlum", "gog", "repack", "multi", "goldberg", - // repack/site tags that show up in folder names "ankergames", "steamrip", "onlinefix", "online-fix", "gload", "fitgirlrepacks", ) private const val EXE_NOISE_WORDS = """win64|win32|shipping|wingdk|client|launcher|game|x64|x86|64bit|32bit""" - /** - * Does this exe basename look like engine/arch boilerplate rather than a title? Deliberately - * UNANCHORED — "GameConTest" is boilerplate-ish wherever the giveaway sits, and the answer - * decides whether we trust the folder name instead of the exe name. - */ private val EXE_NOISE_RE = Regex("""(?i)[-_. ]*(?:$EXE_NOISE_WORDS)""") - /** - * Trailing noise to strip off a title. ANCHORED to the end and allowed to repeat, because these - * words are only junk as a suffix: "Witcher3-Win64-Shipping" -> "Witcher3", while - * "Game of Thrones" and "Client Simulator" must survive intact. - * - * Kept separate from [EXE_NOISE_RE] on purpose. One regex used to serve both jobs, so - * anchoring it to fix stripping ALSO narrowed the boilerplate test — and a game in - * "Game Controller Test/GameConTest.exe" stopped preferring its folder and became "GameConTest". - */ private val EXE_SUFFIX_RE = Regex("""(?i)(?:[-_. ]*(?:$EXE_NOISE_WORDS))+$""") private val VERSION_TOKEN_RE = Regex("""(?i)\b(v?\d+([._]\d+)+|build[-_ ]?\d+|update[-_ ]?\d+)\b""") - /** - * True when an exe basename is a launcher/loader rather than the game — its parent folder is the - * better title. Catches "PlayGTAV"/"Play - GTA IV"/"LaunchGame"/"MyLauncher"/"GSE" but NOT a real - * title that merely starts with those letters ("PlayerUnknown"). A "play|launch|start|run" prefix - * counts only when it's a whole token — followed by a separator or a camelCase uppercase boundary, - * not another lowercase letter (checked case-sensitively, so it survives the case-insensitive JUNK - * matching used elsewhere). - */ - /** True when an exe basename looks like a launcher/loader rather than the game itself. */ fun isLauncherExeName(base: String): Boolean { val lower = base.lowercase() if (lower == "gse" || lower.endsWith("launcher") || lower.endsWith("redirector")) return true for (p in arrayOf("play", "launch", "start", "run")) { if (!lower.startsWith(p)) continue val rest = base.substring(p.length) - if (rest.isEmpty()) return true // exactly "Play" + if (rest.isEmpty()) return true val c = rest[0] - if (!c.isLetterOrDigit() || c.isUpperCase()) return true // "Play - GTA IV" / camelCase "PlayGTAV" - // lowercase continuation ("Player…") → part of a longer word, not a launcher + if (!c.isLetterOrDigit() || c.isUpperCase()) return true } return false } - /** A PE ProductName/FileDescription that describes a launcher/loader, not the game — reject it so it - * doesn't become the shortcut name (e.g. "Rockstar Games Launcher Redirector", "GSE", "Steam"). */ 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.*)$""" ) - /** True when a PE name is a launcher/loader label rather than a real game title. */ fun isJunkPeName(name: String): Boolean { val n = name.trim() return n.isEmpty() || JUNK_NAME_RE.matches(n) } - /** - * Prefer the game's install-folder name (usually the real title) over the exe basename when the - * exe is a UE-style shipping binary OR a launcher/loader; then strip repack/suffix noise. - */ private fun cleanedFallbackName(exeFile: File): String? { val exeBase = exeFile.nameWithoutExtension val preferFolder = EXE_NOISE_RE.containsMatchIn(exeBase) || @@ -293,14 +203,12 @@ object GameIdentifier { return cleanName(raw) } - /** Strip bracketed tags, repack-group names, engine/arch suffixes and version tokens; tidy separators. */ private fun cleanName(raw: String): String? { var s = raw - s = s.replace(Regex("""[\[(][^\])]*[\])]"""), " ") // (…) […] + s = s.replace(Regex("""[\[(][^\])]*[\])]"""), " ") s = VERSION_TOKEN_RE.replace(s, " ") s = EXE_SUFFIX_RE.replace(s, " ") s = s.replace(Regex("""[._]+"""), " ") - // Drop standalone repack-group words. s = s.split(Regex("""[\s-]+""")) .filter { it.isNotBlank() && it.lowercase() !in REPACK_TOKENS } .joinToString(" ") @@ -308,20 +216,16 @@ object GameIdentifier { return s.ifBlank { null } } - // ── tiny text/format helpers (regex-based → no Android/org.json dependency, unit-testable) ── - /** Read a small text file (guards against accidentally slurping a huge file). */ private fun readSmallTextFile(f: File, maxBytes: Long = 1L * 1024 * 1024): String? = try { if (f.isFile && f.length() in 1..maxBytes) f.readText(Charsets.UTF_8) else null } catch (_: Throwable) { null } - /** Value of a Valve KeyValues (.acf/.vdf) `"key" "value"` pair, first match, case-insensitive key. */ private fun vdfValue(text: String, key: String): String? = Regex(""""${Regex.escape(key)}"\s*"([^"]*)"""", RegexOption.IGNORE_CASE).find(text)?.groupValues?.getOrNull(1)?.trim() - /** Value of a JSON `"key": "value"` pair, first match. Sufficient for the flat goggame .info files. */ private fun jsonStringValue(text: String, key: String): String? = Regex(""""${Regex.escape(key)}"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"""").find(text) ?.groupValues?.getOrNull(1)?.replace("\\\"", "\"")?.replace("\\\\", "\\")?.trim() diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GameLaunchLink.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GameLaunchLink.kt index c19709250..27374023c 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GameLaunchLink.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GameLaunchLink.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.frontend -/** External callers can select a game, never a guest path or arbitrary Steam command. */ object GameLaunchLink { private val idPattern = Regex("[1-9][0-9]{0,19}") private val linkPattern = Regex("droiddeck://game/([1-9][0-9]{0,19})") diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GameShortcuts.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GameShortcuts.kt index 5cd221658..f6ea660da 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GameShortcuts.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GameShortcuts.kt @@ -42,7 +42,6 @@ object GameShortcuts { if (!requested) Toast.makeText(activity, R.string.game_shortcut_unsupported, Toast.LENGTH_LONG).show() } - /** Standard shortcut picker result, understood by launchers that import Android shortcuts. */ fun result(activity: Activity, game: Library.SteamGame): Intent = activity.getSystemService(ShortcutManager::class.java).createShortcutResultIntent(info(activity, game)) } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/GamesFolderMigration.kt b/app/src/main/java/com/droiddeck/launcher/frontend/GamesFolderMigration.kt index 56846c807..faf79c153 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/GamesFolderMigration.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/GamesFolderMigration.kt @@ -8,15 +8,6 @@ import com.droiddeck.launcher.stores.StoreInstallRoot import java.io.File import java.util.zip.CRC32 -/** - * Games folders (the roots earlier builds let the user add in Steam settings) become what the - * Games tab's "Add all games in this folder" makes: one entry per game ([AddedExes]). Everything - * a game had is keyed by its own folder and stays as it was - the chosen exe, the shortcut appid, - * the name, Start in, launch options, art choices, a removal. The roots stay bound into the - * session at the same guest paths ([SessionPrefs.gameFolderBinds]), so every exe keeps its path - * inside the session and every Steam shortcut its target; they are no longer Steam libraries. - * Runs whenever saved roots remain (so twice is the same as once) and clears them after. - */ object GamesFolderMigration { private const val TAG = "GamesFolderMigration" @@ -26,7 +17,6 @@ object GamesFolderMigration { if (dirs.isEmpty()) return val roots = legacyRoots(SessionPrefs.gameFolderBinds(context).map { it.first } + dirs) .filter { root -> root.host.path in dirs } - // The binds first: a game's guest path (and with it the shortcut's appid) depends on them. SessionPrefs.setGameFolderBinds(context, SessionPrefs.gameFolderBinds(context) + roots.map { it.host.path to it.guest }) val removed = SessionPrefs.removedAddedGames(context).toSet() val entries = AddedExes.list(context).map { it.folder }.toMutableSet() @@ -50,10 +40,6 @@ object GamesFolderMigration { SessionPrefs.setGamesFoldersMigrated(context) } - /** - * The guest paths earlier builds gave Games folders: /root/Games/<folder name>, with a short - * hash of the host path when two share a name. Kept exactly, so nothing moves in the session. - */ internal fun legacyRoots(paths: List<String>): List<AddedGames.Root> { val hosts = paths.distinct().map { File(it) } val names = hosts.groupingBy { it.name.lowercase() }.eachCount() diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt b/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt index f985b2e5b..1fd87f97d 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/Library.kt @@ -8,42 +8,24 @@ import com.droiddeck.launcher.session.SessionPrefs import com.droiddeck.launcher.session.SecondaryLibrary import java.io.File -/** - * What the front end lists: the Steam client's installed games (from its own appmanifests, both - * libraries) and each emulator's games (files in the ROMs folder, by extension, with a system - * folder as a hint). Read on a worker thread; nothing here is cached beyond one screen refresh. - */ object Library { - /** [gameId] is what steam://rungameid/ takes: the appid for a Steam title, the shortcut id for an added game. */ - /** - * [art] is the portrait capsule; [hero] the wide banner Steam shows above a game's page, when - * the client has cached one. [lastPlayed] is Steam's own "LastPlayed" (unix seconds), 0 = never. - */ class SteamGame( val appId: Int, val name: String, val art: File?, val library: String, val gameId: Long = appId.toLong(), val hero: File? = null, val lastPlayed: Long = 0L, val gameFiles: File? = null, val protonPrefix: File? = null, val icon: File? = null, - /** Where the game came from: [SOURCE_STEAM], a store's id (gog, epic, amazon) or [ADDED] for a folder the user added. */ val source: String = SOURCE_STEAM, - /** The store's own id for a store install; null otherwise. */ val storeId: String? = null, ) { - /** Decimal form used by Steam links and Android shortcuts, including unsigned shortcut ids. */ val gameIdString: String get() = java.lang.Long.toUnsignedString(gameId) - /** Key used by per-game environment and Windows-component profiles. */ val profileKey: String get() = if (library == ADDED) java.lang.Integer.toUnsignedString(appId) else appId.toString() } - /** The [SteamGame.library] of a game added to the library rather than installed by Steam, and the [SteamGame.source] of a plain added folder. */ const val ADDED = "added" - /** The [SteamGame.source] of a title Steam itself installed. */ const val SOURCE_STEAM = "steam" class Rom(val name: String, val hostPath: File, val guestPath: String, val emulatorId: String, val art: File? = null) class Emulator(val id: String, val name: String, val system: String, val program: String, val installed: Boolean, val games: List<Rom>) { - /** The shown name of [system] when it is words rather than a platform name, else 0. */ val systemRes: Int get() = if (id == "retroarch") R.string.content_system_many else 0 - /** The emulator's own icon, bundled (the runtime keeps them as theme SVGs the app cannot draw). */ val iconRes: Int get() = when (id) { "rpcs3" -> R.drawable.emu_rpcs3; "armsx2" -> R.drawable.emu_pcsx2; "dolphin" -> R.drawable.emu_dolphin "duckstation" -> R.drawable.emu_duckstation; "melonds" -> R.drawable.emu_melonds; "cemu" -> R.drawable.emu_cemu @@ -51,34 +33,33 @@ object Library { } } - /** The client's own tools and runtimes live in steamapps beside the games; they are not titles. */ private val NOT_GAMES = setOf( - 858280, // Proton 3.7 - 961940, // Proton 3.16 - 993090, // Lossless Scaling - 1054830, // Proton 4.2 - 1070560, // Steam Linux Runtime 1.0 - 1113280, // Proton 4.11 - 1245040, // Proton 5.0 - 1391110, // Steam Linux Runtime 2.0 - 1420170, // Proton 5.13 - 1493710, // Proton Experimental - 1580130, // Proton 6.3 - 1628350, // Steam Linux Runtime 3.0 - 1887720, // Proton 7 - 2180100, // Proton Hotfix - 228980, // Steamworks Common Redistributables - 2348590, // Proton 8 - 2805730, // Proton 9 - 3029110, // Lepton - 3127680, // FEX - 3658110, // Proton 10 - 4183110, // Steam Linux Runtime 4.0 - 4185400, // Steam Linux Runtime 4.0 for ARM64 - 4427310, // Proton Experimental for ARM64 - 4628710, // Proton 11 / Proton Next - 4628740, // Proton 11 for ARM64 - 4690330, // Legacy Steam Runtime + 858280, + 961940, + 993090, + 1054830, + 1070560, + 1113280, + 1245040, + 1391110, + 1420170, + 1493710, + 1580130, + 1628350, + 1887720, + 2180100, + 228980, + 2348590, + 2805730, + 3029110, + 3127680, + 3658110, + 4183110, + 4185400, + 4427310, + 4628710, + 4628740, + 4690330, ) private val STEAM_CAPSULES = listOf("library_capsule.jpg", "library_600x900.jpg") private val STEAM_HEROES = listOf("library_hero.jpg") @@ -88,7 +69,6 @@ object Library { private val LAST_PLAYED = Regex("^\\s*\"LastPlayed\"\\s*\"(\\d+)\"", RegexOption.MULTILINE) private val INSTALL_DIR = Regex("^\\s*\"installdir\"\\s*\"([^\"]*)\"", RegexOption.MULTILINE) - /** Steam's library roots visible to this launcher: its private default plus the selected library. */ private fun steamLibraries(context: Context): List<Pair<File, String>> { val root = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam") return listOfNotNull( @@ -97,7 +77,6 @@ object Library { ) + GameStorage.gamesFolderLibraries(context).map { it.host to it.label } } - /** Proton keeps each game's prefix below compatdata/<appid>/pfx in a Steam library. */ fun protonPrefix( context: Context, appId: Long, preferredLibrary: File? = null, libraries: List<File> = steamLibraries(context).map { it.first }, @@ -130,14 +109,10 @@ object Library { val text = try { manifest.readText() } catch (e: Exception) { if (strictRead) throw e; return@forEach } val name = NAME.find(text)?.groupValues?.get(1)?.trim().orEmpty() val flags = STATE.find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0 - // StateFlags 4 = fully installed; anything else is downloading, updating or broken. if (name.isEmpty() || flags and 4 == 0) return@forEach val art = steamCacheImage(cache, appId, STEAM_CAPSULES) val hero = steamCacheImage(cache, appId, STEAM_HEROES) val lastPlayed = LAST_PLAYED.find(text)?.groupValues?.get(1)?.toLongOrNull() ?: 0L - // App manifests are Valve KeyValues (VDF) files. installdir is one folder - // below steamapps/common; only expose it when the directory exists and the - // manifest value cannot escape that directory. 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) } @@ -153,14 +128,11 @@ object Library { return out.values.toList() } - /** The same installed-game inventory used for links, shortcuts and file exports. */ - /** [added]: the added games when the caller has just scanned them, so they are not scanned again. */ 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 } } - /** One added game as the Games tab lists it; [roots] the Steam libraries, for its Proton prefix. */ fun addedGame(context: Context, g: AddedGames.Game, roots: List<File> = steamLibraries(context).map { it.first }): SteamGame = AddedGameArt.resolve(context, g).let { art -> SteamGame( @@ -172,7 +144,6 @@ object Library { ) } - /** Steam stores current library art inside hash-named folders under the app's cache dir. */ private fun steamCacheImage(cache: File, appId: Int, names: List<String>): File? { val appDir = File(cache, appId.toString()) val dirs = listOf(appDir) + appDir.listFiles() @@ -182,12 +153,9 @@ object Library { .firstOrNull { it.isFile && it.length() > 0L } } - /** [atPanel]: frames cheap enough to draw at the panel's own size (see [drawsAtPanel]). */ private class Spec(val id: String, val name: String, val system: String, val program: String, val folders: List<String>, val exts: Set<String>, val atPanel: Boolean = false) private val specs = listOf( Spec("rpcs3", "RPCS3", "PS3", "/opt/appimages/rpcs3.AppImage", listOf("ps3"), setOf("iso")), - // PS2: ARMSX2, the PCSX2 fork with ARM64 recompilers. Upstream PCSX2 interprets the PS2's - // CPUs on ARM64 (NFS Underground 2: 17 fps against ARMSX2's full 60), so it is not offered. Spec("armsx2", "ARMSX2", "PS2", "/opt/appimages/armsx2.AppImage", listOf("ps2"), setOf("iso", "chd", "cso", "gz")), Spec("dolphin", "Dolphin", "GameCube / Wii", "/opt/appimages/dolphin.AppImage", listOf("gc", "gamecube", "wii"), setOf("iso", "rvz", "gcz", "wbfs", "ciso")), Spec("duckstation", "DuckStation", "PS1", "/opt/appimages/duckstation.AppImage", listOf("ps1", "psx"), setOf("cue", "chd", "pbp", "iso", "bin", "img", "ecm", "m3u")), @@ -196,73 +164,46 @@ object Library { Spec("ppsspp", "PPSSPP", "PSP", "/usr/bin/PPSSPPSDL", listOf("psp"), setOf("iso", "cso", "pbp", "chd")), Spec("retroarch", "RetroArch", "many systems", "/usr/bin/retroarch", emptyList(), emptySet()), ) - /** - * A dump's file name as a title: its tags - (USA), (En,Fr,Es,Pt), [!], (v1.01) - identify the - * file and are dropped for reading ("Tomb Raider (USA) (En,Fr,Es,Pt)" -> "Tomb Raider"). The - * file keeps them, and the cover lookup still uses them. - */ private fun displayTitle(name: String): String = name.replace(Regex("\\s*[(\\[][^)\\]]*[)\\]]"), "").trim().ifEmpty { name } - /** Every system folder name the specs claim, so a loose-file scan of the root skips them. */ private val systemFolders: Set<String> by lazy { specs.flatMap { it.folders }.toSet() } private val installedIds = mapOf( "rpcs3" to "rpcs3", "armsx2" to "armsx2", "dolphin" to "dolphin", "duckstation" to "duckstation", "melonds" to "melonds", "cemu" to "cemu", "ppsspp" to "emulators", "retroarch" to "emulators", ) - /** The emulator's name for a program path from the rail ("ARMSX2"), or null. */ fun nameForProgram(program: String?): String? = 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 specs.firstOrNull { it.program == program }?.name - /** Added apps' names by Flatpak id or directory, as launched: the session only knows that. */ val flatpakNames = java.util.concurrent.ConcurrentHashMap<String, String>() - /** - * Whether a program from the rail runs at the panel's own resolution rather than the session's - * 720p default. melonDS draws two 256x192 screens on the CPU: the panel's size costs it - * nothing, and 720p scaled up to the panel blurs the sharp pixels its screen layout is set - * up for (droiddeck-pad-defaults). - */ fun drawsAtPanel(program: String?): Boolean = specs.any { it.program == program && it.atPanel } - /** Desktop catalog package that supplies this emulator. */ fun packageId(emulatorId: String): String? = installedIds[emulatorId] - /** Every emulator the app knows, installed or not, with the games its system folder holds. */ fun emulators(context: Context, installedPackage: (String) -> Boolean): List<Emulator> { val romsRoot = SessionPrefs.romsDir(context).takeIf { it.isNotEmpty() }?.let(::File)?.takeIf { it.isDirectory } return specs.map { spec -> val games = ArrayList<Rom>() if (romsRoot != null && spec.exts.isNotEmpty()) { - // The system's folder(s), matched without regard to case, and up to three folders - // inside them - a dump usually comes as a folder named for the game with the image - // inside it, and people sort those into folders of their own (ps2/games/<game>/). - // The root itself only one deep, for a file left loose there. val systemDirs = romsRoot.listFiles { f -> f.isDirectory && f.name.lowercase() in spec.folders }.orEmpty().toList() val dirs = LinkedHashSet<File>() - // A BIOS folder holds the console's firmware, not games (psx/bios/SCPH1001.BIN). for (top in systemDirs) top.walkTopDown().maxDepth(3).onEnter { it.name.lowercase() !in firmwareFolders } .filter { it.isDirectory }.forEach { dirs.add(it) } dirs.add(romsRoot) - // ...and not into another system's folder: a PS3 .iso in ps3/ is not a PS2 game. romsRoot.listFiles { f -> f.isDirectory && f.name.lowercase() !in systemFolders }?.forEach { dirs.add(it) } for (dir in dirs) { - // A PS3 disc dump is a folder with PS3_GAME in it; RPCS3 boots the folder. if (spec.id == "rpcs3" && File(dir, "PS3_GAME").isDirectory) { val rel = dir.relativeTo(romsRoot).path games.add(Rom(displayTitle(dir.name), dir, "/root/ROMs/$rel", spec.id, - // The dump carries its own art, as an installed package does. art = File(dir, "PS3_GAME/ICON0.PNG").takeIf { it.isFile })) continue } val files = dir.listFiles()?.sortedBy { it.name.lowercase() }.orEmpty() - // A disc sheet (.cue, .gdi) names its track files and a playlist (.m3u) its - // discs: the sheet is the game, what it names is part of it. A .bin beside a - // .cue of the same name is a track even when the sheet can't be read. val parts = files.filter { it.isFile && it.extension.lowercase() in sheetExts }.flatMap(::sheetParts).toSet() files.forEach { f -> val ext = f.extension.lowercase() @@ -275,7 +216,6 @@ object Library { } } if (spec.id == "rpcs3") games.addAll(rpcs3Installed(context)) - // A cover found for the game before (CoverArt) where it has no art of its own. val withArt = games.map { g -> if (g.art != null) g else CoverArt.cached(context, g)?.let { Rom(g.name, g.hostPath, g.guestPath, g.emulatorId, it) } ?: g } Emulator(spec.id, spec.name, spec.system, spec.program, installedPackage(installedIds.getValue(spec.id)), withArt) } @@ -284,27 +224,19 @@ object Library { private val firmwareFolders = setOf("bios", "firmware") private val sheetExts = setOf("cue", "gdi", "m3u") - /** The file names (lower case) a .cue, .gdi or .m3u refers to, from its own folder. */ private fun sheetParts(sheet: File): List<String> = runCatching { if (sheet.length() > 64 * 1024) return emptyList() sheet.readLines().mapNotNull { line -> val t = line.trim() when (sheet.extension.lowercase()) { - // FILE "Tekken 3 (USA) (Track 1).bin" BINARY "cue" -> Regex("^FILE\\s+\"([^\"]+)\"", RegexOption.IGNORE_CASE).find(t)?.groupValues?.get(1) ?: Regex("^FILE\\s+(\\S+)", RegexOption.IGNORE_CASE).find(t)?.groupValues?.get(1) - // 1 0 4 2352 "track01.bin" 0 / 1 0 4 2352 track01.bin 0 "gdi" -> Regex("\"([^\"]+)\"").find(t)?.groupValues?.get(1) ?: t.split(Regex("\\s+")).getOrNull(4) else -> t.takeIf { it.isNotEmpty() && !it.startsWith("#") } } }.map { File(it.replace('\\', '/')).name.lowercase() } }.getOrDefault(emptyList()) - /** - * What RPCS3 has installed on its own HDD - packages (PSN games) land in dev_hdd0/game/<ID> - * with a PARAM.SFO for the title and an EBOOT to boot - as RPCS3's own file list would show - * them. Not in the ROMs folder, so listed from RPCS3's home in the runtime. - */ private fun rpcs3Installed(context: Context): List<Rom> { val hdd = File(LinuxRuntime.rootDir(context), "root/.config/rpcs3/dev_hdd0/game") return hdd.listFiles { f -> f.isDirectory }?.sortedBy { it.name }?.mapNotNull { dir -> @@ -312,7 +244,6 @@ object Library { val sfo = File(dir, "PARAM.SFO") if (!eboot.isFile || !sfo.isFile) return@mapNotNull null val fields = readSfo(sfo) - // Only games: patches, DLC and save data live here too, with their own categories. if (fields["CATEGORY"]?.let { it == "HG" || it == "DG" || it == "GD" } == false) return@mapNotNull null val title = fields["TITLE"]?.trim()?.takeIf { it.isNotEmpty() } ?: dir.name Rom(title, eboot, "/root/.config/rpcs3/dev_hdd0/game/${dir.name}/USRDIR/EBOOT.BIN", "rpcs3", @@ -320,7 +251,6 @@ object Library { } ?: emptyList() } - /** The string fields of a PARAM.SFO (the PSP/PS3 metadata file): a small binary table. */ private fun readSfo(file: File): Map<String, String> { val out = HashMap<String, String>() try { @@ -337,7 +267,7 @@ object Library { var keyEnd = keyStart while (keyEnd < b.size && b[keyEnd] != 0.toByte()) keyEnd++ val key = String(b, keyStart, keyEnd - keyStart, Charsets.US_ASCII) - if (fmt == 0x0204 || fmt == 0x0004) { // utf8 string (null-terminated or not) + if (fmt == 0x0204 || fmt == 0x0004) { val start = data + dataOff val end = minOf(b.size, start + len) var stop = start @@ -346,38 +276,21 @@ object Library { } } } catch (e: Exception) { - // unreadable metadata: the folder name stands in } return out } - /** How the emulator is told which game to boot, on its command line. */ fun launchArgs(emulatorId: String, guestPath: String): List<String> = when (emulatorId) { "rpcs3" -> listOf("--no-gui", guestPath) - // Straight into the game in its controller-driven full-screen UI (first-time setup there - // too), and gone when the game is quit from its pause menu (guide button): -batch. "armsx2" -> listOf("-batch", "-bigpicture", "-fullscreen", "--", guestPath) - // DuckStation shares PCSX2's flags (src/duckstation-qt/qthost.cpp): the same launch and exit. "duckstation" -> listOf("-batch", "-bigpicture", "-fullscreen", "--", guestPath) - // Dolphin: full screen, drawn inside its own main window, without its "stop the emulation?" - // question; no warning boxes either, which wait for a click a controller cannot give - // (they still go to Dolphin's log). The guide button is Dolphin's Toggle Fullscreen hotkey - // (droiddeck-pad-defaults), which shows Dolphin's window and its settings - so no -b, which - // hides that window; closing Dolphin ends the session. Under gamescope Dolphin does not - // always see its window as focused, and by default both its hotkeys and the game's - // controller then stop: HotkeysRequireFocus off, BackgroundInput on. -C sets a Dolphin.ini - // value for this run only, so Dolphin started from the desktop keeps its own settings. "dolphin" -> listOf( "-C", "Dolphin.Display.Fullscreen=True", "-C", "Dolphin.Display.RenderToMain=True", "-C", "Dolphin.Interface.ConfirmStop=False", "-C", "Dolphin.Interface.UsePanicHandlers=False", "-C", "Dolphin.General.HotkeysRequireFocus=False", "-C", "Dolphin.Input.BackgroundInput=True", "-e", guestPath, ) - // Full screen (LaunchSettings.cpp -f); the settings seed keeps the Getting Started - // dialog away (droiddeck-pad-defaults). "cemu" -> listOf("-f", "-g", guestPath) - // Full screen (CLI.cpp --fullscreen); the guide button leaves it for melonDS's menus and - // comes back (HK_FullscreenToggle, droiddeck-pad-defaults). "melonds" -> listOf("-f", guestPath) else -> listOf(guestPath) } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/LibraryCache.kt b/app/src/main/java/com/droiddeck/launcher/frontend/LibraryCache.kt index cd946724a..c8e1cbabf 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/LibraryCache.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/LibraryCache.kt @@ -6,11 +6,6 @@ import org.json.JSONArray import org.json.JSONObject import java.io.File -/** - * The last game list the front end showed, kept in the app's files so the next start draws the - * Steam wall on its first frame. Building the list reads every manifest and walks the added-games - * folders, which can take a second or more; the fresh list replaces this one when it is ready. - */ object LibraryCache { private const val TAG = "LibraryCache" private fun file(context: Context) = File(context.filesDir, "library-cache.json") diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/PeVersionInfo.kt b/app/src/main/java/com/droiddeck/launcher/frontend/PeVersionInfo.kt index 77b61deca..99ead7d6f 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/PeVersionInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/PeVersionInfo.kt @@ -3,27 +3,11 @@ package com.droiddeck.launcher.frontend import java.io.File import java.io.RandomAccessFile -/** - * Reads the StringFileInfo strings (ProductName, FileDescription, …) out of the - * VS_VERSION_INFO resource embedded in a Windows PE executable (.exe). - * - * Walks the PE resource directory to the first RT_VERSION resource — the same - * navigation [com.droiddeck.launcher.files.PeIconExtractor] uses for icons — then parses the alignment-heavy - * VS_VERSIONINFO → StringFileInfo → StringTable → String tree by hand. - * - * Everything is best-effort: any malformed / oversized / unsupported input returns - * null (or an empty map). No allocation beyond the single resource blob. - */ object PeVersionInfo { private const val RT_VERSION = 16 - private const val MAX_RES_SECTION = 64 * 1024 * 1024 // don't slurp absurd resource sections + private const val MAX_RES_SECTION = 64 * 1024 * 1024 - /** - * Returns the StringFileInfo key→value map (e.g. `"ProductName" -> "Palworld"`), - * or null if the file has no readable version resource. Keys are the raw - * VS_VERSIONINFO names; look up [productName]/[fileDescription] for the common ones. - */ fun read(file: File): Map<String, String>? = try { RandomAccessFile(file, "r").use { raf -> val blob = versionResourceBytes(raf) ?: return@use null @@ -33,13 +17,6 @@ object PeVersionInfo { null } - /** - * Best display name from a version map. ProductName is usually the cleanest title, but is - * sometimes an internal abbreviation (e.g. God of War ships ProductName "GoW" while - * FileDescription is "God of War"). When ProductName is a single spaceless token and - * FileDescription is a normal multi-word title, prefer FileDescription. Trimmed; null if - * neither is meaningful. - */ fun bestName(map: Map<String, String>?): String? { if (map == null) return null val product = map["ProductName"]?.trim()?.takeIf { it.isNotEmpty() } @@ -52,15 +29,14 @@ object PeVersionInfo { return product ?: desc } - // ── locate the RT_VERSION resource blob (mirrors PeIconExtractor's navigation) ── private fun versionResourceBytes(raf: RandomAccessFile): ByteArray? { val dos = read(raf, 0, 0x40) ?: return null - if (u8(dos, 0) != 0x4D || u8(dos, 1) != 0x5A) return null // "MZ" + if (u8(dos, 0) != 0x4D || u8(dos, 1) != 0x5A) return null val peOff = u32(dos, 0x3C).toInt() val coff = read(raf, peOff.toLong(), 24) ?: return null - if (u8(coff, 0) != 0x50 || u8(coff, 1) != 0x45 || u8(coff, 2) != 0 || u8(coff, 3) != 0) return null // "PE\0\0" + if (u8(coff, 0) != 0x50 || u8(coff, 1) != 0x45 || u8(coff, 2) != 0 || u8(coff, 3) != 0) return null val numSections = u16(coff, 6) val sizeOpt = u16(coff, 20) if (numSections <= 0 || numSections > 96 || sizeOpt <= 0) return null @@ -68,8 +44,8 @@ object PeVersionInfo { val optOff = peOff + 24L val opt = read(raf, optOff, sizeOpt) ?: return null val magic = u16(opt, 0) - val ddOff = if (magic == 0x20B) 112 else 96 // PE32+ vs PE32 data-directory offset - val resDirEntry = ddOff + 2 * 8 // data directory index 2 = resource table + val ddOff = if (magic == 0x20B) 112 else 96 + val resDirEntry = ddOff + 2 * 8 if (resDirEntry + 8 > opt.size) return null val resRVA = u32(opt, resDirEntry).toInt() val resSize = u32(opt, resDirEntry + 4).toInt() @@ -105,7 +81,6 @@ object PeVersionInfo { return dataEntryBytes(res, verData, secVA) } - /** Resolve the sub-directory under [dirIdx] whose id matches [wantId]; null if absent or not a directory. */ private fun findIdSubdir(res: ByteArray, dirIdx: Int, wantId: Int, resDirStart: Int): Int? { if (dirIdx + 16 > res.size) return null val total = u16(res, dirIdx + 12) + u16(res, dirIdx + 14) @@ -123,7 +98,6 @@ object PeVersionInfo { return null } - /** Descend the first entry of each level until an IMAGE_RESOURCE_DATA_ENTRY; returns its index. */ private fun firstDataEntry(res: ByteArray, dirIdx: Int, resDirStart: Int): Int? { if (dirIdx + 16 > res.size) return null if (u16(res, dirIdx + 12) + u16(res, dirIdx + 14) == 0) return null @@ -137,7 +111,6 @@ object PeVersionInfo { } } - /** Read the bytes an IMAGE_RESOURCE_DATA_ENTRY points at (its OffsetToData is an RVA). */ private fun dataEntryBytes(res: ByteArray, entryIdx: Int, secVA: Int): ByteArray? { if (entryIdx < 0 || entryIdx + 8 > res.size) return null val rva = u32(res, entryIdx).toInt() @@ -147,17 +120,11 @@ object PeVersionInfo { return res.copyOfRange(start, start + size) } - // ── VS_VERSIONINFO tree parse ── - // - // Every node is: WORD wLength, WORD wValueLength, WORD wType, WCHAR szKey[], - // then (32-bit aligned) an optional Value, then (32-bit aligned) child nodes. - // We only care about: VS_VERSIONINFO → StringFileInfo → StringTable → String. private fun parseStringFileInfo(b: ByteArray): Map<String, String> { val out = LinkedHashMap<String, String>() if (b.size < 6) return out val rootLen = minOf(u16(b, 0), b.size) - // VS_VERSION_INFO header: skip szKey "VS_VERSION_INFO" then the FIXEDFILEINFO value. val rootValLen = u16(b, 2) var pos = align4(6 + (utf16zLen(b, 6) + 1) * 2) pos = align4(pos + rootValLen) @@ -173,7 +140,6 @@ object PeVersionInfo { return out } - /** Iterate the StringTable children inside a StringFileInfo block [start, end). */ private fun parseTables(b: ByteArray, start: Int, end: Int, out: MutableMap<String, String>) { var p = align4(start + 6 + (utf16zLen(b, start + 6) + 1) * 2) while (p + 6 <= end) { @@ -185,7 +151,6 @@ object PeVersionInfo { } } - /** Iterate the String children inside a StringTable block [start, end). */ private fun parseStrings(b: ByteArray, start: Int, end: Int, out: MutableMap<String, String>) { var q = align4(start + 6 + (utf16zLen(b, start + 6) + 1) * 2) while (q + 6 <= end) { @@ -202,11 +167,9 @@ object PeVersionInfo { } } - // ── little-endian / UTF-16 readers ── private fun align4(x: Int): Int = (x + 3) and 3.inv() - /** Number of UTF-16 code units before the null terminator starting at [off]. */ private fun utf16zLen(b: ByteArray, off: Int): Int { var i = off while (i + 1 < b.size) { @@ -216,7 +179,6 @@ object PeVersionInfo { return (i - off) / 2 } - /** Read a null-terminated UTF-16LE string starting at [off], not reading past [limit]. */ private fun utf16z(b: ByteArray, off: Int, limit: Int = b.size): String { val sb = StringBuilder() var i = off diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/PrefixInstalledDetector.kt b/app/src/main/java/com/droiddeck/launcher/frontend/PrefixInstalledDetector.kt index 7f5ca5b0b..91625338c 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/PrefixInstalledDetector.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/PrefixInstalledDetector.kt @@ -3,18 +3,9 @@ package com.droiddeck.launcher.frontend import org.json.JSONObject import java.io.File -/** - * Which catalog components a game's prefix already has from elsewhere - a real installer, the game, - * the user - so the list can say so instead of recommending them again. Bannerlator's detector, - * on DroidDeck's compatdata/<id>: only markers absent from a fresh Proton prefix count (OpenAL32, - * Mono, PhysX, Games for Windows Live); msvcp140, d3dx9 and the like ship with Proton and cannot - * tell. Files DroidDeck put there itself (droiddeck-wincomponents' state) are not counted. - * Pure filesystem reads; never throws. - */ object PrefixInstalledDetector { private const val STATE = ".droiddeck-wincomponents.json" - /** Detector names (oalinst, mono, physx, XLiveRedist) present in [compat]/pfx. */ fun detect(compat: File?): Set<String> = try { val pfx = compat?.let { File(it, "pfx") } if (pfx == null || !pfx.isDirectory) emptySet() else { @@ -38,7 +29,6 @@ object PrefixInstalledDetector { emptySet() } - /** Windows names ignore case; the prefix's folder may spell them either way. */ private fun fileIgnoringCase(pfx: File, rel: String): Boolean { val target = File(pfx, rel) return target.parentFile?.listFiles()?.any { it.isFile && it.name.equals(target.name, ignoreCase = true) } == true diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SessionInstalls.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SessionInstalls.kt index 5291f80fc..3c32bcb06 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SessionInstalls.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SessionInstalls.kt @@ -7,17 +7,10 @@ import com.droiddeck.launcher.session.GameStorage import com.droiddeck.launcher.session.SessionState import java.io.File -/** - * A Steam game whose download finishes during a session, noticed by its appmanifest (a new one, - * or StateFlags turning to 4, fully installed) with a light look every five seconds while the - * session runs, and handed to [AutoComponents] as a single job that may run in the session, so its - * Windows components are in place when the user plays it right after the download. - */ object SessionInstalls { private const val TAG = "SessionInstalls" private const val POLL_MS = 5_000L - /** One appmanifest as read: the app, its name and folder, whether it is fully installed. */ class Manifest(val appId: Int, val name: String, val installDir: String, val installed: Boolean, val steamapps: File) private val APPID = Regex("\"appid\"\\s*\"(\\d+)\"", RegexOption.IGNORE_CASE) @@ -32,16 +25,11 @@ object SessionInstalls { STATE.find(text)?.groupValues?.get(1)?.toIntOrNull() == 4, file.parentFile!!) }.getOrNull() - /** The libraries the session's client installs to, as host folders. */ 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 } - /** - * Keeps what the libraries hold between looks: a manifest re-read only when its time changed. - * [poll] returns the apps that became fully installed since the last look (none on the first). - */ internal class Watch(private val dirs: () -> List<File>) { private val seen = HashMap<String, Pair<Long, Manifest?>>() private var first = true @@ -60,22 +48,16 @@ object SessionInstalls { } } - /** A finished install as the Games tab would list it. */ internal fun gameOf(m: Manifest): Library.SteamGame = Library.SteamGame(m.appId, m.name.ifEmpty { m.appId.toString() }, null, m.steamapps.parentFile?.path ?: "steam", gameFiles = File(m.steamapps, "common/${m.installDir}"), source = Library.SOURCE_STEAM) - /** - * Looks every five seconds while the session runs, on a thread of its own at background - * priority; each finished install becomes one job for its Windows components. - */ fun watch(context: Context) { val app = context.applicationContext Thread({ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND) val watch = Watch { steamappsDirs(app) } try { - // The session is still starting when this is called: up to two minutes for it. var waited = 0L while (!SessionState.running && waited < 120_000L) { Thread.sleep(POLL_MS); waited += POLL_MS } watch.poll() diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SteamAppInfo.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SteamAppInfo.kt index 16a37fc0a..e847d8060 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SteamAppInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamAppInfo.kt @@ -12,25 +12,16 @@ import java.io.RandomAccessFile import java.nio.ByteBuffer import java.nio.ByteOrder -/** - * Steam's own data for an app, from the client in the runtime: its app-info cache - * (Steam/appcache/appinfo.vdf, the PICS data the client keeps for every app it has seen). What is - * read is which Steamworks Shared depots (the VC++, DirectX... redistributables of app 228980) the - * app's depots point at, and its name. Kept per appid in the app's files, so it works offline; an - * app the client has not seen is looked for again after a week. - */ object SteamAppInfo { private const val TAG = "SteamAppInfo" private const val CACHE = "steam-appinfo.json" private const val RETRY_AFTER_MS = 7L * 24 * 3600 * 1000 - /** The Steamworks Shared app the redistributable depots belong to. */ const val SHARED_APP = "228980" class Info(val name: String, val sharedDepots: List<String>) fun appinfoFile(context: Context) = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/appcache/appinfo.vdf") - /** The shared depots for [appId]: from the cache, else the client's app-info file; null when unknown. */ @Synchronized fun info(context: Context, appId: Int, appinfo: File = appinfoFile(context)): Info? { val cacheFile = AtomicFile(File(context.filesDir, CACHE)) @@ -40,13 +31,10 @@ object SteamAppInfo { val known = cache.optJSONObject(appId.toString()) known?.let { o -> if (!o.optBoolean("miss")) return Info(o.optString("name"), o.optJSONArray("depots").strings()) - // A miss is read again when the client has written its cache since (the user may have - // looked at the app in Steam), else a week later. if (now - o.optLong("at") < RETRY_AFTER_MS && o.optLong("mtime") == mtime) return null } val read = runCatching { read(appinfo, appId) }.onFailure { Log.w(TAG, "appinfo: ${it.javaClass.simpleName}") }.getOrNull() val entry = JSONObject().put("at", now).put("mtime", mtime) - // When Steam was last asked anonymously ([lookup]), kept across re-reads of the file. known?.optLong("picsAt")?.takeIf { it > 0 }?.let { entry.put("picsAt", it) } if (read == null) entry.put("miss", true) else entry.put("name", read.name).put("depots", JSONArray(read.sharedDepots)) cache.put(appId.toString(), entry) @@ -55,14 +43,9 @@ object SteamAppInfo { return read } - /** Steam asked anonymously ([SteamPics]); a test stands in for it. */ @androidx.annotation.VisibleForTesting internal var pics: (List<Int>) -> Map<Int, Info?>? = { SteamPics.lookup(it) } - /** - * Of [appIds], those neither the client's cache (appinfo.vdf, read first: free) nor an earlier - * answer has, and that Steam was not asked about anonymously in the past week. - */ fun needLookup(context: Context, appIds: List<Int>): List<Int> { val wanted = appIds.distinct().filter { it > 0 && info(context, it) == null } if (wanted.isEmpty()) return emptyList() @@ -71,11 +54,6 @@ object SteamAppInfo { return wanted.filter { now - (cache.optJSONObject(it.toString())?.optLong("picsAt") ?: 0L) >= RETRY_AFTER_MS } } - /** - * [appIds] the client never saw, asked of Steam anonymously in one connection ([SteamPics], in - * batches) and every answer kept like the client's: an app's depots and name, or a miss tried - * again in a week. False when nothing needed asking or Steam could not be reached. Blocking. - */ @Synchronized fun lookup(context: Context, appIds: List<Int>): Boolean { val need = needLookup(context, appIds) @@ -96,7 +74,6 @@ object SteamAppInfo { return true } - /** What is kept for [appId]: "hit", "miss" or "none" (part of a game's list fingerprint). */ fun state(context: Context, appId: Int): String = cache(context).optJSONObject(appId.toString())?.let { if (it.optBoolean("miss")) "miss" else "hit" } ?: "none" private fun cache(context: Context): JSONObject = @@ -104,10 +81,6 @@ object SteamAppInfo { private fun JSONArray?.strings(): List<String> = if (this == null) emptyList() else (0 until length()).map { optString(it) } - /** - * [appId]'s entry in an appinfo.vdf (versions 27, 28 and 29, the last with its keys in a string - * table), or null when the file does not have it. - */ internal fun read(file: File, appId: Int): Info? { if (!file.isFile) return null RandomAccessFile(file, "r").use { raf -> @@ -181,7 +154,6 @@ object SteamAppInfo { return String(bytes, Charsets.UTF_8) } - /** One binary key-value map, to its end marker; keys by name, or by index into [strings]. */ private fun readMap(b: ByteBuffer, strings: List<String>?): Map<String, Any> { val out = LinkedHashMap<String, Any>() while (b.hasRemaining()) { diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SteamGridDb.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SteamGridDb.kt index 630fb64cd..dc35dafb2 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SteamGridDb.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamGridDb.kt @@ -11,15 +11,6 @@ import java.net.HttpURLConnection import java.net.URL import java.net.URLEncoder -/** - * SteamGridDB, the art source after Steam's store: a game by its Steam appid, else by a search - * whose result must carry the same name, then its best-rated grids, heroes, logos and icons. - * - * The API key: the user's own (Steam settings › Games), sealed with [CredentialCipher] in the - * app's files, else the one the build carries (`BuildConfig.SGDB_API_KEY`, from a CI secret), else - * none, and then this step is skipped. The key travels only in the Authorization header and is - * never logged. - */ object SteamGridDb { private const val TAG = "SteamGridDb" private const val API = "https://www.steamgriddb.com/api/v2" @@ -31,17 +22,14 @@ object SteamGridDb { private fun keyFile(context: Context) = File(context.filesDir, "sgdb/key.json") - /** The key the build carries, or "" (a build without the secret, a fork's PR build). */ fun builtInKey(): String = runCatching { Class.forName("com.droiddeck.launcher.BuildConfig").getField("SGDB_API_KEY").get(null) as? String }.getOrNull().orEmpty() - /** The user's key wins, then the build's; "" when there is neither. */ fun pick(user: String, builtIn: String): String = user.trim().ifEmpty { builtIn.trim() } fun key(context: Context): String = pick(userKey(context), builtInKey()) - /** The key the user entered, or "". */ @Synchronized fun userKey(context: Context): String { val f = keyFile(context) @@ -56,7 +44,6 @@ object SteamGridDb { } } - /** Saves the user's key sealed (plain only where the Keystore does not work); "" clears it. */ @Synchronized fun setUserKey(context: Context, key: String) { val f = keyFile(context) @@ -75,7 +62,6 @@ object SteamGridDb { if (!tmp.renameTo(f)) { f.delete(); tmp.renameTo(f) } } - /** The SteamGridDB game id for [steamAppId], else for a search on [name] that returns the same name; null when neither. */ fun gameId(key: String, steamAppId: Int?, name: String): Int? { if (steamAppId != null) { json(key, "$API/games/steam/$steamAppId")?.optJSONObject("data")?.optInt("id")?.takeIf { it > 0 }?.let { return it } @@ -85,16 +71,11 @@ object SteamGridDb { return match(name, (0 until results.length()).map { results.getJSONObject(it) }.map { it.optInt("id") to it.optString("name") }) } - /** The id of the first result whose name matches [name] ([AddedGameArt.nameMatch]); no blind first pick. */ internal fun match(name: String, results: List<Pair<Int, String>>): Int? = results.map { (id, got) -> id to AddedGameArt.nameMatch(name, got) } .filter { it.second > 0 && it.first > 0 } .maxByOrNull { it.second }?.first - /** - * Fetches what is missing into [cache] under the names Steam's art uses there (p.jpg, - * header.jpg, hero.jpg, logo.png) and the icon as icon-sgdb.png. True when anything arrived. - */ fun download(key: String, id: Int, cache: File): Boolean { var got = false for ((path, local) in listOf( @@ -116,7 +97,6 @@ object SteamGridDb { return got } - /** The best-scored [limit] images for [slot] of game [id], as (thumbnail, full image) URLs. */ fun images(key: String, id: Int, slot: AddedGameArt.Slot, limit: Int): List<Pair<String, String>> { val path = when (slot) { AddedGameArt.Slot.COVER -> "grids/game/$id?dimensions=600x900&$QUERY" @@ -128,20 +108,17 @@ object SteamGridDb { return ranked(items, pngOnly = slot == AddedGameArt.Slot.LOGO || slot == AddedGameArt.Slot.ICON).take(limit) } - /** [items] best-scored first, as (thumbnail, full image) URLs; for a .png slot only PNGs. */ internal fun ranked(items: JSONArray, pngOnly: Boolean): List<Pair<String, String>> = (0 until items.length()).map { items.getJSONObject(it) } .filter { it.optString("url").startsWith("https://") && (!pngOnly || it.optString("mime") == "image/png" || it.optString("url").endsWith(".png", true)) } .sortedByDescending { it.optInt("score", 0) } .map { (it.optString("thumb").takeIf { t -> t.startsWith("https://") } ?: it.optString("url")) to it.optString("url") } - /** The highest-scored image's URL; for a .png slot only a PNG. */ internal fun best(items: JSONArray, pngOnly: Boolean): String? = (0 until items.length()).map { items.getJSONObject(it) } .filter { it.optString("url").startsWith("https://") && (!pngOnly || it.optString("mime") == "image/png" || it.optString("url").endsWith(".png", true)) } .maxByOrNull { it.optInt("score", 0) }?.optString("url") - /** A GET with the key in its Authorization header; the body as JSON, or null. Logs the path only. */ private fun json(key: String, url: String): JSONObject? { val c = URL(url).openConnection() as HttpURLConnection c.connectTimeout = 10_000 diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SteamMatch.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SteamMatch.kt index 177888a46..c0c83676d 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SteamMatch.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamMatch.kt @@ -5,20 +5,9 @@ import android.util.AtomicFile import org.json.JSONObject import java.io.File -/** - * Which Steam app a game added in DroidDeck is, kept once per game (by its folder) and used by - * everything that asks: its art ([AddedGameArt]) and its Windows components ([WinCompSources]), - * so the two never disagree. - * - * [Certainty.FILES]: the game's own files name the appid (steam_appid.txt, also under - * steam_settings/ or a parent folder; an emulator ini; Steam's appmanifest) - certain. - * [Certainty.NAME]: Steam's store search returned a game of the same name - a guess. - * [Certainty.NONE]: neither; a miss is tried again after a week. - */ object SteamMatch { enum class Certainty { FILES, NAME, NONE } - /** [source]: what gave the appid (GameIdentifier's source, or "store search"). */ class Match(val appId: Int?, val certainty: Certainty, val source: String, val checkedAt: Long) private const val FILE = "steam-matches.json" @@ -50,12 +39,6 @@ object SteamMatch { try { out.write(json.toString().toByteArray()); atomic.finishWrite(out) } catch (e: Exception) { atomic.failWrite(out) } } - /** - * The match for the game in [key] (its folder), [exe] its exe and [name] its title: its files - * first, always read again; else the one kept (a name match stays, a miss for a week); else - * [legacy], an appid an earlier build's art lookup kept; else [search] on the name, when given. - * Blocking ([search] asks the network). - */ fun resolve(context: Context, key: String, exe: File, name: String, legacy: File? = null, search: ((String) -> Int?)? = null): Match { val now = System.currentTimeMillis() val identity = runCatching { GameIdentifier.identify(exe) }.getOrNull() diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SteamPics.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SteamPics.kt index ae0bf12f2..2dd132c51 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SteamPics.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamPics.kt @@ -18,18 +18,9 @@ import java.util.zip.GZIPInputStream import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory -/** - * Steam's own app info (PICS) for apps the client in the runtime has never seen, asked of Steam - * anonymously: a connection to one of Steam's connection managers (the list from Steam's public - * ISteamDirectory/GetCMListForConnect, the WebSocket transport Steam's web clients use), an - * anonymous logon - no account, nothing of the user's Steam sign-in - a request for the apps' - * access tokens and then their product info, the app KeyValues read for their Steamworks Shared - * depots and name, and the connection closed. What steamcmd's anonymous `app_info_print` sees. - */ object SteamPics { private const val TAG = "SteamPics" private const val CM_LIST = "https://api.steampowered.com/ISteamDirectory/GetCMListForConnect/v1/?cellid=0&format=json" - /** At most this many apps per request. */ const val BATCH = 25 private const val PROTO = 0x80000000.toInt() private const val EMSG_MULTI = 1 @@ -41,13 +32,8 @@ object SteamPics { private const val EMSG_INFO_REQUEST = 8903 private const val EMSG_INFO_RESPONSE = 8904 private const val PROTOCOL_VERSION = 65580 - /** An anonymous user's SteamID: universe Public, account type AnonUser, instance 0. */ private const val ANON_STEAMID = (1L shl 56) or (10L shl 52) - /** - * [appIds]' info, one connection for all of them in batches of [BATCH]: each known app's - * info, null for an app Steam does not know. Null when Steam could not be reached. Blocking. - */ fun lookup(appIds: List<Int>): Map<Int, SteamAppInfo.Info?>? { if (appIds.isEmpty()) return emptyMap() val host = runCatching { cmHost() }.getOrNull() ?: return null @@ -56,7 +42,6 @@ object SteamPics { ws.send(message(EMSG_LOGON, header(ANON_STEAMID, 0), Proto().varint(1, PROTOCOL_VERSION.toLong()).varint(7, 16).string(6, "english").bytes())) var steamId = 0L var session = 0 - // The logon's answer (other messages before it are passed over). loop@ while (true) for ((emsg, hdr, body) in messages(ws.receive() ?: return null)) if (emsg == EMSG_LOGON_RESPONSE) { val h = fields(hdr); val b = fields(body) if ((b[1]?.firstOrNull() as? Long ?: 0L) != 1L) { Log.w(TAG, "anonymous logon refused: ${b[1]}"); return null } @@ -99,10 +84,8 @@ object SteamPics { } } - /** [appIds] in requests of at most [BATCH], each once. */ fun batches(appIds: List<Int>): List<List<Int>> = appIds.distinct().chunked(BATCH) - /** A connection manager for the WebSocket transport, least loaded first as Steam lists them. */ private fun cmHost(): String? { val c = URL(CM_LIST).openConnection() as HttpURLConnection c.connectTimeout = 10_000; c.readTimeout = 10_000 @@ -112,11 +95,9 @@ object SteamPics { .firstOrNull { it.optString("type") == "websockets" }?.optString("endpoint")?.substringBefore(':') } - // ---- the product info answer ---- class InfoResponse(val apps: Map<Int, SteamAppInfo.Info>, val unknown: List<Int>, val pending: Boolean) - /** A CMsgClientPICSProductInfoResponse: each app's KeyValues read, the unknown ones, whether more follows. */ @VisibleForTesting internal fun parseInfoResponse(body: ByteArray): InfoResponse { val f = fields(body) @@ -132,17 +113,14 @@ object SteamPics { return InfoResponse(apps, unknown, (f[6]?.firstOrNull() as? Long ?: 0L) != 0L) } - /** An app's KeyValues text ("appinfo" { "common" {...} "depots" {...} }) to its name and shared depots. */ @VisibleForTesting internal fun infoFromKeyValues(text: String): SteamAppInfo.Info? { - // The buffer ends in a NUL, kept or not by whoever handed it over. val tree = runCatching { KeyValues(text.trimEnd('\u0000', ' ', '\n', '\r', '\t')).read() }.getOrNull() ?: return null @Suppress("UNCHECKED_CAST") val app = tree["appinfo"] as? Map<String, Any> ?: return null return SteamAppInfo.infoOf(app) } - /** Valve's text KeyValues: quoted keys and values, nested braces. */ private class KeyValues(private val s: String) { private var i = 0 fun read(): Map<String, Any> = map(top = true) @@ -182,7 +160,6 @@ object SteamPics { } } - // ---- messages ---- private fun header(steamId: Long, session: Int) = Proto().fixed64(1, steamId).varint(2, session.toLong()).bytes() @@ -190,7 +167,6 @@ object SteamPics { ByteBuffer.allocate(8 + header.size + body.size).order(ByteOrder.LITTLE_ENDIAN) .putInt(emsg or PROTO).putInt(header.size).put(header).put(body).array() - /** One message off the wire as (emsg, header, body); a Multi unpacked (gunzipped when it says so) into the ones inside. */ @VisibleForTesting internal fun messages(data: ByteArray): List<Triple<Int, ByteArray, ByteArray>> { if (data.size < 8) return emptyList() @@ -217,9 +193,7 @@ object SteamPics { return out } - // ---- protobuf, the little needed ---- - /** Writes protobuf fields. */ @VisibleForTesting internal class Proto { private val out = ByteArrayOutputStream() @@ -231,7 +205,6 @@ object SteamPics { fun bytes(): ByteArray = out.toByteArray() } - /** Reads protobuf fields: number to values (Long for varint and fixed, ByteArray for length-delimited). */ @VisibleForTesting internal fun fields(b: ByteArray): Map<Int, List<Any>> { val out = HashMap<Int, MutableList<Any>>() @@ -258,16 +231,13 @@ object SteamPics { return out } - // ---- WebSocket over TLS, binary frames ---- private class Wss(host: String, timeoutMs: Int) : AutoCloseable { - // Made with the host name, so it goes out as SNI. private val socket: SSLSocket = (SSLSocketFactory.getDefault().createSocket(host, 443) as SSLSocket).apply { soTimeout = timeoutMs } private val out: OutputStream private val input: DataInputStream init { - // Host name checked against the certificate, as HTTPS does. socket.sslParameters = socket.sslParameters.apply { endpointIdentificationAlgorithm = "HTTPS" } socket.startHandshake() out = socket.outputStream @@ -305,7 +275,6 @@ object SteamPics { out.write(frame.toByteArray()); out.flush() } - /** The next binary message (fragments joined); null on close. */ fun receive(): ByteArray? { val message = ByteArrayOutputStream() while (true) { diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt index f656df784..f41b8265f 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamRedists.kt @@ -2,14 +2,7 @@ package com.droiddeck.launcher.frontend import java.io.File -/** - * What Steam itself installs alongside a game: the Steamworks Shared redistributables (app 228980) - * listed under "SharedDepots" in the game's appmanifest. Those live in Steamworks Shared rather than - * the game's folder, so the folder scan ([DependencyDetector]) cannot see them; this reads Steam's - * list instead. Depots are matched to Bannerlator's catalog names; unknown ones are left out. - */ object SteamRedists { - /** Steamworks Shared depot -> (catalog component, what Steam calls it). 2015-2019 runtimes are one family. */ val DEPOTS = linkedMapOf( "228981" to ("vcredist2005_dll" to "VC++ 2005"), "228982" to ("vcredist2008_dll" to "VC++ 2008"), @@ -25,7 +18,6 @@ object SteamRedists { private val BLOCK = Regex("\"SharedDepots\"\\s*\\{([^}]*)\\}", RegexOption.IGNORE_CASE) private val ENTRY = Regex("\"(\\d+)\"\\s+\"\\d+\"") - /** The game's appmanifest beside its install folder (steamapps/common/<dir> -> steamapps/). */ fun manifest(gameDir: File?, appId: Int): File? { val steamapps = gameDir?.parentFile?.parentFile ?: return null return File(steamapps, "appmanifest_$appId.acf").takeIf { it.isFile } diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/StoreListBackfill.kt b/app/src/main/java/com/droiddeck/launcher/frontend/StoreListBackfill.kt index 0ac8cf34d..63315ac7b 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/StoreListBackfill.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/StoreListBackfill.kt @@ -14,47 +14,28 @@ import com.droiddeck.launcher.stores.gog.GogDownloadManager import org.json.JSONObject import java.io.File -/** - * For a GOG or Epic game installed before its sidecar kept the store's list (GOG's build - * `dependencies`, Epic's manifest prerequisite): the list read once from the store's build - * manifest with the saved sign-in - the manifest alone, never a game file - and kept in the sidecar - * the way an install keeps it. Runs on the components' worker ([AutoComponents]). Without a - * sign-in or a network it is skipped quietly and tried again a week later. - */ object StoreListBackfill { private const val TAG = "StoreListBackfill" private const val FILE = "store-list-backfill.json" private const val RETRY_AFTER_MS = 7L * 24 * 3600 * 1000 - /** The store's list for the game in a sidecar: GOG's dependency ids, or Epic's prerequisite {name, path}; null when it could not be had. */ fun interface Fetcher { fun fetch(context: Context, sidecar: StoreGameSidecar): Map<String, String>? } - /** The store itself, with the saved sign-in. */ internal val storeFetcher = Fetcher { context, sidecar -> fromStore(context, sidecar) } @VisibleForTesting internal var fetcher: Fetcher = storeFetcher - /** Whether [sidecar] was written before the list was kept. */ - /** - * Whether [sidecar] has no list read whole: written before the list was kept, or by a build - * that could have written an empty one for a manifest it did not read right. - */ fun needs(sidecar: StoreGameSidecar): Boolean = (sidecar.store == Store.GOG || sidecar.store == Store.EPIC) && WinCompSources.LIST_READ !in sidecar.extra - /** - * The list for the game in [folder], fetched and kept when its sidecar lacks one and it was not - * tried in the past week. True when the sidecar changed. Blocking (network). - */ @Synchronized fun fill(context: Context, folder: File): Boolean { val sidecar = StoreGameSidecar.read(folder)?.takeIf { it.isInstalled && needs(it) } ?: return false val tries = runCatching { JSONObject(AtomicFile(File(context.filesDir, FILE)).readFully().toString(Charsets.UTF_8)) }.getOrDefault(JSONObject()) val now = System.currentTimeMillis() if (now - tries.optLong(folder.path, 0L) < RETRY_AFTER_MS) return false - // Offline is no try: it waits for a network rather than a week. if (!AutoComponents.online(context)) return false val list = runCatching { fetcher.fetch(context, sidecar) }.getOrNull() if (list == null) { @@ -65,7 +46,6 @@ object StoreListBackfill { Log.i(TAG, "${sidecar.title}: the ${sidecar.store.label} list could not be had; again in a week") return false } - // Read fresh and written whole, as an install writes it: only the list is added. val current = StoreGameSidecar.read(folder) ?: return false current.copy(extra = current.extra + list + (WinCompSources.LIST_READ to "1")).write(folder) Log.i(TAG, "backfill: ${sidecar.title} " + when (sidecar.store) { diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/WinCompSources.kt b/app/src/main/java/com/droiddeck/launcher/frontend/WinCompSources.kt index b96d23bd6..7562cc1ee 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/WinCompSources.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/WinCompSources.kt @@ -7,35 +7,11 @@ import org.json.JSONObject import java.io.File import java.util.Locale -/** - * Which Windows components a game needs, from the list its source keeps - the source badge decides - * which list is trusted: - * - * - **Steam**: the Steamworks Shared redistributables Steam installs with the game (its - * appmanifest's SharedDepots, else the client's app info) - turned on by themselves. - * - **GOG**: the build manifest's `dependencies`, kept in the sidecar at install - on by themselves. - * - **Epic Games**: the manifest's prerequisite (usually UE4/UE5's prerequisite setup: VC++ and the - * DirectX June 2010 runtime), kept in the sidecar at install - on by themselves. - * - **Amazon Games**: fuel.json's PostInstall installers - on by themselves. - * - **Custom** whose files name a Steam appid ([SteamMatch], certain): Steam's list for that app - - * on by themselves. Matched by name only: the same list as Recommended. No match: the folder scan - * ([DependencyDetector]) as Recommended. - * - * Every store entry is mapped to catalog names here, in one place; one that maps to nothing is a - * suggestion under its own name. The folder scan is offered as Recommended for every game. - */ object WinCompSources { - /** Whose list a finding came from; the reason line says it. */ enum class Origin { STEAM, GOG, EPIC, AMAZON, STEAM_NAME, FOLDER } - /** - * One thing a list names: [component] the catalog name it maps to (null: none, a suggestion), - * [original] what the list called it, [auto] whether it is turned on by itself, [args] what - * the reason line names (Steam: the label, the game, the appid). - */ data class Finding(val component: String?, val original: String, val origin: Origin, val auto: Boolean, val args: List<String> = emptyList()) - // ---- mapping tables: a store's names -> catalog components ---- private val VC_YEAR = mapOf( "2005" to "vcredist2005", "2008" to "vcredist2008", "2010" to "vcredist2010", "2012" to "vcredist2012", @@ -47,10 +23,6 @@ object WinCompSources { "472" to "dotnet472", "48" to "dotnet48", "481" to "dotnet48", ) - /** - * A GOG dependency id ("MSVC2017_x64", "DirectX", "DOTNET48", "PhysX", "OpenAL", "XNA4") to its - * components; empty for one that is none of ours (ISI scripts, GOG's own helpers). - */ fun gog(id: String): List<String> { val key = id.uppercase(Locale.US).replace(Regex("[^A-Z0-9]"), "") Regex("""^(?:MSVC|VCREDIST|VC)(\d{4})""").find(key)?.let { m -> return listOfNotNull(VC_YEAR[m.groupValues[1]]) } @@ -67,40 +39,30 @@ object WinCompSources { } } - /** Unreal's prerequisite setup (UE4PrereqSetup_x64.exe, UEPrereqSetup_x64.exe): VC++ 2015-2022 and DirectX June 2010. */ private val UE_PREREQ = Regex("""(?i)ue\d?prereqsetup""") - /** An Epic prerequisite ([path] the installer it runs, [name] what it is called) to its components. */ fun epic(path: String, name: String): List<String> = when { UE_PREREQ.containsMatchIn(path) || name.contains("UE4 Prerequisites", true) || name.contains("UE5 Prerequisites", true) || name.contains("Unreal Engine Prerequisites", true) -> listOf("vcredist2022", "d3dx9") else -> DependencyDetector.componentsFor(path).ifEmpty { if (path.isEmpty()) gog(name) else emptyList() } } - /** An installer a store runs after an install (Amazon's PostInstall "Command") to its components. */ fun installer(path: String): List<String> = if (UE_PREREQ.containsMatchIn(path)) listOf("vcredist2022", "d3dx9") else DependencyDetector.componentsFor(path) - /** Steamworks Shared depot ids to (component, Steam's label). */ fun steamDepots(ids: List<String>): List<Pair<String, String>> = ids.mapNotNull { SteamRedists.DEPOTS[it] }.distinctBy { it.first } - // ---- what each store left at install ---- - /** Sidecar keys the store installs fill. */ const val GOG_DEPENDENCIES = "dependencies" const val EPIC_PREREQ_PATH = "prereqPath" const val EPIC_PREREQ_NAME = "prereqName" - /** Set beside the list when it was read whole (an empty list then means the store lists none). */ const val LIST_READ = "listRead" - /** fuel.json's PostInstall commands. */ fun amazonPostInstall(folder: File): List<String> = runCatching { val arr = JSONObject(File(folder, "fuel.json").readText()).optJSONArray("PostInstall") ?: return emptyList() (0 until arr.length()).mapNotNull { arr.optJSONObject(it)?.optString("Command")?.takeIf { c -> c.isNotEmpty() } } }.getOrDefault(emptyList()) - // ---- per game ---- - /** Steam's list for [appId]: the local appmanifest's SharedDepots, else the client's app info. */ fun steamList(context: Context, gameDir: File?, appId: Int, gameName: String, auto: Boolean): List<Finding> { val local = SteamRedists.detect(gameDir, appId) val pairs = if (local.isNotEmpty()) local.map { it.componentName to it.label } @@ -110,7 +72,6 @@ object WinCompSources { } } - /** Everything known for [game], blocking (files; the client's app info; no network). */ fun forGame(context: Context, game: Library.SteamGame): List<Finding> { val folder = game.gameFiles val out = ArrayList<Finding>() @@ -141,8 +102,6 @@ object WinCompSources { } else -> {} } - // A game added in DroidDeck (Custom, or a store's) that is a Steam app: Steam's list for it, - // on by itself when its files say so, else only recommended. if (game.library == Library.ADDED && folder != null) SteamMatch.get(context, folder.path)?.let { match -> val id = match.appId ?: return@let when (match.certainty) { @@ -151,7 +110,6 @@ object WinCompSources { else -> {} } } - // The folder scan, for every game: what it bundles, as Recommended. folder?.let { DependencyDetector.detect(it) }?.forEach { rec -> out += Finding(rec.componentName, rec.reason, Origin.FOLDER, false, listOf(rec.reason, rec.kind.name)) } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/Cardboard.kt b/app/src/main/java/com/droiddeck/launcher/gpu/Cardboard.kt index b5ec1b544..6c2522576 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/Cardboard.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/Cardboard.kt @@ -4,20 +4,6 @@ import com.droiddeck.launcher.wayland.WaylandCompositor import org.json.JSONObject import kotlin.math.sqrt -/** - * Cardboard mode: the compositor draws the session as two lens-distorted eye views side by side - * (waylandcomp/src/cardboard.c), for a phone or handheld dropped into a Google Cardboard-style - * viewer. Units are the menu's own: - * - * - [ipdMm] distance between the eyes, millimetres - * - [screenWidthDm] the virtual screen's width, decimetres (40 = 4 m) - * - [screenDistanceDm] how far away it hangs, decimetres (30 = 3 m) - * - [viewer] a [Viewer] id: the lens it is drawn for - * - * [source] SCREEN shows the frame as a flat virtual screen, world-locked by the rotation sensor - * when [headTracking] is on (3DoF), with real stereo depth from the IPD. SBS takes a frame that is - * already a left/right pair (SteamVR, SBS 3D games and video) and only adds the lens. - */ data class Cardboard( val enabled: Boolean = false, val source: Int = SOURCE_SCREEN, @@ -29,12 +15,6 @@ data class Cardboard( ) { val lens: Viewer get() = Viewer.byId(viewer) - /** - * Hands the settings to the compositor. [panelWidthMm] x [panelHeightMm] is the physical size of - * the panel the eyes are drawn on (0 = unknown). The width decides how far each lens centre sits - * from its viewport's centre, so the two pictures line up with the viewer's lenses; the height, - * seen through the lens from its focal distance, is the field of view the distortion works in. - */ fun push(panelWidthMm: Float, panelHeightMm: Float) { val l = lens WaylandCompositor.nativeSetCardboard(enabled, source, ipdMm / 1000f, screenDistanceDm / 10f, @@ -45,18 +25,10 @@ data class Cardboard( .put("enabled", enabled).put("source", source).put("viewer", viewer).put("ipdMm", ipdMm) .put("screenWidthDm", screenWidthDm).put("screenDistanceDm", screenDistanceDm).put("headTracking", headTracking) - /** - * A lens the eyes can be drawn for: its distortion (Cardboard viewer profile coefficients, in - * tangent units at the panel), how far it sits from the panel, and how far apart the two are. - * [fallbackFovDeg] is the eye's field of view when the panel's size is unknown, or with no lens. - */ enum class Viewer(val id: String, val k1: Float, val k2: Float, val screenToLensMm: Float, val lensSeparationMm: Float, val chroma: Float, val fallbackFovDeg: Float) { - /** Google Cardboard (2015): the common plastic and cardboard viewers. */ CARDBOARD_V2("cardboard2", 0.34f, 0.55f, 39.3f, 63.9f, 0.006f, 90f), - /** The original 2014 Cardboard: narrower, gentler lenses. */ CARDBOARD_V1("cardboard1", 0.441f, 0.156f, 42f, 60f, 0.004f, 80f), - /** No lens: side-by-side 3D glasses, a 3D TV, or just looking at the stereo pair. */ NONE("none", 0f, 0f, 0f, 0f, 0f, 60f); companion object { @@ -80,21 +52,12 @@ data class Cardboard( headTracking = json.optBoolean("headTracking", true), ) - /** - * One eye's vertical field of view on the panel side of the lens: half the panel's height - * seen from the lens's focal distance. The lens then spreads it wider (the distortion). - */ fun fovDeg(viewer: Viewer, panelHeightMm: Float): Float { if (panelHeightMm <= 0f || viewer.screenToLensMm <= 0f) return viewer.fallbackFovDeg val half = Math.toDegrees(kotlin.math.atan((panelHeightMm / 2f / viewer.screenToLensMm).toDouble())).toFloat() return (2f * half).coerceIn(30f, 140f) } - /** - * The lens centre's inset towards the nose, as a fraction of one eye viewport's width. Each - * eye gets half the panel, so its viewport centre is a quarter of the panel from the middle; - * the lens centre is half the lens separation from it. 0 when either is unknown. - */ fun lensInset(panelWidthMm: Float, lensSeparationMm: Float): Float { if (panelWidthMm <= 0f || lensSeparationMm <= 0f) return 0f val inset = (panelWidthMm / 4f - lensSeparationMm / 2f) / (panelWidthMm / 2f) @@ -103,15 +66,9 @@ data class Cardboard( } } -/** - * Head pose maths for Cardboard mode, kept free of Android types so it is unit-tested on the JVM. - * Matrices are row-major 3x3 FloatArrays. "World" is Android's sensor world (x east, y north, - * z up); "screen" axes are x right, y up, z out of the panel towards the viewer's eyes. - */ object HeadPose { val IDENTITY = floatArrayOf(1f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 1f) - /** Unit quaternion (x, y, z, w) -> rotation matrix: what SensorManager.getRotationMatrixFromVector gives. */ fun fromQuaternion(x: Float, y: Float, z: Float, w: Float): FloatArray { val n = sqrt(x * x + y * y + z * z + w * w).takeIf { it > 1e-6f } ?: 1f val qx = x / n; val qy = y / n; val qz = z / n; val qw = w / n @@ -122,57 +79,33 @@ object HeadPose { ) } - /** The rotation vector sensor's values -> (x, y, z, w), computing w when the sensor leaves it out. */ fun quaternionFromRotationVector(v: FloatArray): FloatArray { val x = v[0]; val y = v[1]; val z = v[2] val w = if (v.size >= 4) v[3] else sqrt((1f - x * x - y * y - z * z).coerceAtLeast(0f)) return floatArrayOf(x, y, z, w) } - /** - * Device -> screen axes for a Display rotation (Surface.ROTATION_* as 0..3): the returned matrix - * maps screen to world, given the device's own device -> world matrix. The same transform as - * input/PadMotion's and SensorManager.remapCoordinateSystem's (ROTATION_90 = AXIS_Y, AXIS_MINUS_X: - * the device's x is the screen's y, its y the screen's -x). - */ fun toScreen(deviceToWorld: FloatArray, rotation: Int): FloatArray { - // Columns of A: where each screen axis points in device coordinates. val a = when (rotation and 3) { - 1 -> floatArrayOf(0f, 1f, 0f, -1f, 0f, 0f, 0f, 0f, 1f) // screen x = -device y, y = device x - 2 -> floatArrayOf(-1f, 0f, 0f, 0f, -1f, 0f, 0f, 0f, 1f) // screen x = -device x, y = -device y - 3 -> floatArrayOf(0f, -1f, 0f, 1f, 0f, 0f, 0f, 0f, 1f) // screen x = device y, y = -device x + 1 -> floatArrayOf(0f, 1f, 0f, -1f, 0f, 0f, 0f, 0f, 1f) + 2 -> floatArrayOf(-1f, 0f, 0f, 0f, -1f, 0f, 0f, 0f, 1f) + 3 -> floatArrayOf(0f, -1f, 0f, 1f, 0f, 0f, 0f, 0f, 1f) else -> IDENTITY } return multiply(deviceToWorld, a) } - /** sin(60 degrees): a view further from level than this is "held flat" for recentring. */ private const val FLAT_SIN = 0.866f - /** - * What a recentre locks to. The pose the compositor gets is basis^T * screenToWorld * tilt: - * - [basis]: a level frame (columns right, up, back in world) facing the recentred heading. Every - * rotation is measured in it, so turning about the real vertical always pans the view; - * - [tilt]: the device's own tilt at the recentre, taken out so that pose starts straight ahead. - * Identity when the device was held upright: the horizon then stays the true one. - */ class Reference(val basis: FloatArray, val tilt: FloatArray) - /** - * Recentre on the current orientation. Held upright (the screen within 60 degrees of vertical, as - * in a viewer), only the heading resets: forward is where the screen faces. Held flat (lying back, - * or the device on a table or in the hands like a tray), forward is where the top of the screen - * points and the tilt resets too; turning at the hips still pans, tipping the device still tilts. - */ fun recenter(screenToWorld: FloatArray): Reference { val fz = -screenToWorld[8] val flat = kotlin.math.abs(fz) > FLAT_SIN - // Upright: the screen's -z. Flat: the screen's +y (its top edge), which is level then. var fx = if (flat) screenToWorld[1] else -screenToWorld[2] var fy = if (flat) screenToWorld[4] else -screenToWorld[5] val len = sqrt(fx * fx + fy * fy).takeIf { it > 1e-4f } ?: run { fx = 0f; fy = 1f; 1f } fx /= len; fy /= len - // right = forward x up(0,0,1) = (fy, -fx, 0); up = (0,0,1); back = -forward. val basis = floatArrayOf( fy, 0f, -fx, -fx, 0f, -fy, @@ -182,7 +115,6 @@ object HeadPose { return Reference(basis, tilt) } - /** head -> recentred world: what the compositor takes. */ fun relative(ref: Reference, screenToWorld: FloatArray): FloatArray = multiply(multiply(transpose(ref.basis), screenToWorld), ref.tilt) @@ -193,7 +125,6 @@ object HeadPose { fun transpose(m: FloatArray): FloatArray = floatArrayOf(m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8]) - /** m * v for a 3-vector. */ fun apply(m: FloatArray, v: FloatArray): FloatArray = floatArrayOf( m[0] * v[0] + m[1] * v[1] + m[2] * v[2], m[3] * v[0] + m[4] * v[1] + m[5] * v[2], diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt index 5e30ebb4a..48c58d01b 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt @@ -9,25 +9,17 @@ import com.droiddeck.launcher.session.SessionPrefs import java.io.File import java.util.Locale -/** - * Everything the device info window shows about the phone's chip: SoC, GPU, CPU, memory, system, - * the phone's own Vulkan driver and the drivers DroidDeck uses. Reads files and asks Vulkan once - * ([VulkanInfo]), so [collect] belongs off the main thread. Values the phone does not give are - * left out rather than guessed. - */ object DeviceInfo { class Section(val title: String, val rows: List<Pair<String, String>>) fun collect(context: Context): List<Section> { val vk = VulkanInfo.query() - // After Vulkan has been asked, detection can use its name too. val gpu = GpuInfo.detect() return listOf( chip(), gpu(gpu), cpu(), memory(context), system(), vulkan(context, vk), drivers(context, gpu), ).filter { it.rows.isNotEmpty() } } - /** The whole window as plain text, for a bug report. */ fun text(sections: List<Section>): String = buildString { append("DroidDeck device info\n") for (s in sections) { @@ -78,7 +70,6 @@ object DeviceInfo { private fun cpu() = Section("CPU", buildList<Pair<String, String>> { add("Cores", Runtime.getRuntime().availableProcessors().toString()) - // Each cpufreq policy is one cluster: the cores it covers and their speed range. val policies = File("/sys/devices/system/cpu/cpufreq").listFiles { f -> f.name.matches(Regex("policy\\d+")) } ?.sortedBy { it.name.removePrefix("policy").toInt() }.orEmpty() for (p in policies) { @@ -94,7 +85,6 @@ object DeviceInfo { private fun ghz(khz: Long) = String.format(Locale.US, "%.2f GHz", khz / 1e6) - /** The GPU's slowest and fastest step: KGSL's list, else a devfreq GPU node (Mali and others). */ private fun gpuRange(): String? { fun mhz(hz: Long) = "${hz / 1_000_000} MHz" val kgsl = read("/sys/class/kgsl/kgsl-3d0/gpu_available_frequencies") @@ -134,7 +124,6 @@ object DeviceInfo { add("API version", vk["api"]) add("Loader version", vk["loader"]) add("Driver", listOfNotNull(vk["driver_name"], vk["driver_version"]).joinToString(" ")) - // Qualcomm's build string starts with its own label ("Driver Build: 43c70540de, ..."). add("Driver build", vk["driver_info"]?.replace(Regex("(?i)^driver build:\\s*"), "")) add("Conformance", vk["conformance"]) add("Vendor / device id", listOfNotNull(vk["vendor_id"], vk["device_id"]).joinToString(" / ")) @@ -144,7 +133,6 @@ object DeviceInfo { val features = listOf("geometry_shader" to "Geometry shaders", "tessellation_shader" to "Tessellation", "texture_bc" to "BC textures") for ((key, label) in features) add(label, vk[key]) for ((key, value) in vk) if (key.startsWith("ext.")) add(key.removePrefix("ext."), value) - // What Android certifies the phone for. val pm = context.packageManager val hw = pm.systemAvailableFeatures.associate { it.name to it.version } hw[PackageManager.FEATURE_VULKAN_HARDWARE_VERSION]?.let { diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt b/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt index 2814e4bfb..db8dc86c4 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt @@ -11,37 +11,18 @@ import java.io.File import java.io.IOException import java.util.zip.ZipInputStream -/** - * An all-in-one driver package: one zip carrying both halves of a matched pair, the display - * (Android, bionic) driver in one directory and the runtime (Linux, glibc) driver in another, - * indexed by a root `manifest.json` - DroidDeck's own DD-Turnip releases (Droid-Deck/Drivers): - * - * ``` - * manifest.json {"package", "version", "drivers": {"android": {"path"}, "linux": {"path"}}} - * android/ meta.json (AdrenoTools) + libvulkan_freedreno.so - * linux/ meta.json + libvulkan_freedreno.so - * ``` - * - * Installing one puts each half through its own importer, with that importer's checks, and - * remembers the two as one bundle: picking either half sets both, and deleting either deletes both, - * so a bundle is always used whole. Zips of the older kind, one driver each, never reach this. - */ object DriverBundle { private const val TAG = "DriverBundle" private const val PREFS = "driver_bundles" private const val MANIFEST = "manifest.json" - /** Refuses a zip that unpacks to more than this: both halves together are about 30 MB. */ private const val MAX_BYTES = 512L shl 20 class Manifest(val name: String, val version: String, val tag: String, val androidPath: String, val linuxPath: String) - /** An installed bundle: [androidId] in TurnipDriver's list, [linuxId] in LinuxVulkanDriverManager's. */ class Bundle(val id: String, val name: String, val version: String, val androidId: String, val linuxId: String) { - /** "DD-Turnip 0.1.0". */ val label: String get() = if (version.isEmpty()) name else "$name $version" } - /** The manifest of a bundle, or null when [json] is not one (both halves, in two directories). */ fun parseManifest(json: JSONObject): Manifest? { val drivers = json.optJSONObject("drivers") ?: return null val android = cleanPath(drivers.optJSONObject("android")?.optString("path").orEmpty()) ?: return null @@ -58,7 +39,6 @@ object DriverBundle { return p.takeIf { it.isNotEmpty() && it.split('/').none { part -> part.isEmpty() || part == "." || part == ".." } } } - /** True when the zip at [uri] is a bundle rather than a single driver. */ fun isBundle(context: Context, uri: Uri): Boolean = try { context.contentResolver.openInputStream(uri)?.use { input -> ZipInputStream(input).use { zip -> @@ -71,11 +51,6 @@ object DriverBundle { false } - /** - * Install both halves of the bundle at [uri]. Both or neither: a half that is refused removes - * the one already in. Throws [IllegalArgumentException] with a user-facing reason when the zip - * is not a usable bundle, [IOException] on read failures. - */ fun install(context: Context, uri: Uri): Bundle { val work = File(context.cacheDir, "bundle-${System.nanoTime()}") val td = TurnipDriver(context) @@ -153,7 +128,6 @@ object DriverBundle { } ?: throw IOException("cannot open $uri") } - /** Every installed bundle whose two halves are both still there. */ fun all(context: Context): List<Bundle> { val td = TurnipDriver(context) val lm = LinuxVulkanDriverManager(context) @@ -167,24 +141,20 @@ object DriverBundle { fun get(context: Context, id: String): Bundle? = all(context).firstOrNull { it.id == id } - /** The bundle [driverId] is a half of, in the runtime list when [linux], else the display list. */ fun containing(context: Context, driverId: String, linux: Boolean): Bundle? = all(context).firstOrNull { (if (linux) it.linuxId else it.androidId) == driverId } - /** The bundle both drivers are set to right now, if they are one. */ fun active(context: Context): Bundle? { val android = SessionPrefs.androidDriver(context) val linux = SessionPrefs.linuxDriver(context) return all(context).firstOrNull { it.androidId == android && it.linuxId == linux } } - /** Set both halves as the drivers in use. */ fun select(context: Context, bundle: Bundle) { SessionPrefs.setAndroidDriver(context, bundle.androidId) SessionPrefs.setLinuxDriver(context, bundle.linuxId) } - /** Delete both halves; a driver still set to either goes back to its default. */ fun remove(context: Context, bundle: Bundle) { TurnipDriver(context).remove(bundle.androidId) LinuxVulkanDriverManager(context).removeDriver(bundle.linuxId) diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DriverPairs.kt b/app/src/main/java/com/droiddeck/launcher/gpu/DriverPairs.kt index f3b28fe38..f737b4d4c 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DriverPairs.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DriverPairs.kt @@ -2,20 +2,6 @@ package com.droiddeck.launcher.gpu import com.droiddeck.launcher.gpu.GpuInfo.Family -/** - * The release drivers as matched pairs: a display build (the app's compositor, bionic) and a Linux - * build (the runtime, glibc) that were made to run together. Mixing a runtime driver carrying a - * patch with a display driver that lacks it is where the hard-to-read bugs come from, so the app - * installs and selects both halves of one pair at once. - * - * - Banners-Turnip publishes each variant as a set in every release (`Turnip-<tag><variant>.zip` - * and `...-Linux.zip`), rebuilt from upstream Mesa as it moves: the pair is one variant of the - * newest release that carries both halves. - * - WinNative ships its display (`v1.x`) and Linux (`linux-v0.x`) builds as two release lines with - * their own numbers: the pair is the newest of each, of one flavour (Balanced or Performance). - * - DroidDeck's DD-Turnip ships both halves in one zip per release (DriverBundle): the pair is the - * newest bundle, and it can never be mixed. - */ object DriverPairs { const val BANNER = "banner" const val BANNER_A8XX = "banner-a8xx" @@ -24,24 +10,18 @@ object DriverPairs { const val WN_BALANCED = "wn-b" const val WN_PERFORMANCE = "wn-p" const val DD_TURNIP = "dd-turnip" - /** Each DD-Turnip release is its own pair: "dd-turnip:DD-Turnip-v0.1.0". */ fun ddTurnip(tag: String) = "$DD_TURNIP:$tag" class DriverPair( val key: String, - /** "Banners-Turnip · Adreno 6xx/7xx", "WinNative · Balanced". */ val name: String, - /** The families the pair is built for; the list only offers it on those unless asked. */ val families: Set<Family>, val display: TurnipReleases.Asset?, val linux: TurnipReleases.Asset?, - /** Both halves in one zip, instead of [display] and [linux]. */ val bundle: TurnipReleases.Asset? = null, ) { val complete: Boolean get() = bundle != null || (display != null && linux != null) - /** The zips to download for the pair: the bundle, or the two halves. */ val assets: List<TurnipReleases.Asset> get() = listOfNotNull(bundle ?: display, if (bundle == null) linux else null) - /** "r4 · 1.19 + 0.1.2": the build of each half. */ val version: String get() = listOfNotNull(bundle?.tag, display?.tag, linux?.tag).distinct().joinToString(" + ") { it.removePrefix("linux-").removePrefix("DD-Turnip-") } fun suits(gpu: GpuInfo) = gpu.family in families || @@ -65,7 +45,6 @@ object DriverPairs { else -> key to ALL_ADRENO } - /** Every pair the last release check found, in the order the list shows them. */ fun from(check: TurnipReleases.Check?): List<DriverPair> { val assets = check?.assets.orEmpty().filter { it.pair.isNotEmpty() } val order = listOf(DD_TURNIP, BANNER, BANNER_A8XX, BANNER_ONEUI, BANNER_710, WN_BALANCED, WN_PERFORMANCE) @@ -75,9 +54,6 @@ object DriverPairs { val (name, families) = describe(key) val halves = assets.filter { it.pair == key } halves.firstOrNull { it.bundle }?.let { return@map DriverPair(key, name, families, null, null, it) } - // The list keeps the newest release of each half; for Banners-Turnip the two halves - // must come from the same release, so a set missing one half in its newest release - // is offered incomplete rather than mixed with an older one. val display = halves.firstOrNull { !it.linux } val linux = halves.firstOrNull { it.linux } val sameRelease = !key.startsWith(BANNER) || display?.tag == linux?.tag @@ -85,24 +61,15 @@ object DriverPairs { } } - /** - * The pair Auto sets for [gpu], among the [pairs] the last release check found: the newest - * official DD-Turnip release on the Adreno 840 (840 and 840v2), WinNative Balanced on the other - * 8xx (Banners' A8xx builds still break on some first boots) and on an 840 while no DD-Turnip - * release is listed, the OneUI build for an 8 Gen 2 under One UI, the patched test builds for - * the 710/720/722, and upstream Banners-Turnip for the rest. Null when nothing fits (not Adreno). - */ fun recommendedKey(gpu: GpuInfo, pairs: List<DriverPair>): String? = when (gpu.family) { Family.NOT_ADRENO -> null Family.A8XX -> if (gpu.model == 840) newestDdTurnip(pairs) ?: WN_BALANCED else WN_BALANCED Family.A7XX_LOW -> BANNER_710 Family.A7XX -> if (gpu.oneUi8Gen2) BANNER_ONEUI else BANNER Family.A6XX -> BANNER - // KGSL gave no model: Android 16 arrived with the 8xx, so a device that new is taken as one. Family.ADRENO_UNKNOWN -> if (android.os.Build.VERSION.SDK_INT >= 36) WN_BALANCED else BANNER } - /** The complete DD-Turnip pair with the highest version, or null when none is listed. */ fun newestDdTurnip(pairs: List<DriverPair>): String? = pairs .filter { it.key.startsWith("$DD_TURNIP:") && it.complete } .maxByOrNull { p -> diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DriverSetupLog.java b/app/src/main/java/com/droiddeck/launcher/gpu/DriverSetupLog.java index 00aca26a5..0fb72fbac 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DriverSetupLog.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DriverSetupLog.java @@ -5,7 +5,6 @@ import android.util.Log; import java.io.File; import java.io.FileWriter; -/** Setup breadcrumbs survive failures before a session log exists. No account data. */ public final class DriverSetupLog { private DriverSetupLog() {} @@ -13,7 +12,7 @@ public final class DriverSetupLog { Log.i("DriverSetup", stage); append(context.getFilesDir(), stage); try { append(context.getExternalFilesDir(null), stage); } - catch (Exception ignored) { /* External storage is optional. */ } + catch (Exception ignored) { } } private static void append(File directory, String stage) { @@ -21,10 +20,9 @@ public final class DriverSetupLog { try { directory.mkdirs(); File file = new File(directory, "driver-setup.txt"); - // Keep the latest run's breadcrumbs bounded across repeated attempts. try (FileWriter out = new FileWriter(file, file.length() < 131072)) { out.write(System.currentTimeMillis() + " " + stage + "\n"); } - } catch (Exception ignored) { /* Diagnostics must never prevent startup. */ } + } catch (Exception ignored) { } } } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/FrameGen.kt b/app/src/main/java/com/droiddeck/launcher/gpu/FrameGen.kt index b864e837b..952ada616 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/FrameGen.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/FrameGen.kt @@ -5,34 +5,20 @@ import android.util.Log import com.droiddeck.launcher.R import com.droiddeck.launcher.wayland.WaylandCompositor -/** - * The frame-generation setting and how it reaches the compositor. - * - * Both engines generate on gamescope's final output inside our compositor, so they work for any - * game with nothing changed in the runtime. Win-FG is our own and needs nothing; LSFG needs the - * shader chain from the user's copy of Lossless Scaling (see [Lossless]). Every compositor - * setter is a value store on the native side, so this can be applied before the compositor is - * up and changed live from the main screen while a session runs. - */ object FrameGen { const val ENGINE_OFF = "off" const val ENGINE_WINFG = "winfg" const val ENGINE_LSFG = "lsfg" - /** Win-FG's Performance preset, pinned: changing it live rebuilds the whole chain. */ private const val WINFG_PERF_PRESET = 2 - /** Bidirectional flow with the occlusion gate - the chain's own default. */ private const val WINFG_MODEL = 4 - /** Optical-flow resolution as a fraction of the output; Bannerlator's per-engine defaults. */ private const val FLOW_SCALE_WINFG = 0.60f private const val FLOW_SCALE_LSFG = 0.80f - /** LSFG's adaptive targets: it generates as many frames as it takes to reach one. */ val ADAPTIVE_TARGETS = listOf(120, 90, 60) private const val PREFS = "frame_gen" - /** A fixed [multiplier] (2..4), or for LSFG a [target] frame rate (0 = fixed). */ data class Mode(val engine: String, val multiplier: Int = 2, val target: Int = 0) { companion object { val OFF = Mode(ENGINE_OFF) @@ -54,7 +40,6 @@ object FrameGen { .putString("engine", mode.engine).putInt("multiplier", mode.multiplier).putInt("target", mode.target).apply() } - /** "Off", "Win-FG 2×", "LSFG 3×", "LSFG Adaptive 90". */ fun label(context: Context): String = label(context, mode(context)) fun label(context: Context, mode: Mode): String = when { @@ -64,11 +49,6 @@ object FrameGen { else -> context.getString(R.string.frame_gen_off) } - /** - * Pushes the saved setting into the compositor. Returns a message for the user when the - * engine cannot run (LSFG without Lossless Scaling), or null. Blocks while a new or updated - * Lossless.dll is prepared. - */ fun apply(context: Context, refreshHz: Float): String? { val mode = mode(context) when (mode.engine) { diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt index 87bf01d4d..d14fe8417 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt @@ -6,38 +6,23 @@ import com.droiddeck.launcher.R import com.droiddeck.launcher.core.FileUtils import java.io.File -/** - * What GPU this is, in the terms the driver lists are sorted by. KGSL names the model - * ("Adreno740v2", "Adreno825"); the family decides which Turnip builds run on it at all, and the - * support level is what the app has actually been tested on (Adreno 650, 725 and newer) - an - * Adreno 610 may start, but it is not "supported" just because it is a Qualcomm chip. - */ data class GpuInfo( - /** "Adreno 740", or the vendor's own name when this is not an Adreno. */ val name: String, - /** The three-digit Adreno model (740, 825), or 0 when KGSL does not say. */ val model: Int, val family: Family, - /** The SoC as a person would say it ("Snapdragon 8 Gen 2 (QCS8550)"), or "" when the phone does not say. */ val soc: String, - /** Samsung's One UI on an 8 Gen 2: its Turnip needs the OneUI build, or frames tear and flicker. */ val oneUi8Gen2: Boolean, - /** KGSL's own name for the GPU, as the kernel spells it ("Adreno740v2", "Adreno33v2"). */ val kgslName: String = "", - /** Where [model] came from: "kernel", "vulkan" or "platform", "" when unknown. */ val modelSource: String = "", ) { enum class Family(val label: String) { A8XX("Adreno 8xx"), - /** Adreno 710/720/722: gen 7 cores cut down enough to need their own patches. */ A7XX_LOW("Adreno 710/720/722"), A7XX("Adreno 7xx"), A6XX("Adreno 6xx"), - /** An Adreno whose model KGSL does not give: treated as the newest family it could be. */ ADRENO_UNKNOWN("Adreno"), NOT_ADRENO("Not an Adreno GPU"); - /** [label] in the app's language: the Adreno families are names, the rest is words. */ fun label(context: Context): String = if (this == NOT_ADRENO) context.getString(R.string.gpuinfo_not_adreno) else label } @@ -52,7 +37,6 @@ data class GpuInfo( else -> Support.UNTESTED } - /** One line for the device card and the system check, in English for the device report. */ val supportText: String get() = when (support) { Support.TESTED -> "Supported" @@ -61,30 +45,23 @@ data class GpuInfo( Support.UNSUPPORTED -> "Not supported: DroidDeck needs an Adreno (Snapdragon) GPU" } - /** [supportText] in the app's language. */ fun supportText(context: Context): String = context.getString(when (support) { Support.TESTED -> R.string.gpuinfo_supported Support.UNTESTED -> if (family == Family.A7XX_LOW) R.string.gpuinfo_experimental else R.string.gpuinfo_below_tested Support.UNSUPPORTED -> R.string.gpuinfo_unsupported }) - /** [name] for the screen: the stand-in for a GPU the device does not name is in the app's language. */ fun displayName(context: Context): String = if (name == UNNAMED) context.getString(R.string.gpu_this_gpu) else name companion object { - /** [name] when the device names no GPU; English, as the device report shows it. */ private const val UNNAMED = "this GPU" fun detect(): GpuInfo { val adreno = File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists() val raw = listOf("/sys/class/kgsl/kgsl-3d0/gpu_model", "/sys/class/kgsl/kgsl-3d0/gpu_chipid") .firstNotNullOfOrNull { FileUtils.readString(File(it))?.trim()?.takeIf(String::isNotEmpty) } - // Where vendors put the chip's model, named when it is known: "Snapdragon 8 Gen 2 (QCS8550)". val soc = if (adreno) SocNames.label() else "" val fromKernel = raw?.let { threeDigits(it) } ?: 0 - // Some kernels name the GPU without its model (AYANEO's Pocket FIT: "Adreno33v2"). The - // Vulkan driver's own name, when this process has asked it, then the platform's code - // name stand in - the GPU is the same on every phone of a platform. val fromVulkan = if (fromKernel > 0 || !adreno) 0 else VulkanInfo.cachedOrNull()?.get("device")?.let { threeDigits(it) } ?: 0 val fromPlatform = if (fromKernel > 0 || fromVulkan > 0 || !adreno) 0 else platformModel() val model = maxOf(fromKernel, fromVulkan, fromPlatform) @@ -106,7 +83,6 @@ data class GpuInfo( private fun threeDigits(text: String): Int = Regex("""(\d{3})""").find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0 - /** Qualcomm platform code names whose GPU is known for certain. */ private val PLATFORMS = mapOf( "msmnile" to 640, "kona" to 650, "lahaina" to 660, "taro" to 730, "cape" to 730, "kalama" to 740, "pineapple" to 750, "sun" to 830, @@ -115,7 +91,6 @@ data class GpuInfo( internal fun platformModel(platform: String = systemProperty("ro.board.platform")): Int = PLATFORMS[platform.lowercase()] ?: PLATFORMS[systemProperty("ro.vendor.qti.soc_name").lowercase()] ?: 0 - /** android.os.SystemProperties.get, which apps may read but not call directly. */ fun systemProperty(name: String): String = try { Class.forName("android.os.SystemProperties").getMethod("get", String::class.java).invoke(null, name) as? String ?: "" } catch (_: Exception) { diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/HeadTracker.kt b/app/src/main/java/com/droiddeck/launcher/gpu/HeadTracker.kt index 3841854e2..849c7870e 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/HeadTracker.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/HeadTracker.kt @@ -10,15 +10,6 @@ import android.os.HandlerThread import android.util.Log import com.droiddeck.launcher.wayland.WaylandCompositor -/** - * 3DoF head tracking for Cardboard mode: the rotation sensor -> the compositor's head pose - * ([HeadPose] for the maths). Game rotation vector first (gyro + accelerometer: no compass jumps - * indoors, and only the heading drifts, which a recentre fixes); the plain rotation vector where a - * device has no game one. Events arrive on a thread of their own and go straight to native code, - * so the session's UI thread never sits between the sensor and the screen. - * - * [rotation] is the Display rotation (Surface.ROTATION_*) the session is drawn in. - */ class HeadTracker(context: Context, private val rotation: () -> Int) : SensorEventListener { private val sensors = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager private val sensor: Sensor? = sensors.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR) @@ -28,7 +19,6 @@ class HeadTracker(context: Context, private val rotation: () -> Int) : SensorEve @Volatile private var recenterPending = true @Volatile private var latest: FloatArray? = null - /** Whether this device has a rotation sensor at all (no sensor = a fixed, head-locked screen). */ val available: Boolean get() = sensor != null val sensorName: String get() = sensor?.let { "${it.name} (${if (it.type == Sensor.TYPE_GAME_ROTATION_VECTOR) "game rotation vector" else "rotation vector"})" } ?: "none" @@ -54,7 +44,6 @@ class HeadTracker(context: Context, private val rotation: () -> Int) : SensorEve WaylandCompositor.nativeSetCardboardPose(HeadPose.IDENTITY, false) } - /** Make where the viewer looks now straight ahead (see HeadPose.recenter). */ fun recenter() { recenterPending = true latest?.let { apply(it) } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java index 4662f894f..d74b5a3f1 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriver.java @@ -6,28 +6,8 @@ import android.util.Log; import java.util.ArrayList; import java.util.List; -/** - * The Vulkan driver a session draws with inside the runtime: in Steam mode the native client's - * own UI (OpenGL through the runtime's Zink), every game it launches (D3D through Proton's - * DXVK/VKD3D) and gamescope's compositing; in desktop mode every program on the desktop. One - * choice per mode covers all of it. - * - * <p>Distinct from the Android driver the app's compositor loads to put the finished frame on the - * screen ({@link TurnipDriver}), which every session ends in - and NOT interchangeable with it: - * these are glibc processes, that one is bionic. - * - * <p>"" (the default) means the driver the runtime was built with, at - * {@code usr/lib/libvulkan_freedreno.so} inside it - nothing is set and nothing is touched. - * Otherwise the value is an imported driver's id ({@link LinuxVulkanDriverManager}), and the - * session is handed that driver's ICD manifest in {@code BL_VK_DRIVER}: the session script points - * the Vulkan loader at it with {@code VK_DRIVER_FILES} after checking both the manifest and the - * library are readable from inside, so the runtime itself is never modified and the choice is - * reversible. Ported from Bannerlator's {@code LinuxVulkanDriver} (GPL-3.0); the choice lives in - * {@code SessionPrefs} per mode here rather than on a shortcut. - */ public final class LinuxVulkanDriver { private static final String TAG = "LinuxVulkanDriver"; - /** Env var the session reads; its value is an absolute ICD manifest path. */ public static final String ENV = "BL_VK_DRIVER"; public static final String HELP_TEXT = @@ -38,7 +18,6 @@ public final class LinuxVulkanDriver { private LinuxVulkanDriver() {} - /** Stored values in editor order: the runtime's own driver, then each imported one. */ public static List<String> optionValues(Context context) { ArrayList<String> values = new ArrayList<>(); values.add(""); @@ -46,10 +25,6 @@ public final class LinuxVulkanDriver { return values; } - /** - * Label for a stored value. An id that is no longer installed says so - the launch path falls - * back to the runtime's own driver for it. - */ public static String optionLabel(Context context, String value) { if (value == null || value.isEmpty()) return "Runtime default (built into the runtime)"; LinuxVulkanDriverManager m = new LinuxVulkanDriverManager(context); @@ -58,10 +33,6 @@ public final class LinuxVulkanDriver { return m.getDriverName(value) + (ver.isEmpty() ? "" : " " + ver) + " (imported)"; } - /** - * The ICD manifest path to hand the session, or null to leave the runtime's own driver alone. - * An id whose import is gone resolves to null rather than failing the launch. - */ public static String resolveIcdPath(Context context, String value) { if (value == null || value.isEmpty()) return null; LinuxVulkanDriverManager m = new LinuxVulkanDriverManager(context); diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java index e5c3d585c..3973297cd 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java @@ -22,30 +22,6 @@ import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -/** - * Imported LINUX Vulkan drivers: glibc Turnip ICDs ({@code Turnip-<tag>[-variant]-Linux.zip} from - * Banners-Turnip) for the runtime - the gamescope session that runs Valve's native ARM64 Steam - * client, and the KDE Plasma desktop beside it. This is the driver that DRAWS there: the client's own UI - * (OpenGL through the runtime's Zink), every game the client launches (D3D through Proton's - * DXVK/VKD3D) and everything on the desktop. Putting the frame on the screen stays the Android - * driver's job, in the app's compositor ({@link TurnipDriver}). - * - * <p>Not interchangeable with that one: the client and its games are glibc processes and cannot - * load a bionic object at all, and this one cannot be loaded by the app. The check that separates - * them is the libc the driver links against - {@code libc.so.6} here, {@code libc.so} there. - * - * <p>Layout (under the app's files dir, which the session sees by its full host path): - * <pre> - * files/linux_vulkan_drivers/<id>/ - * libvulkan_freedreno.so the driver - * icd.json generated Vulkan ICD manifest; library_path = the .so's ABSOLUTE path - * meta.json name / driverVersion / minGlibc, our own schema - * </pre> - * The session gets {@code icd.json}'s path in {@code BL_VK_DRIVER} and exports it as - * {@code VK_DRIVER_FILES}, so nothing inside the runtime is modified and the choice is reversible. - * - * <p>Ported from Bannerlator's {@code LinuxVulkanDriverManager} (GPL-3.0), unchanged in behaviour. - */ public class LinuxVulkanDriverManager { private static final String TAG = "LinuxVulkanDriver"; public static final String DIR_NAME = "linux_vulkan_drivers"; @@ -66,14 +42,12 @@ public class LinuxVulkanDriverManager { return new File(rootDir, id); } - /** Installed = has the .so AND the generated manifest (a half-extracted dir is never offered). */ public boolean isInstalled(String id) { if (id == null || id.isEmpty() || id.contains("/") || id.contains("..")) return false; File dir = getDriverDir(id); return new File(dir, LIB_NAME).isFile() && new File(dir, ICD_NAME).isFile(); } - /** Absolute path of the driver's ICD manifest, or null when the id isn't installed. */ public String getIcdPath(String id) { return isInstalled(id) ? new File(getDriverDir(id), ICD_NAME).getAbsolutePath() : null; } @@ -108,7 +82,6 @@ public class LinuxVulkanDriverManager { return m != null ? m.optString("driverVersion", "") : ""; } - /** The glibc the driver asks for, as its zip recorded it ("" when the zip did not say). */ public String getMinGlibc(String id) { JSONObject m = readMeta(id); return m != null ? m.optString("minGlibc", "") : ""; @@ -118,14 +91,8 @@ public class LinuxVulkanDriverManager { if (id == null || id.isEmpty() || id.contains("/") || id.contains("..")) return; Log.d(TAG, "removing imported Linux Vulkan driver " + id); FileUtils.delete(getDriverDir(id)); - // A mode still set to this id falls back to the runtime's own driver at launch (logged there). } - /** - * Import a {@code -Linux.zip}. Returns the new driver id. - * @throws IllegalArgumentException with a user-facing reason when the zip is not an importable - * Linux driver; {@link IOException} on read/extract failures. - */ public String installDriver(Uri zipUri, String displayName) throws IOException { File tmpDir = newStagingDir(); boolean keep = false; @@ -138,7 +105,6 @@ public class LinuxVulkanDriverManager { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { if (entry.isDirectory()) continue; - // Flatten: only the base name matters, and it also defeats zip-slip paths. String base = new File(entry.getName()).getName(); if (base.isEmpty()) continue; if (base.startsWith("libvulkan_freedreno") && base.endsWith(".so")) { @@ -154,8 +120,6 @@ public class LinuxVulkanDriverManager { Log.w(TAG, "meta.json unreadable, ignoring: " + e.getMessage()); } } - // The zip's own freedreno_icd.aarch64.json is dropped on purpose: its - // library_path is relative to itself, and adopt() writes an absolute one. } } String id = adopt(tmpDir, soName, zipMeta, displayName); @@ -166,7 +130,6 @@ public class LinuxVulkanDriverManager { } } - /** An empty directory beside the installed drivers, for a driver's files before {@link #adopt}. */ File newStagingDir() throws IOException { File tmpDir = new File(rootDir, ".tmp-" + System.nanoTime()); FileUtils.delete(tmpDir); @@ -174,11 +137,6 @@ public class LinuxVulkanDriverManager { return tmpDir; } - /** - * Check a staged driver ({@link #LIB_NAME} in {@code tmpDir}, copied from {@code soName} in - * the zip), write its manifest and meta.json, and move it in. Returns the id; the caller - * deletes {@code tmpDir} when this throws. - */ String adopt(File tmpDir, String soName, JSONObject zipMeta, String displayName) throws IOException { try { if (soName == null) { @@ -188,9 +146,6 @@ public class LinuxVulkanDriverManager { if (!isAarch64Elf(so)) { throw new IllegalArgumentException(context.getString(R.string.lvd_not_aarch64, soName)); } - // The one check that actually separates this from the bionic builds. Both sonames - // are in the driver's .dynstr, so a byte scan is enough and needs no ELF parsing: - // glibc's is versioned ("libc.so.6"), bionic's is not ("libc.so"). if (!containsAscii(so, "libc.so.6")) { throw new IllegalArgumentException(context.getString(R.string.lvd_not_glibc, soName)); } @@ -209,8 +164,6 @@ public class LinuxVulkanDriverManager { String id = uniqueId(sanitizeId(name)); File dir = getDriverDir(id); - // ICD manifest: absolute library_path, so the loader never depends on cwd and the guest - // reads it at the same path the app wrote (the files dir is bound into the session). JSONObject icd = new JSONObject(); icd.put("file_format_version", "1.0.0"); JSONObject icdBody = new JSONObject(); @@ -251,7 +204,6 @@ public class LinuxVulkanDriverManager { return s.isEmpty() ? "linux-driver" : s; } - /** ELF magic, EI_CLASS = 64-bit, EI_DATA = little-endian, e_machine = 0xB7 (AArch64). */ static boolean isAarch64Elf(File f) { try (FileInputStream in = new FileInputStream(f)) { byte[] h = new byte[20]; @@ -292,7 +244,6 @@ public class LinuxVulkanDriverManager { while (j < nb.length && buf[i + j] == nb[j]) j++; if (j == nb.length) return true; } - // Keep the last needle-1 bytes so a match across a buffer boundary is still found. carry = Math.min(nb.length - 1, len); System.arraycopy(buf, len - carry, buf, 0, carry); } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/Lossless.kt b/app/src/main/java/com/droiddeck/launcher/gpu/Lossless.kt index b07e5d4bb..375a60a57 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/Lossless.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/Lossless.kt @@ -10,16 +10,6 @@ import java.io.File import java.security.DigestOutputStream import java.security.MessageDigest -/** - * LSFG's shader chain, from the user's own Lossless Scaling (Steam app 993090). - * - * The copy the Steam client installs, in any library it installs to, is taken by - * itself and taken again whenever the client updates it. Once the session has seen the account - * own the app, a Lossless.dll can also be imported by hand; it stays until the Steam copy changes - * or another is imported. The DLL is copied in and its shaders are built into a cache named after - * its contents, so a different DLL hands the compositor a different path and its engine reloads. - * Everything here blocks: call it off the main thread. - */ object Lossless { enum class Source { NONE, STEAM, IMPORT } @@ -46,7 +36,6 @@ object Lossless { return State(owned(context), cacheFile(context)?.isFile == true, source) } - /** Recorded once: by the session's ownership check (a flag in the guest's home) or a Steam copy. */ fun owned(context: Context): Boolean { if (prefs(context).getBoolean(KEY_OWNED, false)) return true if (!ownedFlag(context).exists()) return false @@ -56,7 +45,6 @@ object Lossless { fun cacheFile(context: Context): File? = prefs(context).getString(KEY_ID, null)?.let { cacheFor(context, it) } - /** Takes a new or updated Steam copy and rebuilds a missing or outdated cache. A [LsfgNative] status. */ @Synchronized fun sync(context: Context): Int { val steam = steamDll(context) @@ -163,7 +151,6 @@ object Lossless { ?.forEach { it.delete() } } - /** The newest copy in any library the client installs to: its own, the card's, the Games folders. */ private fun steamDll(context: Context): File? { val libraries = listOfNotNull( File(LinuxRuntime.rootDir(context), "root/.local/share/Steam"), diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/LsfgNative.java b/app/src/main/java/com/droiddeck/launcher/gpu/LsfgNative.java index 6eb3c3441..d16a8c7a4 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/LsfgNative.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/LsfgNative.java @@ -1,16 +1,10 @@ package com.droiddeck.launcher.gpu; -/** - * The native half of LSFG's shader cache (lsfg_jni.cpp, compiled into the compositor library): - * {@code Lossless.dll} is mapped read-only and parsed as data, never loaded or executed, and its - * DXBC chain translated to SPIR-V. {@link Lossless} decides when; these calls block. - */ final class LsfgNative { static { System.loadLibrary("droiddeckwayland"); } private LsfgNative() {} - // Mirrors lsfg::DllStatus. static final int STATUS_OK = 0; static final int STATUS_NOT_INSTALLED = 1; static final int STATUS_UNREADABLE_FILE = 2; @@ -19,7 +13,6 @@ final class LsfgNative { static final int STATUS_TRANSLATION_FAILED = 5; static final int STATUS_CACHE_UNUSABLE = 6; - // Mirrors lsfg::Variant. static final int VARIANT_NONE = 0; static native int nativeValidateDll(String dllPath); diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/ScreenEffects.kt b/app/src/main/java/com/droiddeck/launcher/gpu/ScreenEffects.kt index 36d233f49..a7c7175c7 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/ScreenEffects.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/ScreenEffects.kt @@ -6,20 +6,6 @@ import com.droiddeck.launcher.wayland.WaylandCompositor import org.json.JSONObject import kotlin.math.abs -/** - * The compositor's post chain (waylandcomp/src/effects_chain.c) as the session menu drives it: - * the colour grade, the sharpening, the shader toggles and terminal debanding. Units are the - * menu's own, so a value goes straight from a row into the setter it drives: - * - * - [brightness] / [contrast] -100..100, 0 = neutral - * - [gamma] 0.5..3.0, 1.0 = neutral - * - [saturation] 0..200 percent, 100 = neutral - * - [casLevel] 0..100; the pass runs only while [cas] is on - * - [debandStrength] 0..200, 100 = one LSB - * - * Applies live: the compositor picks the change up on its next frame. While any pass is on, a - * zero-copy game goes through the compositor pass instead, which costs a little GPU time. - */ data class ScreenEffects( val brightness: Int = 0, val contrast: Int = 0, @@ -35,14 +21,11 @@ data class ScreenEffects( val crt: Boolean = false, val ntsc: Boolean = false, ) { - /** The sharpening level the picture gets: 0 while the pass is off, whatever its level is parked at. */ val effectiveCas: Int get() = if (cas) casLevel else 0 - /** Whether any pass runs, so the menu can say when the picture is untouched. */ val any: Boolean get() = brightness != 0 || contrast != 0 || abs(gamma - 1f) > 0.005f || saturation != 100 || effectiveCas > 0 || hdr || deband || fxaa || toon || crt || ntsc - /** Hands every pass to the compositor, with the Look's name for its log line. */ fun push(upscaler: Int) { WaylandCompositor.nativeSetCas(cas, casLevel) WaylandCompositor.nativeSetHdr(hdr) @@ -77,30 +60,19 @@ data class ScreenEffects( } } -/** - * One Look: a named set of values for the rows that already exist on the Display page. Picking - * a Look moves those rows; each stays adjustable afterwards, and the moment one moves the Look - * row reads Custom. [scalingMode] null leaves the scaling mode alone; otherwise it is a - * SessionPrefs.upscalerChoices mode the Look switches to. - */ data class Look( - /** The Look's id: what the row picks by and the compositor logs. */ val name: String, val effects: ScreenEffects, val scalingMode: Int? = null, - /** One line under the row while this Look is picked. */ val desc: String, - /** [name] and [desc] in the app's language, for the menu. */ @StringRes val label: Int, @StringRes val descRes: Int, ) object ScreenEffectLooks { - // Scaling modes as the compositor numbers them (WaylandCompositor.nativeSetUpscaler). private const val SCALING_NEAREST = 2 private const val SCALING_FSR = 4 - /** Index 0 is the neutral state a fresh session matches, so the row always names something. */ val LOOKS: List<Look> = listOf( Look("Off", ScreenEffects.OFF, desc = "Nothing applied.", label = R.string.frame_gen_off, descRes = R.string.look_off_desc), Look("Game Clarity", ScreenEffects(brightness = 2, contrast = 12, saturation = 108, cas = true, casLevel = 55), @@ -127,11 +99,6 @@ object ScreenEffectLooks { desc = "Toon outlines plus sharpening.", label = R.string.look_anime_edge, descRes = R.string.look_anime_edge_desc), ) - /** - * The Look the live values are, or null for Custom. A Look's sharpening is compared as the - * effective level, so a pass switched off matches a Look without one whatever its level is - * parked at; a Look with no scaling mode of its own matches under any. - */ fun match(e: ScreenEffects, upscaler: Int): Look? = LOOKS.firstOrNull { l -> val t = l.effects t.brightness == e.brightness && t.contrast == e.contrast && abs(t.gamma - e.gamma) < 0.005f && diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt b/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt index 76adb4bf2..39ad3f2ab 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt @@ -2,14 +2,7 @@ package com.droiddeck.launcher.gpu import android.os.Build -/** - * Which chip this is, as a person would say it. Handhelds often leave Android's SoC model blank - * (the AYANEO Pocket FIT does), so the model is looked for where vendors put it, the same order - * ClusterTune uses; then it is named from a table of models we are sure of, or from the platform - * code name as a family ("Snapdragon 8 Gen 3 family") when the model itself is a vendor variant. - */ object SocNames { - /** Model codes whose product name is certain. */ private val MODELS = mapOf( "SM8150" to "Snapdragon 855", "SM8250" to "Snapdragon 865", "SM8250-AC" to "Snapdragon 870", "SM8350" to "Snapdragon 888", "SM8450" to "Snapdragon 8 Gen 1", "SM8475" to "Snapdragon 8+ Gen 1", @@ -19,27 +12,22 @@ object SocNames { "SM6375" to "Snapdragon 695", ) - /** Platform code names: every chip of one platform shares its CPU and GPU design. */ private val PLATFORMS = mapOf( "msmnile" to "Snapdragon 855/860", "kona" to "Snapdragon 865/870", "lahaina" to "Snapdragon 888", "taro" to "Snapdragon 8 Gen 1", "cape" to "Snapdragon 8+ Gen 1", "kalama" to "Snapdragon 8 Gen 2", "pineapple" to "Snapdragon 8 Gen 3", "sun" to "Snapdragon 8 Elite", ) - /** The chip's model code ("QCS8550", "SG8350P"), or "" when the phone does not say. */ fun model(prop: (String) -> String = GpuInfo::systemProperty): String { val sdk = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL.takeIf { it != Build.UNKNOWN }.orEmpty() else "" return listOf(prop("ro.soc.model"), prop("ro.vendor.qti.soc_model"), sdk, prop("ro.fota.platform")) .map { it.trim() } - // ro.fota.platform is often a branch name ("MSM_13.0"), not a chip. .firstOrNull { it.isNotEmpty() && !Regex("(?i)^msm_\\d").containsMatchIn(it) }.orEmpty() } - /** "Snapdragon 8 Gen 2", "Snapdragon 8 Gen 3 family", or null when neither the model nor the platform is known. */ fun name(model: String, platform: String = GpuInfo.systemProperty("ro.board.platform")): String? = MODELS[model.uppercase()] ?: PLATFORMS[platform.lowercase()]?.let { "$it family" } - /** For the GPU card: "Snapdragon 8 Gen 2 (QCS8550)", the bare model, or "". */ fun label(model: String = model(), platform: String = GpuInfo.systemProperty("ro.board.platform")): String { val name = name(model, platform) return when { diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/SteamVr.kt b/app/src/main/java/com/droiddeck/launcher/gpu/SteamVr.kt index c592b2604..93d71262c 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/SteamVr.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/SteamVr.kt @@ -4,18 +4,10 @@ import android.content.Context import com.droiddeck.launcher.agent.AgentGuest import org.json.JSONObject -/** - * Cardboard mode's SteamVR (docs/cardboard-vr.md): Valve's ARM64 SteamVR in the guest, rendering - * through the DroidDeck driver as a side-by-side pair that the Cardboard pass puts the lens on. Driven - * through the guest agent's fixed "vr" request (droiddeck-steamvr), so it needs no agent commands. - * Every call blocks on the guest: run them off the main thread. - */ object SteamVr { enum class Status { - /** No session, no agent, or a runtime without the helper or the driver. */ UNAVAILABLE, NOT_INSTALLED, - /** Downloading or unpacking (571 MB). */ INSTALLING, READY, RUNNING, @@ -25,24 +17,17 @@ object SteamVr { parse(AgentGuest.call(context, JSONObject().put("kind", "vr").put("action", "status"), STATUS_TIMEOUT_MS)) }.getOrDefault(Status.UNAVAILABLE) - /** install, start or stop; true when the guest did it. */ fun run(context: Context, action: String): Boolean = runCatching { require(action in setOf("install", "start", "stop")) succeeded(action, AgentGuest.call(context, JSONObject().put("kind", "vr").put("action", action), ACTION_TIMEOUT_MS)) }.getOrDefault(false) - /** - * The agent answers ok whenever it ran the request, even when droiddeck-steamvr then failed; the - * helper's own exit code says whether start or stop worked. install runs detached and only reports - * that it started (its progress shows in status). - */ fun succeeded(action: String, answer: JSONObject): Boolean = when { !answer.optBoolean("ok", false) -> false action == "install" -> answer.optBoolean("started", false) else -> answer.optInt("exitCode", -1) == 0 } - /** The agent's status answer -> what the menu shows. */ fun parse(answer: JSONObject): Status = when { !answer.optBoolean("ok", false) -> Status.UNAVAILABLE answer.optInt("exitCode", 0) != 0 -> Status.UNAVAILABLE diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java index c125b531c..700ec81c3 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java @@ -24,43 +24,17 @@ import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -/** - * The Vulkan driver the in-app compositor runs on. It has to be Turnip: the system Adreno driver - * does not implement VK_EXT_image_drm_format_modifier, so importing the dma-bufs gamescope hands - * over fails and the session renders nothing. Two builds ship in the apk, one per Adreno - * generation, and the GPU decides which is unpacked - unless the user has imported an AdrenoTools - * zip of their own and chosen it, which then wins. - * - * <p>The guest's own Turnip is a different copy entirely - a glibc build inside the rootfs, or an - * imported one ({@link LinuxVulkanDriverManager}). This one is the bionic build the app process - * loads through adrenotools, and it is what every session ends in: whatever draws inside the - * runtime, this is the driver that puts the frame on the panel. - * - * <p>Every driver, bundled or imported, lives under {@code files/graphics_driver/<id>/} with a - * {@code meta.json} whose {@code libraryName} names the .so beside it - the AdrenoTools layout. - * The import path is ported from Bannerlator's {@code AdrenotoolsManager} (GPL-3.0), including - * the refusal of zips that belong in the Linux runtime list: a "-Linux" (glibc) Turnip names no - * library on purpose, and installed here it would put the compositor on the system Vulkan - a - * black screen wearing a driver's name. - */ public final class TurnipDriver { private static final String TAG = "TurnipDriver"; - /** Pinned A6xx/A7xx upstream build; used as the A6xx offline fallback. */ private static final String DRIVER_A6XX = "turnip26.3.0-a6xx"; - /** Adreno 7xx (8 Gen 2/3, e.g. the Pocket FIT's A750). */ private static final String DRIVER_A7XX = "turnip25.1.0"; - /** Adreno 8xx (8 Elite, e.g. a Fold 8's A830). */ private static final String DRIVER_A8XX = "turnip-sdk36"; - /** The two the apk carries; never removable, unpacked on demand. */ public static final List<String> BUNDLED = Collections.unmodifiableList(Arrays.asList( DRIVER_A7XX, DRIVER_A8XX, com.droiddeck.launcher.session.QuestExperimental.DRIVER, DRIVER_A6XX, com.droiddeck.launcher.session.QuestExperimental.DRIVER_A6XX)); - /** Overrides the pick, for a device we cannot reach: a7xx, a8xx or system. */ private static final String OVERRIDE_FILE = "Download/droiddeck-driver"; - /** Bundled drivers the user deleted: hidden from the list, their unpacked copy removed. */ private static final String PREFS = "graphics_driver"; private static final String KEY_HIDDEN = "hiddenBundled"; - /** Stored choice meaning "pick by GPU" - the default, and what a removed import falls back to. */ public static final String AUTO = ""; public static final String HELP_TEXT = @@ -78,7 +52,6 @@ public final class TurnipDriver { this.contentDir = new File(context.getFilesDir(), "graphics_driver"); } - /** Absolute directory holding the driver, with a trailing slash - adrenotools wants both. */ public String driverPath(String driverId) { return new File(contentDir, driverId).getAbsolutePath() + "/"; } @@ -88,7 +61,6 @@ public final class TurnipDriver { return meta != null ? meta.optString("libraryName", null) : null; } - /** meta.json's "name", or the id when it has none. */ public String displayName(String driverId) { JSONObject meta = readMeta(driverId); String name = meta != null ? meta.optString("name", "") : ""; @@ -113,17 +85,11 @@ public final class TurnipDriver { } } - /** An imported driver is installed when its meta.json names a library that is there. */ public boolean isInstalled(String driverId) { String library = libraryName(driverId); return library != null && !library.isEmpty() && new File(new File(contentDir, driverId), library).isFile(); } - /** - * The bundled drivers the list shows: {@link #BUNDLED} minus the ones the user deleted. A deleted - * one still lives in the apk and Auto still unpacks it on a GPU that needs it, so deleting can - * never leave a device without a driver; it only leaves the list. - */ public List<String> visibleBundled() { java.util.Set<String> hidden = hiddenBundled(); ArrayList<String> out = new ArrayList<>(); @@ -136,7 +102,6 @@ public final class TurnipDriver { .getStringSet(KEY_HIDDEN, java.util.Collections.<String>emptySet())); } - /** "Delete" for a bundled driver: remove its unpacked copy and hide it from the list. */ public void hideBundled(String driverId) { if (!BUNDLED.contains(driverId)) return; FileUtils.delete(new File(contentDir, driverId)); @@ -150,7 +115,6 @@ public final class TurnipDriver { context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().remove(KEY_HIDDEN).apply(); } - /** Imported drivers only - the bundled two are listed by {@link #BUNDLED}. */ public List<String> enumerateImported() { ArrayList<String> ids = new ArrayList<>(); File[] dirs = contentDir.listFiles(); @@ -163,7 +127,6 @@ public final class TurnipDriver { return ids; } - /** The bundled build the GPU would pick on its own, for the "Auto" row's label. */ public String autoId() { String id = chooseByDevice(); return id == null ? "system" : id; @@ -177,12 +140,6 @@ public final class TurnipDriver { if (driverId.equals(SessionPrefs.androidDriver(context))) SessionPrefs.setAndroidDriver(context, AUTO); } - /** - * Import an AdrenoTools zip. Returns the new driver id. - * @throws IllegalArgumentException with a user-facing reason when the zip is not an AdrenoTools - * driver - including when it is a Linux runtime driver, which says which list it belongs in; - * {@link IOException} on read/extract failures. - */ public String installFromZip(Uri zipUri, String displayName) throws IOException { File tmpDir = newStagingDir(); boolean keep = false; @@ -193,8 +150,6 @@ public final class TurnipDriver { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { if (entry.isDirectory()) continue; - // Flatten: adrenotools reads the .so beside meta.json, and a base name also - // defeats zip-slip paths. String base = new File(entry.getName()).getName(); if (base.isEmpty()) continue; Files.copy(zis, new File(tmpDir, base).toPath(), StandardCopyOption.REPLACE_EXISTING); @@ -208,7 +163,6 @@ public final class TurnipDriver { } } - /** An empty directory beside the installed drivers, for a driver's files before {@link #adopt}. */ File newStagingDir() throws IOException { if (!contentDir.exists()) contentDir.mkdirs(); File tmpDir = new File(contentDir, ".tmp-" + System.nanoTime()); @@ -217,10 +171,6 @@ public final class TurnipDriver { return tmpDir; } - /** - * Check a staged driver (meta.json and its library, flat) and move it in as a new driver. - * Returns the id; the caller deletes {@code tmpDir} when this throws. - */ String adopt(File tmpDir, String displayName) throws IOException { String rejected = rejectionReason(tmpDir); if (rejected != null) throw new IllegalArgumentException(context.getString(R.string.turnip_not_adrenotools, rejected)); @@ -246,10 +196,6 @@ public final class TurnipDriver { } } - /** - * Why a just-extracted zip is not something adrenotools can be pointed at, or null when it is. - * The kinds refused by name are the other driver lists' zips, which carry a meta.json too. - */ private String rejectionReason(File dir) { File metaFile = new File(dir, "meta.json"); if (!metaFile.isFile()) return context.getString(R.string.turnip_reject_no_meta); @@ -272,7 +218,6 @@ public final class TurnipDriver { return context.getString(R.string.turnip_reject_library_missing, libraryName); if (!LinuxVulkanDriverManager.isAarch64Elf(library)) return context.getString(R.string.turnip_reject_not_aarch64, libraryName); - // A glibc build that happens to carry a libraryName would load nothing in this process. if (LinuxVulkanDriverManager.containsAscii(library, "libc.so.6")) return context.getString(R.string.turnip_reject_glibc, libraryName); return null; @@ -285,11 +230,6 @@ public final class TurnipDriver { return id; } - /** - * Unpacks the driver this device needs and returns its id, or null to fall back to the system - * Vulkan loader (which means a black session on Adreno, but is better than refusing to start - * on a GPU neither build covers). - */ public String install() { String id = choose(); DriverSetupLog.write(context, "install.begin driver=" + id); @@ -314,11 +254,6 @@ public final class TurnipDriver { return id; } - /** - * The user's choice first - an imported driver, or one of the bundled two pinned by hand - - * then the Downloads override, then the GPU. A chosen import that has since been removed logs - * and falls through, so a stale preference never blocks a session. - */ private String choose() { String chosen = SessionPrefs.androidDriver(context); if (chosen != null && !chosen.isEmpty()) { @@ -349,7 +284,6 @@ public final class TurnipDriver { String model = gpuModel(); Log.i(TAG, "gpu model: " + (model == null ? "unknown" : model)); if (model != null) { - // "Adreno750", "adreno_830" - the generation is the first digit of the three. java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d)\\d\\d").matcher(model); if (m.find()) { char generation = m.group(1).charAt(0); @@ -358,11 +292,9 @@ public final class TurnipDriver { if (generation == '6') return DRIVER_A6XX; } } - // Nothing to go on: Android 16 shipped with the 8 Elite, so treat a new device as 8xx. return android.os.Build.VERSION.SDK_INT >= 36 ? DRIVER_A8XX : DRIVER_A7XX; } - /** KGSL names the GPU here, and this file is world-readable where /dev/kgsl-3d0 is not. */ private static String gpuModel() { for (String path : new String[]{ "/sys/class/kgsl/kgsl-3d0/gpu_model", diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt index acc941928..2523afb20 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt @@ -12,41 +12,20 @@ import java.io.IOException import java.net.HttpURLConnection import java.net.URL -/** - * Driver downloads from the Turnip release repos, offered in both driver menus. Checked only when - * the user taps refresh - the result is remembered, so the menu shows the last check without going - * online. A download goes through the same importer as a zip picked by hand, so it gets the same - * checks and lands as an ordinary imported driver. - * - * Each repo's recent releases are read newest first and every driver variant is offered from the - * newest release that carries it: a repo that stops publishing one variant (Banners-Turnip's A8xx - * Linux build after v26.3.0-20260923) still offers its last one, and a repo with two release lines - * (WinNative's Android `v1.x` and Linux `linux-v0.x`) offers the newest of each. - */ object TurnipReleases { private const val PREFS = "turnip_releases" private const val KEY_RELEASE = "latest" private const val KEY_DOWNLOADS = "downloads" - /** [label] names the variant for the menu: the GPUs it is for, and a build flavour. */ - /** [sha256] is the release asset's digest from GitHub; empty for a list cached before it was kept. */ - /** - * [pair] names the matched set an asset belongs to (DriverPairs): a display build and a Linux - * build of the same pair are made to run together. "" for a list cached before pairs were kept. - */ - /** [bundle]: one zip holding both halves of the pair (DriverBundle); [linux] is false for it. */ class Asset(val source: String, val tag: String, val name: String, val url: String, val size: Long, val linux: Boolean, val label: String, val sha256: String = "", val pair: String = "", val bundle: Boolean = false) - /** [latest] = each source's newest release tag, for the refresh line. */ class Check(val assets: List<Asset>, val latest: List<Pair<String, String>>, val failed: List<String>, val checkedAt: Long) - /** What an asset is: a Linux build or not, the menu label, and the pair it belongs to. */ private class Kind(val linux: Boolean, val label: String, val pair: String, val bundle: Boolean = false) private class Source(val label: String, val repo: String, val classify: (name: String, tag: String) -> Kind?) private val SOURCES = listOf( - // Turnip-<tag>[-variant][-Linux|-Wayland].zip; -Wayland is for a path this app does not have. Source("Banners-Turnip", "The412Banner/Banners-Turnip") { name, tag -> val prefix = "Turnip-$tag" if (!name.startsWith(prefix) || !name.endsWith(".zip")) return@Source null @@ -63,32 +42,23 @@ object TurnipReleases { } Kind(linux, label, "banner" + variant.lowercase()) }, - // WN-Turnip-<ver>-<b|p>_Axxx.zip (Android) and WN-Linux-Turnip-<ver>-<b|p>_Axxx.zip (Linux): - // b = Balanced, p = Performance, Axxx = every Adreno. Source("WinNative", "WinNative-Emu/Drivers") { name, _ -> val m = Regex("""^WN-(Linux-)?Turnip-[^-]+-([a-z]+)_(\w+)\.zip$""").find(name) ?: return@Source null val flavour = when (m.groupValues[2]) { "b" -> "Balanced"; "p" -> "Performance"; else -> m.groupValues[2] } val gpus = if (m.groupValues[3] == "Axxx") "all Adreno" else m.groupValues[3] Kind(m.groupValues[1].isNotEmpty(), "$gpus · $flavour", "wn-" + m.groupValues[2]) }, - // DD-Turnip-v<ver>.zip: DroidDeck's own all-in-one build, both halves in one zip, published - // to Drivers-CI (releases only). Every release is offered, not just the newest, each its own pair. Source("DroidDeck", "Droid-Deck/Drivers-CI") { name, tag -> if (!Regex("""^DD-Turnip-v\d+\.\d+\.\d+\.zip$""").matches(name)) return@Source null Kind(false, "Android + Linux · all Adreno", DriverPairs.ddTurnip(tag), bundle = true) }, ) - /** The result of the last check, or null when the user has never checked. */ fun cached(context: Context): Check? { val raw = prefs(context).getString(KEY_RELEASE, null) ?: return null return runCatching { parse(JSONObject(raw)) }.getOrNull() } - /** - * Read every source now and remember the result. A source that fails is named in [Check.failed] - * and the others still count; throws only when every source failed. - */ fun refresh(context: Context): Check { val assets = JSONArray() val latest = JSONArray() @@ -109,13 +79,10 @@ object TurnipReleases { val a = list.getJSONObject(j) val name = a.getString("name") val kind = src.classify(name, tag) ?: continue - // A bundle is only offered from an official release, never a pre-release. if (kind.bundle && r.optBoolean("prerelease")) continue val linux = kind.linux val label = kind.label - // Only assets GitHub has a sha256 for are offered: the download is checked against it. val sha = Hashes.githubSha256(a.optString("digest")) ?: continue - // Newest first: the first release carrying a variant is the one offered. if (!seen.add(if (kind.bundle) kind.pair else "$linux|$label")) continue assets.put(JSONObject().put("source", src.label).put("tag", tag).put("name", name) .put("url", a.getString("browser_download_url")).put("size", a.optLong("size")) @@ -170,11 +137,9 @@ object TurnipReleases { return Check(out, latest, failed, json.optLong("checkedAt")) } - /** The id an asset was installed as, when it was downloaded before and is still installed. */ fun installedId(context: Context, asset: Asset, isInstalled: (String) -> Boolean): String? = downloads(context).optString(asset.name, "").takeIf { it.isNotEmpty() && isInstalled(it) } - /** True when [id] came from a release download rather than a zip picked by hand. */ fun isDownloaded(context: Context, id: String): Boolean { val d = downloads(context) return d.keys().asSequence().any { d.optString(it) == id } @@ -184,7 +149,6 @@ object TurnipReleases { prefs(context).edit().putString(KEY_DOWNLOADS, downloads(context).put(asset.name, id).toString()).apply() } - /** Forget a deleted driver, so its release offers the download again. */ fun forget(context: Context, id: String) { val d = downloads(context) val keys = d.keys().asSequence().filter { d.optString(it) == id }.toList() @@ -193,7 +157,6 @@ object TurnipReleases { prefs(context).edit().putString(KEY_DOWNLOADS, d.toString()).apply() } - /** Download an asset into the cache; the caller imports it and deletes the file. */ fun download(context: Context, asset: Asset, progress: (Int) -> Unit): File { if (asset.sha256.isEmpty()) throw IOException(context.getString(R.string.turnip_list_predates_checksums)) val target = File(context.cacheDir, asset.name) diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/VulkanInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/VulkanInfo.kt index ee4ca1a69..d03e6a5c6 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/VulkanInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/VulkanInfo.kt @@ -2,17 +2,11 @@ package com.droiddeck.launcher.gpu import android.util.Log -/** - * The phone's own Vulkan driver, as it describes itself (native/deviceinfo): device name, API and - * driver versions, a few limits and the extensions DXVK/VKD3D lean on. Asked once per process and - * kept; the first [query] creates a Vulkan instance, so it belongs off the main thread. - */ object VulkanInfo { @Volatile private var cached: Map<String, String>? = null @JvmStatic private external fun nativeQuery(): String - /** Blocking on first use. Empty-ish map with an "error" key when Vulkan cannot be asked. */ @Synchronized fun query(): Map<String, String> { cached?.let { return it } @@ -31,6 +25,5 @@ object VulkanInfo { return map } - /** What [query] found, if it has run in this process; never blocks. */ fun cachedOrNull(): Map<String, String>? = cached } diff --git a/app/src/main/java/com/droiddeck/launcher/input/ControllerPrefs.kt b/app/src/main/java/com/droiddeck/launcher/input/ControllerPrefs.kt index 739f83d3f..09b282d8a 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/ControllerPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/ControllerPrefs.kt @@ -123,7 +123,6 @@ object ControllerPrefs { prefs(context).edit().putBoolean("keyboardButton", on).apply() } - /** The physical Select button opens the Steam menu instead of View/Select. */ fun setSelectSteam(context: Context, on: Boolean) { prefs(context).edit().putBoolean("selectSteam", on).apply() } @@ -151,7 +150,6 @@ object ControllerPrefs { } fun resetAll(context: Context) { - // On older Android versions clear() does not notify listeners; restore rumble explicitly. prefs(context).edit().clear().putBoolean("rumble", true).apply() } diff --git a/app/src/main/java/com/droiddeck/launcher/input/DeckControls.kt b/app/src/main/java/com/droiddeck/launcher/input/DeckControls.kt index 23146be37..a8c499793 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/DeckControls.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/DeckControls.kt @@ -1,15 +1,6 @@ package com.droiddeck.launcher.input -/** - * The Steam Deck controller's back grips and trackpads, which a handheld's own pad does not have: - * the second screen's Deck controls (DeckControlsPanel) drive them, and each change goes into the - * pad's ring ([FakeInputWriter.writeDeckControls]) for libfakeinput's Deck report. - * - * Trackpad positions are -1..1 each way with y up, as on a Deck; a pad touched reports its - * position, a pad clicked also reports full pressure. - */ object DeckControls { - /** Bits of the ring's controls word - MUST match DECK_EXTRA_* in fakeinput_steam.cpp. */ const val L4 = 1 const val R4 = 2 const val L5 = 4 @@ -19,7 +10,6 @@ object DeckControls { private const val LEFT_CLICK = 64 private const val RIGHT_CLICK = 128 - /** The pad the Deck controller is made from (PadBridge's slot). */ private const val SLOT = 0 private var controls = 0 @@ -32,7 +22,6 @@ object DeckControls { publish() } - /** A finger on (or off) one trackpad at x, y in -1..1, y up. */ @Synchronized fun setPad(right: Boolean, touching: Boolean, x: Float = 0f, y: Float = 0f) { val at = if (right) 2 else 0 @@ -49,7 +38,6 @@ object DeckControls { publish() } - /** Everything let go - for when the panel goes away with a finger still on it. */ @Synchronized fun releaseAll() { if (controls == 0 && pads.all { it.toInt() == 0 } && pressure.all { it.toInt() == 0 }) return diff --git a/app/src/main/java/com/droiddeck/launcher/input/EvdevKeys.java b/app/src/main/java/com/droiddeck/launcher/input/EvdevKeys.java index 23aae64be..48f9ea30d 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/EvdevKeys.java +++ b/app/src/main/java/com/droiddeck/launcher/input/EvdevKeys.java @@ -2,21 +2,11 @@ package com.droiddeck.launcher.input; import android.view.KeyEvent; -/** - * Android key codes to Linux evdev codes, for the keys we inject into the compositor's - * wl_keyboard. A hardware keyboard is how an account name and password are typed on the client's - * first run, so this matters before any game does. - * - * <p>evdev's order is not alphabetical: the numbers are the physical positions of a PC keyboard, - * which is what the compositor's xkb keymap is written against. Table taken from Bannerlator's - * Wayland path unchanged. - */ public final class EvdevKeys { private EvdevKeys() {} public static int fromKeyCode(int kc) { switch (kc) { - // Letters (evdev order is NOT alphabetical) case KeyEvent.KEYCODE_A: return 30; case KeyEvent.KEYCODE_B: return 48; case KeyEvent.KEYCODE_C: return 46; case KeyEvent.KEYCODE_D: return 32; case KeyEvent.KEYCODE_E: return 18; case KeyEvent.KEYCODE_F: return 33; @@ -30,36 +20,30 @@ public final class EvdevKeys { case KeyEvent.KEYCODE_U: return 22; case KeyEvent.KEYCODE_V: return 47; case KeyEvent.KEYCODE_W: return 17; case KeyEvent.KEYCODE_X: return 45; case KeyEvent.KEYCODE_Y: return 21; case KeyEvent.KEYCODE_Z: return 44; - // Digit row case KeyEvent.KEYCODE_1: return 2; case KeyEvent.KEYCODE_2: return 3; case KeyEvent.KEYCODE_3: return 4; case KeyEvent.KEYCODE_4: return 5; case KeyEvent.KEYCODE_5: return 6; case KeyEvent.KEYCODE_6: return 7; case KeyEvent.KEYCODE_7: return 8; case KeyEvent.KEYCODE_8: return 9; case KeyEvent.KEYCODE_9: return 10; case KeyEvent.KEYCODE_0: return 11; - // Whitespace / edit case KeyEvent.KEYCODE_ENTER: return 28; case KeyEvent.KEYCODE_NUMPAD_ENTER: return 28; case KeyEvent.KEYCODE_SPACE: return 57; case KeyEvent.KEYCODE_TAB: return 15; - case KeyEvent.KEYCODE_DEL: return 14; /* backspace */ + case KeyEvent.KEYCODE_DEL: return 14; case KeyEvent.KEYCODE_FORWARD_DEL: return 111; case KeyEvent.KEYCODE_ESCAPE: return 1; - // Modifiers case KeyEvent.KEYCODE_SHIFT_LEFT: return 42; case KeyEvent.KEYCODE_SHIFT_RIGHT: return 54; case KeyEvent.KEYCODE_CTRL_LEFT: return 29; case KeyEvent.KEYCODE_CTRL_RIGHT: return 97; case KeyEvent.KEYCODE_ALT_LEFT: return 56; case KeyEvent.KEYCODE_ALT_RIGHT: return 100; case KeyEvent.KEYCODE_CAPS_LOCK: return 58; - // Arrows / nav case KeyEvent.KEYCODE_DPAD_UP: return 103; case KeyEvent.KEYCODE_DPAD_DOWN: return 108; case KeyEvent.KEYCODE_DPAD_LEFT: return 105; case KeyEvent.KEYCODE_DPAD_RIGHT: return 106; case KeyEvent.KEYCODE_MOVE_HOME: return 102; case KeyEvent.KEYCODE_MOVE_END: return 107; case KeyEvent.KEYCODE_PAGE_UP: return 104; case KeyEvent.KEYCODE_PAGE_DOWN: return 109; case KeyEvent.KEYCODE_INSERT: return 110; - // Punctuation case KeyEvent.KEYCODE_GRAVE: return 41; case KeyEvent.KEYCODE_MINUS: return 12; case KeyEvent.KEYCODE_EQUALS: return 13; case KeyEvent.KEYCODE_LEFT_BRACKET: return 26; case KeyEvent.KEYCODE_RIGHT_BRACKET: return 27; case KeyEvent.KEYCODE_BACKSLASH: return 43; case KeyEvent.KEYCODE_SEMICOLON: return 39; case KeyEvent.KEYCODE_APOSTROPHE: return 40; case KeyEvent.KEYCODE_SLASH: return 53; case KeyEvent.KEYCODE_COMMA: return 51; case KeyEvent.KEYCODE_PERIOD: return 52; - // Function row case KeyEvent.KEYCODE_F1: return 59; case KeyEvent.KEYCODE_F2: return 60; case KeyEvent.KEYCODE_F3: return 61; case KeyEvent.KEYCODE_F4: return 62; case KeyEvent.KEYCODE_F5: return 63; case KeyEvent.KEYCODE_F6: return 64; @@ -70,23 +54,15 @@ public final class EvdevKeys { } } - /** - * The character a key carries when Shift is not held, for a character that needs it - so '@' - * gives '2' and 'A' gives 'a'. Returns 0 when the character is typed without Shift, which is - * also the answer for anything this layout does not place on a key. US layout, which is what - * the session's keymap is. - */ public static int unshiftedChar(int ch) { if (ch >= 'A' && ch <= 'Z') return Character.toLowerCase(ch); int at = SHIFTED_CHARS.indexOf(ch); return at >= 0 ? PLAIN_CHARS.charAt(at) : 0; } - // Index-aligned: the symbol, and the key it shares with Shift held. private static final String SHIFTED_CHARS = "!@#$%^&*()_+{}|:\"<>?~"; private static final String PLAIN_CHARS = "1234567890-=[]\\;',./`"; - /** The Android key code that carries an unshifted character, or 0 when nothing does. */ public static int keycodeForChar(int ch) { if (ch >= 'a' && ch <= 'z') return KeyEvent.KEYCODE_A + (ch - 'a'); if (ch >= '0' && ch <= '9') return KeyEvent.KEYCODE_0 + (ch - '0'); diff --git a/app/src/main/java/com/droiddeck/launcher/input/FakeInputRingPaths.java b/app/src/main/java/com/droiddeck/launcher/input/FakeInputRingPaths.java index 05e5fc306..d16aa0b53 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/FakeInputRingPaths.java +++ b/app/src/main/java/com/droiddeck/launcher/input/FakeInputRingPaths.java @@ -2,16 +2,11 @@ package com.droiddeck.launcher.input; import java.io.File; -/** - * Where the fake input rings live and how the guest is told about them. Kept apart from - * {@link FakeInputWriter}, which loads native code, so it can be tested on the JVM. - */ final class FakeInputRingPaths { static final String RING_DIR_NAME = "fakeinput-rings"; private FakeInputRingPaths() {} - /** The rings sit beside the fake evdev nodes: session/dev/input -> session/dev/fakeinput-rings. */ static File ringDir(File fakeInputDir) { if (fakeInputDir == null) { return null; @@ -26,13 +21,6 @@ final class FakeInputRingPaths { return ringDir != null ? new File(ringDir, "ring" + slot) : null; } - /** - * The ring's path as the guest opens it. The session tree is bound into the guest at its own - * host path, spelled the way {@code Context.getFilesDir()} spells it (LinuxRuntime.binds), so - * the export has to use that same spelling. A canonical path is not that: where - * /data/user/0 is a link, or a separate mount, onto /data/data (ColorOS 16, #166) it resolves - * to a prefix nothing is bound at, and every pad, physical and on-screen, goes dead. - */ static String exportPath(File ringFile) { return ringFile.getAbsolutePath(); } diff --git a/app/src/main/java/com/droiddeck/launcher/input/FakeInputWriter.java b/app/src/main/java/com/droiddeck/launcher/input/FakeInputWriter.java index ed4a88d27..f698f481d 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/FakeInputWriter.java +++ b/app/src/main/java/com/droiddeck/launcher/input/FakeInputWriter.java @@ -9,33 +9,17 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; -/** - * Fake evdev transport for the emulated Xbox 360 pads. - * - * <p>Replaces the old append-file transport (which desynced after ~15 min when an - * unlooped {@code channel.write} left a partial event in the file and the - * IOException was swallowed) with a fixed-size mmap RING BUFFER shared with the - * native reader in {@code fakeinput.cpp}. Delta events are published into the - * ring under a monotonic {@code write_seq}; an authoritative absolute-state - * snapshot is published alongside under a seqlock so the native reader can replay - * a full keyframe to heal any desync (open, ring overflow) without duplicate - * input. Ring layout (magic/version/offsets) is a paired ABI with the native - * {@code FakeInputRingHeader} - keep the two in lockstep. - */ public class FakeInputWriter { private static final String TAG = "FakeInputWriter"; - // Event types public static final short EV_SYN = 0x00; public static final short EV_KEY = 0x01; public static final short EV_ABS = 0x03; public static final short EV_MSC = 0x04; - // Event codes public static final short MSC_SCAN = 0x04; public static final short SYN_REPORT = 0x00; - // Xbox 360 controller button codes public static final short BTN_A = 0x130; public static final short BTN_B = 0x131; public static final short BTN_X = 0x133; @@ -48,7 +32,6 @@ public class FakeInputWriter { public static final short BTN_MODE = 0x13C; public static final short BTN_THUMBR = 0x13E; - // Absolute axis codes public static final short ABS_X = 0x00; public static final short ABS_Y = 0x01; public static final short ABS_RX = 0x03; @@ -58,17 +41,12 @@ public class FakeInputWriter { public static final short ABS_HAT0X = 0x10; public static final short ABS_HAT0Y = 0x11; - // Button mapping (bit i of the snapshot buttons word) private static final short[] BUTTON_MAP = { BTN_A, BTN_B, BTN_X, BTN_Y, BTN_TL, BTN_TR, BTN_SELECT, BTN_START, BTN_THUMBL, BTN_THUMBR, BTN_MODE }; - /** Bit 10 of the snapshot word. The first ten are PadState's own bit order; the Steam - * button is not, because PadState keeps 10 and 11 for the triggers. */ private static final int SNAPSHOT_IDX_MODE = 10; - /** Bit 11: the Quick Access button. Snapshot only - an Xbox pad's evdev node has no such key; - * libfakeinput's Deck controller reads it from here. */ private static final int SNAPSHOT_IDX_QAM = 11; private static final int EVENT_SIZE = 24; @@ -76,11 +54,10 @@ public class FakeInputWriter { private static final int BUFFER_SIZE = EVENT_SIZE * MAX_EVENTS_PER_UPDATE; private static final int MAX_FAKE_INPUT_SLOTS = 4; - // Ring layout - MUST match FakeInputRingHeader in fakeinput.cpp. private static final int RING_CAPACITY_EVENTS = 4096; private static final int RING_HEADER_SIZE = 64; private static final int RING_SIZE = RING_HEADER_SIZE + (RING_CAPACITY_EVENTS * EVENT_SIZE); - private static final int RING_MAGIC = 0x46494252; // FIBR + private static final int RING_MAGIC = 0x46494252; private static final int RING_VERSION = 2; private static final int RING_MAGIC_OFFSET = 0; private static final int RING_VERSION_OFFSET = 4; @@ -88,26 +65,18 @@ public class FakeInputWriter { private static final int RING_CAPACITY_OFFSET = 12; private static final int RING_WRITE_SEQ_OFFSET = 16; private static final int RING_GENERATION_OFFSET = 24; - // Authoritative absolute-state snapshot the native reader replays as a full - // keyframe to heal any delta-stream desync. Written under a seqlock - // (RING_SNAPSHOT_SEQ_OFFSET: odd = write in progress). private static final int RING_SNAPSHOT_SEQ_OFFSET = 32; private static final int RING_SNAPSHOT_BUTTONS_OFFSET = 40; - private static final int RING_SNAPSHOT_AXES_OFFSET = 44; // short[8] - // After the events - MUST match the blocks in fakeinput_steam.cpp. DeckImu carries the - // Deck-only motion, grips and trackpads. TritonShared carries the paired Steam Controller's - // native BLE reports and output reports, so the Steam client can see it as its own device. + private static final int RING_SNAPSHOT_AXES_OFFSET = 44; private static final int IMU_OFFSET = RING_SIZE; - private static final int IMU_MAGIC = 0x31554D49; // IMU1 - // Written by libfakeinput, not here: the client's SETTING_IMU_MODE | 0x80000000 (readImuMode). + private static final int IMU_MAGIC = 0x31554D49; private static final int IMU_MODE_OFFSET = IMU_OFFSET + 4; private static final int IMU_SEQ_OFFSET = IMU_OFFSET + 8; - private static final int IMU_ACCEL_OFFSET = IMU_OFFSET + 16; // short[3] - private static final int IMU_GYRO_OFFSET = IMU_OFFSET + 22; // short[3] - private static final int DECK_PADS_OFFSET = IMU_OFFSET + 28; // short[4]: left X, Y, right X, Y - private static final int DECK_PRESSURE_OFFSET = IMU_OFFSET + 36; // short[2]: left, right - private static final int DECK_CONTROLS_OFFSET = IMU_OFFSET + 40; // int: DeckControls bits - // Slot 0 only: what the pad is to the Steam client (libfakeinput's PAD_TYPE_*), | PAD_TYPE_SET. + private static final int IMU_ACCEL_OFFSET = IMU_OFFSET + 16; + private static final int IMU_GYRO_OFFSET = IMU_OFFSET + 22; + private static final int DECK_PADS_OFFSET = IMU_OFFSET + 28; + private static final int DECK_PRESSURE_OFFSET = IMU_OFFSET + 36; + private static final int DECK_CONTROLS_OFFSET = IMU_OFFSET + 40; private static final int PAD_TYPE_OFFSET = IMU_OFFSET + 44; private static final int PAD_TYPE_SET = 0x80000000; public static final int PAD_TYPE_DECK = 0; @@ -115,13 +84,10 @@ public class FakeInputWriter { public static final int PAD_TYPE_XBOX = 2; public static final int PAD_TYPE_DS4 = 3; public static final int PAD_TYPE_DUALSENSE = 4; - /** The type chosen this process, written into a ring made after the choice too; -1 for none. */ private static volatile int padType = -1; - /** The paired controller's state, likewise written into a ring made after it connected. */ private static volatile boolean tritonConnected = false; private static final int TRITON_OFFSET = RING_SIZE + 64; - private static final int TRITON_MAGIC = 0x4e525454; // TTRN - // Whether the paired controller is connected: libfakeinput offers its device only then. + private static final int TRITON_MAGIC = 0x4e525454; private static final int TRITON_CONNECTED_OFFSET = TRITON_OFFSET + 4; private static final int TRITON_INPUT_SEQ_OFFSET = TRITON_OFFSET + 8; private static final int TRITON_REPORT_ID_OFFSET = TRITON_OFFSET + 16; @@ -131,7 +97,6 @@ public class FakeInputWriter { private static final int TRITON_OUTPUT_FEATURE_OFFSET = TRITON_OFFSET + 96; private static final int TRITON_OUTPUT_LENGTH_OFFSET = TRITON_OFFSET + 100; private static final int TRITON_OUTPUT_OFFSET = TRITON_OFFSET + 104; - // A feature read: libfakeinput bumps the request, the reply names the request it answers. private static final int TRITON_GET_REQUEST_OFFSET = TRITON_OFFSET + 168; private static final int TRITON_GET_REPLY_OFFSET = TRITON_OFFSET + 176; private static final int TRITON_GET_REPORT_ID_OFFSET = TRITON_OFFSET + 184; @@ -163,9 +128,6 @@ public class FakeInputWriter { private int prevTriggerL, prevTriggerR; private int prevHatX, prevHatY; private boolean hasChanges = false; - // If a publish fails after prev* state has already advanced, the next frame - // must re-emit the whole state once; otherwise an unchanged control would stay - // silent and the ring snapshot would never catch up. private boolean pendingFullResend = false; private boolean forceResend = false; @@ -186,7 +148,6 @@ public class FakeInputWriter { boolean everActivated; } - // ---- Static ring management (shared by all writers + the env exporter) ---- public static void prepareRingSlots(File fakeInputDir, int slotCount) { int boundedSlotCount = Math.max(0, Math.min(slotCount, MAX_FAKE_INPUT_SLOTS)); @@ -197,10 +158,6 @@ public class FakeInputWriter { } } - /** - * Prepares every slot ring and returns the {@code slot=path;...} spec the native - * reader consumes via FAKE_EVDEV_MEMFD_PATHS. - */ public static String getRingEnv(File fakeInputDir) { synchronized (RING_LOCK) { prepareRingSlotsLocked(fakeInputDir, MAX_FAKE_INPUT_SLOTS); @@ -269,8 +226,6 @@ public class FakeInputWriter { data.putLong(TRITON_GET_REPLY_OFFSET, 0L); } - // Lock order: RING_LOCK, then the slot. Every writer holds the slot while it touches data, and - // re-checks it there: a writer that looked the slot up just before this ran finds it gone. private static void releaseRingSlotLocked(int slot) { RingSlot ringSlot = RING_SLOTS[slot]; if (ringSlot == null) { @@ -433,8 +388,6 @@ public class FakeInputWriter { return false; } - // Raw byte copy: endianness is irrelevant since we never interpret - // multi-byte fields out of the source here. ByteBuffer source = this.buffer.duplicate(); synchronized (ringSlot) { ByteBuffer ring = ringSlot.data; @@ -446,19 +399,12 @@ public class FakeInputWriter { while (source.remaining() >= EVENT_SIZE) { int eventIndex = (int) (writeSeq % RING_CAPACITY_EVENTS); int targetOffset = RING_HEADER_SIZE + (eventIndex * EVENT_SIZE); - // Bulk-copy one event (EVENT_SIZE bytes) instead of byte-by-byte. - // Bound the source to a single event, position the ring at the slot, - // then let put(ByteBuffer) move the whole block. Mutating the ring's - // Java position is harmless: the native reader uses a raw pointer, not - // this position. API-26 safe (no JDK 13+ absolute bulk put). source.limit(source.position() + EVENT_SIZE); ring.position(targetOffset); ring.put(source); source.limit(sourceLimit); writeSeq++; } - // Publish the resulting absolute state. prev* now hold the post-update - // values, i.e. exactly the state the events just written transition to. writeSnapshotLocked(ring); nativeStoreFence(); ring.putLong(RING_WRITE_SEQ_OFFSET, writeSeq); @@ -466,9 +412,6 @@ public class FakeInputWriter { return true; } - // Publishes the full absolute controller state for the native reader to replay - // as a keyframe. seqlock: bump to odd, write fields, bump to even, with - // store-store fences so the reader's acquire loads see consistent payloads. private void writeSnapshotLocked(ByteBuffer ring) { int buttons = 0; for (int i = 0; i < BUTTON_MAP.length; i++) { @@ -480,11 +423,9 @@ public class FakeInputWriter { buttons |= (1 << SNAPSHOT_IDX_QAM); } long seq = ring.getLong(RING_SNAPSHOT_SEQ_OFFSET); - ring.putLong(RING_SNAPSHOT_SEQ_OFFSET, seq + 1); // odd: write in progress + ring.putLong(RING_SNAPSHOT_SEQ_OFFSET, seq + 1); nativeStoreFence(); ring.putInt(RING_SNAPSHOT_BUTTONS_OFFSET, buttons); - // Axis order must match the native kSnapshotAxisCodes: - // X, Y, RX, RY, GAS(=triggerR), BRAKE(=triggerL), HAT0X, HAT0Y. ring.putShort(RING_SNAPSHOT_AXES_OFFSET, clampShort(this.prevThumbLX)); ring.putShort(RING_SNAPSHOT_AXES_OFFSET + 2, clampShort(this.prevThumbLY)); ring.putShort(RING_SNAPSHOT_AXES_OFFSET + 4, clampShort(this.prevThumbRX)); @@ -494,13 +435,9 @@ public class FakeInputWriter { ring.putShort(RING_SNAPSHOT_AXES_OFFSET + 12, clampShort(this.prevHatX)); ring.putShort(RING_SNAPSHOT_AXES_OFFSET + 14, clampShort(this.prevHatY)); nativeStoreFence(); - ring.putLong(RING_SNAPSHOT_SEQ_OFFSET, seq + 2); // even: write complete + ring.putLong(RING_SNAPSHOT_SEQ_OFFSET, seq + 2); } - /** - * Publishes the pad's motion for slot {@code slot}: accelerometer and gyro, already in the Deck - * controller's axes and units (see PadMotion). Safe from any thread. - */ public static void writeMotion(int slot, short[] accel, short[] gyro) { RingSlot ringSlot; synchronized (RING_LOCK) { @@ -511,7 +448,7 @@ public class FakeInputWriter { ByteBuffer ring = ringSlot.data; if (ring == null) return; long seq = ring.getLong(IMU_SEQ_OFFSET); - ring.putLong(IMU_SEQ_OFFSET, seq + 1); // odd: write in progress + ring.putLong(IMU_SEQ_OFFSET, seq + 1); nativeStoreFence(); for (int i = 0; i < 3; i++) { ring.putShort(IMU_ACCEL_OFFSET + i * 2, accel[i]); @@ -523,10 +460,6 @@ public class FakeInputWriter { } } - /** - * The motion the Steam client asked the Deck controller of slot {@code slot} for: its - * SETTING_IMU_MODE (0 = off), or -1 until it has said. Safe from any thread. - */ public static int readImuMode(int slot) { RingSlot ringSlot; synchronized (RING_LOCK) { @@ -540,11 +473,6 @@ public class FakeInputWriter { } } - /** - * Publishes the Deck's back grips and trackpads for slot {@code slot} (see DeckControls): - * {@code controls} is the DeckControls bit set, {@code pads} left X, Y, right X, Y and - * {@code pressure} left, right, already in the Deck's units. Safe from any thread. - */ public static void writeDeckControls(int slot, int controls, short[] pads, short[] pressure) { RingSlot ringSlot; synchronized (RING_LOCK) { @@ -566,20 +494,11 @@ public class FakeInputWriter { } } - /** - * Whether the pad, as the Steam client now sees it, has a Quick Access button: a Deck's or a - * Steam Controller's does, an Xbox or PlayStation pad's does not (libfakeinput drops the bit). - * Unset is the Deck, libfakeinput's default. - */ public static boolean padHasQam() { int type = padType; return type < 0 || type == PAD_TYPE_DECK || type == PAD_TYPE_STEAM; } - /** - * Switches what the pad is to the Steam client while the session runs ([PAD_TYPE_DECK], - * [PAD_TYPE_STEAM] or [PAD_TYPE_XBOX]); libfakeinput unplugs the old device and plugs in the new. - */ public static void setPadType(int type) { padType = type; RingSlot ringSlot; @@ -595,7 +514,6 @@ public class FakeInputWriter { } } - /** The paired Steam Controller came or went (SteamControllerBle): its device is plugged in or out. */ public static void setTritonConnected(int slot, boolean connected) { tritonConnected = connected; RingSlot ringSlot = tritonSlot(slot); @@ -608,7 +526,6 @@ public class FakeInputWriter { } } - /** Publishes a native Triton input report (without the HID report ID byte). */ public static void writeTritonState(int slot, int reportId, byte[] report) { if (report == null || report.length == 0 || report.length > TRITON_REPORT_MAX) return; RingSlot ringSlot; @@ -633,7 +550,6 @@ public class FakeInputWriter { } } - /** Returns the newest Steam-client output report after {@code lastSequence}, or null. */ public static byte[] readTritonOutput(int slot, long lastSequence) { RingSlot ringSlot; synchronized (RING_LOCK) { @@ -657,7 +573,6 @@ public class FakeInputWriter { } } - /** The Steam client's newest unanswered feature read, or 0: its request number. */ public static long readTritonFeatureRequest(int slot) { RingSlot ringSlot = tritonSlot(slot); if (ringSlot == null) return 0L; @@ -670,7 +585,6 @@ public class FakeInputWriter { } } - /** Answers feature read {@code request} with the controller's report, or a failure when null. */ public static void writeTritonFeatureReply(int slot, long request, byte[] report) { RingSlot ringSlot = tritonSlot(slot); if (ringSlot == null) return; @@ -748,11 +662,6 @@ public class FakeInputWriter { isOpen = false; } - /** - * Reset all input state to neutral (all buttons released, axes zeroed). - * This keeps the ring active so games that cache the file descriptor can still - * reconnect. - */ public synchronized void reset() { if (!isOpen && !open()) return; @@ -760,7 +669,6 @@ public class FakeInputWriter { buffer.clear(); hasChanges = false; - // Release all buttons for (int i = 0; i < BUTTON_MAP.length; i++) { if (prevButtonStates[i]) { prevButtonStates[i] = false; @@ -774,7 +682,6 @@ public class FakeInputWriter { hasChanges = true; } - // Zero all axes if (prevThumbLX != 0) { prevThumbLX = 0; writeEvent(EV_ABS, ABS_X, 0); @@ -823,9 +730,6 @@ public class FakeInputWriter { Log.i(TAG, "Soft released fake input: " + eventFile.getAbsolutePath()); } - /** - * Full destroy - reset, tear the ring generation down, and delete the discovery node. - */ public synchronized void destroy() { destroyed = true; reset(); @@ -861,9 +765,6 @@ public class FakeInputWriter { if (!isOpen && !open()) return; - // Keep the ring delta-first for latency. Full event frames are only used as - // a one-shot repair after a failed publish; normal open/overflow recovery is - // handled by the native snapshot keyframe. forceResend = pendingFullResend; pendingFullResend = false; if (forceResend) { @@ -873,13 +774,9 @@ public class FakeInputWriter { buffer.clear(); hasChanges = false; - // Buttons for (int i = 0; i < 10; i++) { writeButton(i, state.isDown(i)); } - // The Steam button. In a Linux session this is what opens the client's own in-game menu, - // and the client looks for it where an Xbox pad keeps it: evdev BTN_MODE, which SDL then - // reports as button 8 - exactly the "guide:b8" in the mapping Steam writes for this pad. writeButton(SNAPSHOT_IDX_MODE, state.isDown(PadState.GUIDE)); boolean qam = state.isDown(PadState.QAM); @@ -888,14 +785,11 @@ public class FakeInputWriter { hasChanges = true; } - // Sticks int lx = (int) (state.leftX * 32767); int ly = (int) (state.leftY * 32767); int rx = (int) (state.rightX * 32767); int ry = (int) (state.rightY * 32767); - // The fake evdev ring is event-queue semantics, so unchanged axes normally - // stay silent; forceResend overrides that to emit a complete keyframe. if (forceResend || lx != prevThumbLX) { prevThumbLX = lx; writeEvent(EV_ABS, ABS_X, lx); @@ -913,7 +807,6 @@ public class FakeInputWriter { writeEvent(EV_ABS, ABS_RY, ry); } - // L2 and R2 (Triggers) int tl = (int) (state.leftTrigger * 255); int tr = (int) (state.rightTrigger * 255); if (forceResend || tl != prevTriggerL) { @@ -925,7 +818,6 @@ public class FakeInputWriter { writeEvent(EV_ABS, ABS_GAS, tr); } - // D-pad int hatX = state.hatX(); int hatY = state.hatY(); if (forceResend || hatX != prevHatX) { @@ -937,13 +829,11 @@ public class FakeInputWriter { writeEvent(EV_ABS, ABS_HAT0Y, hatY); } - // Detect change else no need to write if (hasChanges) { writeEvent(EV_SYN, SYN_REPORT, 0); buffer.flip(); if (!flushBuffer()) { Log.e(TAG, "Gamepad write error: fake input mmap ring unavailable"); - // Couldn't publish; re-assert the whole state on the next frame. pendingFullResend = true; } } diff --git a/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt b/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt index b18279647..494533f43 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/KeyboardHost.kt @@ -13,17 +13,6 @@ import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import android.view.inputmethod.InputMethodManager -/** - * A place for the soft keyboard to land. Steam's UI and the games under it are X11 clients of - * gamescope and know nothing of Wayland text input, so everything the keyboard produces has to - * become key events: what the IME commits as text is turned into the key presses that would have - * typed it (Shift included, from the key character map), and handed to the activity, whose key - * path turns them into evdev codes for the compositor. Backspace, Enter and the keys an IME sends - * directly go the same way. - * - * Invisible and unfocusable until asked for: a focused View would take D-pad and stick motion - * away from the pad. - */ class KeyboardHost( context: Context, private val sendKeyEvent: (KeyEvent) -> Boolean, @@ -38,7 +27,7 @@ class KeyboardHost( init { isFocusable = false isFocusableInTouchMode = false - visibility = VISIBLE // present in the tree, draws nothing + visibility = VISIBLE layoutParams = android.widget.FrameLayout.LayoutParams(1, 1) } @@ -46,8 +35,6 @@ class KeyboardHost( override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? { if (!shown) return null - // No suggestions and no autocorrect: the keyboard sends what was pressed, key by key, which - // is what a password field on the other side needs. outAttrs.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD outAttrs.imeOptions = EditorInfo.IME_ACTION_NONE or EditorInfo.IME_FLAG_NO_FULLSCREEN or @@ -80,9 +67,6 @@ class KeyboardHost( private inner class KeyConnection : BaseInputConnection(this, false) { override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean { if (text.isNullOrEmpty()) return true - // The key events that type this text on a US layout - a capital arrives as Shift - // down, the letter, Shift up. Characters the map cannot place are sent as a bare - // key event carrying the character, which the activity's key path works back from. val events = map.getEvents(text.toString().toCharArray()) if (events != null) { for (event in events) sendKeyEvent(event) @@ -125,7 +109,6 @@ class KeyboardHost( override fun getSelectedText(flags: Int): CharSequence? = null } - /** A key event that carries a character the key map could not place. */ private class CharKeyEvent(origin: KeyEvent, private val ch: Char) : KeyEvent(origin) { override fun getUnicodeChar(): Int = ch.code override fun getUnicodeChar(metaState: Int): Int = ch.code diff --git a/app/src/main/java/com/droiddeck/launcher/input/OnScreenControls.kt b/app/src/main/java/com/droiddeck/launcher/input/OnScreenControls.kt index 8c4de8858..d7ac2259f 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/OnScreenControls.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/OnScreenControls.kt @@ -183,7 +183,6 @@ class OnScreenControls( relayout() } - /** Hide the full touch pad for an unobstructed screen while keeping its quick toggle available. */ fun setQuickHidden(hidden: Boolean) { if (buttonsOnly || editing || quickHidden == hidden) return if (hidden) releaseAll() diff --git a/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java b/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java index 78822422e..6348f58de 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java +++ b/app/src/main/java/com/droiddeck/launcher/input/PadBridge.java @@ -13,27 +13,8 @@ import java.io.File; import java.util.Arrays; import java.util.concurrent.atomic.AtomicIntegerArray; -/** - * A physical controller, republished as a synthetic evdev device the Steam client can see. - * - * <p>Android hands a gamepad to the foreground activity as key and motion events; the Steam client - * inside the runtime never sees that. What it does scan is {@code /dev/input}, which the session's - * interposer (libfakeinput.so) serves out of the shared-memory rings written here. So the path is: - * Android event → {@link PadState} → {@link FakeInputWriter} ring → interposer → evdev node → - * SDL → Big Picture. - * - * <p>The identity the interposer reports is an Xbox 360 pad, which is what makes SDL apply a known - * mapping without the user configuring anything. In a Steam session it is a Steam Deck controller - * to the client instead ({@link com.droiddeck.launcher.session.SteamDeckPad}), which has a Quick - * Access button of its own; an Xbox pad has none, so there the client's menu is reached with the - * Guide-then-A chord the client itself answers. - * - * <p>Each further controller is another player: slots 1 to 3, an Xbox 360 pad each, which in a - * Steam session only the client reads (libfakeinput) and hands games through Steam Input. - */ public final class PadBridge { private static final String TAG = "PadBridge"; - /** Player slots: the session prepares a ring for each (SessionService). */ public static final int SLOTS = 4; private static final float DEAD_ZONE = 0.12f; private static final int[] JOYSTICK_AXES = { @@ -48,18 +29,11 @@ public final class PadBridge { KeyEvent.KEYCODE_BUTTON_THUMBL, KeyEvent.KEYCODE_BUTTON_THUMBR, KeyEvent.KEYCODE_BUTTON_MODE, KeyEvent.KEYCODE_BUTTON_L2, KeyEvent.KEYCODE_BUTTON_R2 }; - // The client takes A as part of the chord only once it has had Guide held for a while, and a - // client starved of CPU needs longer. Too short, and it acts on A as well, selecting whatever it - // had focused before opening QAM: with 80 ms of lead that was 3 times in 20 at rest, and 250 ms - // none in 20; with the session down to 1 fps a fixed 400 ms still let 3 in 15 through, where - // 1000 ms let none. The lead keeps 80 ms when frames are quick, for a QAM that feels immediate, - // and stretches to eight frames when they are slow. private static final long QAM_GUIDE_LEAD_MIN_MS = 80; private static final long QAM_GUIDE_LEAD_MAX_MS = 1500; private static final int QAM_GUIDE_LEAD_FRAMES = 8; private static final long QAM_A_HOLD_MS = 200; private static final long QAM_GUIDE_TAIL_MS = 200; - // Off the main thread, so a busy UI cannot shorten or stretch the steps. private static final Handler chordHandler; static { android.os.HandlerThread thread = new android.os.HandlerThread("qam-chord", android.os.Process.THREAD_PRIORITY_DISPLAY); @@ -70,7 +44,6 @@ public final class PadBridge { private final File fakeInputDir; private final FakeInputWriter writer; private final PadState state = new PadState(); - /** The input device id holding each slot, or {@link #NO_CONTROLLER}. */ private final int[] owners = new int[SLOTS]; private final FakeInputWriter[] playerWriters = new FakeInputWriter[SLOTS]; private final PadState[] playerStates = new PadState[SLOTS]; @@ -82,16 +55,11 @@ public final class PadBridge { private boolean systemQamPressed; private boolean qamChordActive; private boolean qamSyntheticAPressed; - /** The Deck controller's Quick Access button, held for one tap. */ private boolean qamTapPressed; private int qamChordGeneration; - /** Told on the main thread when a player uses the pad or the on-screen controls; see [setOnPlayerInput]. */ private volatile Runnable onPlayerInput; private final java.util.concurrent.atomic.AtomicBoolean playerInputPosted = new java.util.concurrent.atomic.AtomicBoolean(); - // What reached the pad over the last stats window, so a report of "the pad does nothing" or - // "touch and the pad don't work together" shows in the app log whether input arrived, from - // which device, and whether it got to the ring. Logged only for a window that had input. private static final long STATS_WINDOW_MS = 10_000; private int statButtons, statAxes, statOnScreen, statDropped; private String statDevice; @@ -99,14 +67,8 @@ public final class PadBridge { private final java.util.Set<Integer> seenDevices = new java.util.HashSet<>(); private int lastDeviceId = Integer.MIN_VALUE; - /** No physical controller is driving the pad: nothing yet, or the on-screen controls were last. */ public static final int NO_CONTROLLER = -1; - /** - * The Android input device id of the physical controller that last drove any slot, or - * {@link #NO_CONTROLLER}: where rumble goes when it names no slot of ours (RumbleComponent). - */ private static volatile int activeControllerId = NO_CONTROLLER; - /** Per slot, the controller its rumble belongs to; slot 0's is none while the on-screen controls lead. */ private static final AtomicIntegerArray slotControllers = new AtomicIntegerArray(SLOTS); static { for (int slot = 0; slot < SLOTS; slot++) slotControllers.set(slot, NO_CONTROLLER); @@ -114,7 +76,6 @@ public final class PadBridge { public static int activeControllerId() { return activeControllerId; } - /** The controller playing in this ring slot, or the last one used for any other slot. */ public static int controllerForSlot(int slot) { return slot >= 0 && slot < SLOTS ? slotControllers.get(slot) : activeControllerId; } @@ -126,12 +87,10 @@ public final class PadBridge { for (int slot = 1; slot < SLOTS; slot++) playerStates[slot] = new PadState(); } - /** The physical Select button opens the Steam menu instead of View/Select (Setup → Controller). */ public void setSelectSteam(boolean on) { selectSteam = on; } - /** Opens the ring; safe to call more than once. */ public synchronized boolean start() { if (!open) { open = writer.open(); @@ -165,19 +124,13 @@ public final class PadBridge { } } - /** True when the device this event came from is a gamepad or joystick, not the touchscreen. */ public static boolean isFromController(InputDevice device) { if (device == null) return false; - // A virtual device is the system's own synthetic input, not a pad somebody is holding - - // counting it would hide the on-screen controls with nothing to replace them. if (device.isVirtual()) return false; int sources = device.getSources(); if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) return true; if ((sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) return false; if (device.getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC) return true; - // Some composite keyboards advertise joystick input without any sticks or pad buttons. - // A keyboard's mouse ranges do not establish a joystick; a real hybrid's joystick ranges - // or physical controller buttons do. GAMEPAD above remains authoritative even with keys. for (int axis : JOYSTICK_AXES) { InputDevice.MotionRange range = device.getMotionRange(axis, InputDevice.SOURCE_JOYSTICK); if (range != null && range.getMax() > range.getMin()) return true; @@ -188,11 +141,9 @@ public final class PadBridge { return false; } - /** Whether this key belongs to the pad, rather than a keyboard on the same input device. */ public static boolean isControllerKeyEvent(KeyEvent event) { InputDevice device = event.getDevice(); if (device == null || device.isVirtual()) return false; - // An explicit pad button is unambiguous even when a hybrid omits its capabilities. if (KeyEvent.isGamepadButton(event.getKeyCode())) { return device.supportsSource(InputDevice.SOURCE_GAMEPAD) || device.supportsSource(InputDevice.SOURCE_JOYSTICK); @@ -206,9 +157,6 @@ public final class PadBridge { case KeyEvent.KEYCODE_MENU: case KeyEvent.KEYCODE_HOME: if (!isFromController(device)) return false; - // Without GAMEPAD, an alphabetic hybrid's keyboard arrows remain keyboard keys. - // Its ambiguous navigation keys need explicit pad/joystick event provenance; - // DPAD alone also describes ordinary keyboard arrows. return device.supportsSource(InputDevice.SOURCE_GAMEPAD) || device.getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC || event.isFromSource(InputDevice.SOURCE_GAMEPAD) @@ -217,17 +165,13 @@ public final class PadBridge { } } - /** Mouse motion on a composite controller still belongs to the pointer, not its sticks. */ public static boolean isControllerMotionEvent(MotionEvent event) { InputDevice device = event.getDevice(); - // An actual joystick event, like a pad button, survives a hybrid's missing capabilities. return event.isFromSource(InputDevice.SOURCE_JOYSTICK) && device != null && !device.isVirtual() && (device.supportsSource(InputDevice.SOURCE_GAMEPAD) || device.supportsSource(InputDevice.SOURCE_JOYSTICK)); } - /** A Bluetooth keyboard can share a device with a joystick. Classify the key, not just - * the device, so its letters and modifiers still reach the desktop keyboard. */ public static boolean isControllerKey(KeyEvent event) { return isFromController(event.getDevice()) && isPadKey(event.getKeyCode(), event.getDevice().getKeyboardType()); @@ -249,7 +193,6 @@ public final class PadBridge { } } - /** Whether any real controller is attached right now. */ public static boolean anyControllerConnected() { for (int id : InputDevice.getDeviceIds()) { if (isFromController(InputDevice.getDevice(id))) return true; @@ -257,7 +200,6 @@ public final class PadBridge { return false; } - /** Publishes every currently connected controller so the client can enumerate all players. */ public synchronized void syncConnectedControllers() { for (int deviceId : InputDevice.getDeviceIds()) { InputDevice device = InputDevice.getDevice(deviceId); @@ -265,11 +207,6 @@ public final class PadBridge { } } - /** - * Called (main thread) when a player presses a button, pushes a stick, trigger or d-pad past - * halfway, or uses the on-screen controls - the session hides its mouse cursor then. Resting - * sticks and trigger noise do not count. - */ public void setOnPlayerInput(Runnable listener) { onPlayerInput = listener; } @@ -283,7 +220,6 @@ public final class PadBridge { }); } - /** @return true when the event was a pad button and has been consumed. */ public synchronized boolean onKeyEvent(KeyEvent event) { if (!isControllerKeyEvent(event)) return false; int slot = slotFor(event.getDevice()); @@ -297,8 +233,6 @@ public final class PadBridge { case KeyEvent.KEYCODE_BUTTON_L1: pad.press(4, pressed); break; case KeyEvent.KEYCODE_BUTTON_R1: pad.press(5, pressed); break; case KeyEvent.KEYCODE_BUTTON_SELECT: - // Optionally (Setup → Controller) the physical Select button opens the Steam menu - // instead of View/Select: on some handhelds it sits next to the Steam button. if (selectSteam) pad.press(PadState.GUIDE, pressed); else pad.press(6, pressed); break; @@ -307,8 +241,6 @@ public final class PadBridge { case KeyEvent.KEYCODE_MENU: pad.press(7, pressed); break; case KeyEvent.KEYCODE_BUTTON_THUMBL: pad.press(8, pressed); break; case KeyEvent.KEYCODE_BUTTON_THUMBR: pad.press(9, pressed); break; - // The client's own in-game menu is opened by this one; the interposer publishes it as - // BTN_MODE, which SDL reports as the "guide" button. case KeyEvent.KEYCODE_BUTTON_MODE: case KeyEvent.KEYCODE_HOME: pad.press(PadState.GUIDE, pressed); break; case KeyEvent.KEYCODE_BUTTON_L2: pad.leftTrigger = pressed ? 1f : 0f; break; @@ -327,7 +259,6 @@ public final class PadBridge { return true; } - /** @return true when the event was a pad's sticks/triggers and has been consumed. */ public synchronized boolean onMotionEvent(MotionEvent event) { if (!isControllerMotionEvent(event)) return false; if (event.getAction() != MotionEvent.ACTION_MOVE) return false; @@ -337,14 +268,11 @@ public final class PadBridge { statAxes++; scheduleStats(); pad.leftX = axis(event, MotionEvent.AXIS_X); - // Android's Y axis grows downwards and evdev's ABS_Y does too, so no flip here: what the - // pad reports as "down" is what the client is told. pad.leftY = axis(event, MotionEvent.AXIS_Y); pad.rightX = axis(event, MotionEvent.AXIS_Z); pad.rightY = axis(event, MotionEvent.AXIS_RZ); float lt = event.getAxisValue(MotionEvent.AXIS_LTRIGGER); float rt = event.getAxisValue(MotionEvent.AXIS_RTRIGGER); - // Some pads only report the triggers on BRAKE/GAS. if (lt == 0f) lt = event.getAxisValue(MotionEvent.AXIS_BRAKE); if (rt == 0f) rt = event.getAxisValue(MotionEvent.AXIS_GAS); pad.leftTrigger = lt; @@ -363,10 +291,6 @@ public final class PadBridge { return true; } - /** - * The on-screen controls' way in: they mutate the same state a physical pad writes, so the - * client only ever sees one device and a user can use both at once without them fighting. - */ public synchronized void applyTouch(java.util.function.Consumer<PadState> mutation) { mutation.accept(state); activeControllerId = NO_CONTROLLER; @@ -377,12 +301,6 @@ public final class PadBridge { publish(); } - /** - * A controller the app reads itself rather than through Android's input (the Steam Controller - * over Bluetooth, SteamControllerBle): its whole state, merged into the one exported slot as a - * physical pad's events are. Android has no device for it, so a game's rumble cannot reach it. - * It streams its state whether or not anything moved; {@code changed} is whether this one did. - */ public synchronized void applyExternal(java.util.function.Consumer<PadState> mutation, boolean changed) { mutation.accept(state); if (changed) { @@ -394,21 +312,15 @@ public final class PadBridge { publish(); } - /** Touch-only Steam and QAM buttons, merged with physical input without changing its state. */ public synchronized void setSystemButtons(boolean guidePressed, boolean qamPressed) { if (systemGuidePressed == guidePressed && systemQamPressed == qamPressed) return; boolean qamStarted = qamPressed && !systemQamPressed; systemGuidePressed = guidePressed; systemQamPressed = qamPressed; - // A Deck or Steam Controller's QAM button is held for as long as the touch one is. if (qamStarted && !nativeQam()) startQamChord(); else publish(); } - /** - * Everything released and centred - for when the controller stops feeding the game (the - * session drawer opened), so a button or stick held at that moment is not left down in it. - */ public synchronized void releaseAll() { state.clear(); publish(); @@ -419,23 +331,16 @@ public final class PadBridge { } } - /** A controller went away: its player's buttons are let go, and a further player's pad is unplugged. */ public synchronized void onDeviceRemoved(int deviceId) { for (int slot = 0; slot < SLOTS; slot++) { if (owners[slot] == deviceId) free(slot); } } - /** - * Whether the client's pad has its own QAM button to press. The Deck's sysfs being up is not - * enough: the drawer can make the pad an Xbox or PlayStation one, which has none, and a press - * of it never reaches the client. - */ private static boolean nativeQam() { return SessionState.getDeckPad() && FakeInputWriter.padHasQam(); } - /** Opens the client's Quick Access menu: a tap of the pad's button, or the Guide-then-A chord. */ public synchronized void triggerQam() { if (!nativeQam()) { startQamChord(); @@ -488,7 +393,6 @@ public final class PadBridge { publish(); } - /** The slot this controller plays in: the one it has, else the first free one, else player 1's. */ private int slotFor(InputDevice device) { int id = device.getId(); if (owners[0] == id) return 0; @@ -527,7 +431,6 @@ public final class PadBridge { } } - /** Per event, so the common case - the same pad as last time - is a single compare. */ private void noteDevice(InputDevice device, int slot) { activeControllerId = device.getId(); slotControllers.set(slot, device.getId()); diff --git a/app/src/main/java/com/droiddeck/launcher/input/PadMotion.kt b/app/src/main/java/com/droiddeck/launcher/input/PadMotion.kt index 7058bf300..bc640507a 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/PadMotion.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/PadMotion.kt @@ -11,31 +11,14 @@ import android.util.Log import android.view.Surface import kotlin.math.roundToInt -/** - * The handheld's own gyro and accelerometer, as the Steam Deck controller's (SteamDeckPad). - * - * The phone is the controller's body, so its motion sensors are the ones a Deck's would be. Android - * reports them in the device's natural orientation; they are turned to the screen as it is shown - * (x right, y up, z towards the player - SDL's frame for a gamepad), then to the Deck's own axes - * and units as SDL's Deck driver undoes them (SDL_hidapi_steamdeck.c): X right, Y away from the - * player, Z up; 1 g = 16384, 2000 °/s = 32768. The Steam client calibrates the gyro itself. - * - * Sensors run only while the session is on screen with the pad a Deck controller, and the client - * wants motion (see [follow]), sampled as often as the Deck reports (4 ms); each reading goes - * straight into the pad's ring ([FakeInputWriter.writeMotion]), where libfakeinput's 4 ms report - * picks up the latest. - */ class PadMotion(private val context: Context, private val rotation: () -> Int) : SensorEventListener { private val sensors = context.getSystemService(SensorManager::class.java) private var thread: HandlerThread? = null - /** Readings are taken only while this is set; a callback already queued when [stop] ran drops. */ @Volatile private var active = false - // Touched only on the worker thread. private val accel = ShortArray(3) private val gyro = ShortArray(3) private var reading = false private var idlePolls = 0 - // Asking the display costs more than the rest of a reading, which arrives every 2 ms. private var turn = Surface.ROTATION_0 private var turnAt = 0L @@ -65,8 +48,6 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : val worker = thread ?: return thread = null active = false - // On the worker, after any poll or reading already queued there, so nothing registers again - // or overwrites the still controller. Handler(worker.looper).post { sensors?.unregisterListener(this) reading = false @@ -76,12 +57,6 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : worker.quitSafely() } - /** - * Reads the sensors while the client wants motion: the Deck's IMU mode (unknown counts as - * wanted), or Steam's touch controls asking for gyro. A game whose layout has no gyro turns the - * mode off; Steam's own menus keep it on. The client also turns it off and on again for a few - * ms whenever it re-applies a layout, so the sensors stop only after two polls without it. - */ private fun follow(handler: Handler, gyroscope: Sensor?, accelerometer: Sensor?) { val touch = SteamTouchDevice.current val wanted = FakeInputWriter.readImuMode(SLOT) != 0 || @@ -101,7 +76,6 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : } } - /** A still controller, not one frozen mid-turn. */ private fun still() { gyro.fill(0) FakeInputWriter.writeMotion(SLOT, accel, gyro) @@ -115,7 +89,6 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : turn = rotation() turnAt = event.timestamp } - // Natural orientation to the screen's: Android's display-rotation transform. val x: Float val y: Float when (turn) { @@ -132,7 +105,6 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : Sensor.TYPE_ACCELEROMETER -> { target = accel; scale = ACCEL_COUNTS_PER_M_S2 } else -> return } - // Screen frame (right, up, towards the player) to the Deck's (right, away, up). target[0] = counts(x * scale) target[1] = counts(-z * scale) target[2] = counts(y * scale) @@ -146,11 +118,8 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) : companion object { private const val TAG = "PadMotion" - /** The pad the Deck controller is made from (PadBridge's slot). */ private const val SLOT = 0 - /** The Deck controller's report interval (libfakeinput: DECK_REPORT_INTERVAL_US). */ private const val SAMPLING_US = 4000 - /** How often [follow] checks whether the client wants motion. */ private const val POLL_MS = 500L private const val ROTATION_REFRESH_NS = 250_000_000L private const val ACCEL_COUNTS_PER_M_S2 = 16384f / SensorManager.GRAVITY_EARTH diff --git a/app/src/main/java/com/droiddeck/launcher/input/PadState.kt b/app/src/main/java/com/droiddeck/launcher/input/PadState.kt index c798b682f..d337a2a32 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/PadState.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/PadState.kt @@ -1,11 +1,5 @@ package com.droiddeck.launcher.input -/** - * What one Xbox-shaped pad is doing right now: two sticks and two triggers as -1..1 / 0..1, the - * four d-pad directions, and a bitmask of the buttons numbered the way the fake evdev node - * reports them. Written by whatever drives the pad (a physical controller, the on-screen one, or - * both at once) and read by [FakeInputWriter] into the guest's input ring. - */ class PadState { @JvmField var leftX = 0f @JvmField var leftY = 0f @@ -25,7 +19,6 @@ class PadState { fun isDown(button: Int): Boolean = buttons and (1 shl button) != 0 - /** The d-pad as a hat: -1 left, 1 right; -1 up, 1 down. Opposites cancel to 0. */ fun hatX(): Int = if (right && !left) 1 else if (left && !right) -1 else 0 fun hatY(): Int = if (down && !up) 1 else if (up && !down) -1 else 0 @@ -36,7 +29,6 @@ class PadState { buttons = other.buttons } - /** Everything released and centred. */ fun clear() { leftX = 0f; leftY = 0f; rightX = 0f; rightY = 0f leftTrigger = 0f; rightTrigger = 0f @@ -55,9 +47,7 @@ class PadState { const val START = 7 const val L3 = 8 const val R3 = 9 - /** The Steam/Guide button, which the fake evdev node reports as BTN_MODE. */ const val GUIDE = 12 - /** The Quick Access button: a Deck controller's (SteamDeckPad) only, since an Xbox pad has none. */ const val QAM = 13 } } diff --git a/app/src/main/java/com/droiddeck/launcher/input/PointerGestures.kt b/app/src/main/java/com/droiddeck/launcher/input/PointerGestures.kt index 9183578d1..dab9ff652 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/PointerGestures.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/PointerGestures.kt @@ -7,25 +7,12 @@ import android.view.ViewConfiguration import kotlin.math.abs import kotlin.math.roundToInt -/** - * Touch as a mouse, the way a remote desktop does it: where you touch is where the pointer goes. - * - * - tap ............................ left click - * - drag ........................... moves the pointer, no button (hover) - * - hold, then lift ................ right click - * - hold, then drag ................ left-button drag (a window by its title bar, a selection) - * - two fingers up / down .......... scroll wheel - * - * The listener gets pointer positions in the view's pixels; the activity maps them onto the - * compositor. Mouse events are not handled here - a mouse has real buttons. - */ class PointerGestures( private val slopPx: Float, private val listener: Listener, ) { interface Listener { fun onMove(x: Float, y: Float) - /** [button] is an evdev code: BTN_LEFT, BTN_RIGHT. */ fun onButton(button: Int, pressed: Boolean, x: Float, y: Float) fun onWheel(steps: Int) fun onLongPress() @@ -36,8 +23,8 @@ class PointerGestures( private var downY = 0f private var downAt = 0L private var moved = false - private var held = false // the long press fired - private var dragging = false // left button down after a hold + private var held = false + private var dragging = false private var scrolling = false private var scrollAnchorY = 0f private val longPress = Runnable { @@ -55,7 +42,6 @@ class PointerGestures( return true } MotionEvent.ACTION_POINTER_DOWN -> { - // A second finger: scrolling, and whatever the first one was doing is off. handler.removeCallbacks(longPress) if (dragging) { listener.onButton(BTN_LEFT, false, event.x, event.y); dragging = false } scrolling = true @@ -67,7 +53,6 @@ class PointerGestures( val dy = event.getY(0) - scrollAnchorY val steps = (dy / SCROLL_STEP_PX).toInt() if (steps != 0) { - // Finger down = content up = wheel "up", which the compositor takes as negative. listener.onWheel(-steps) scrollAnchorY += steps * SCROLL_STEP_PX } @@ -77,7 +62,6 @@ class PointerGestures( moved = true handler.removeCallbacks(longPress) if (held) { - // Held, then moved: a drag with the left button, from where the hold was. dragging = true listener.onButton(BTN_LEFT, true, downX, downY) } diff --git a/app/src/main/java/com/droiddeck/launcher/input/SecondScreenMode.kt b/app/src/main/java/com/droiddeck/launcher/input/SecondScreenMode.kt index de5f70d94..6789654f7 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SecondScreenMode.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SecondScreenMode.kt @@ -5,18 +5,15 @@ import android.view.Display import androidx.annotation.StringRes import com.droiddeck.launcher.R -/** The control surface shown on a secondary Android display during a Linux session. */ enum class SecondScreenMode(val id: String, @StringRes val label: Int) { NONE("none", R.string.second_mode_none), KEYBOARD_TRACKPAD("keyboard-trackpad", R.string.second_mode_keyboard_trackpad), TERMINAL("terminal", R.string.second_mode_terminal), - /** The Steam Deck controller's back grips and trackpads; offered while the pad is one. */ DECK_CONTROLS("deck-controls", R.string.second_mode_deck_controls), } data class SecondScreenDisplay(val id: Int, val label: String) -/** The presentation-capable displays shared by the session controls and Android app launcher. */ object SecondScreenDisplays { fun available(displayManager: DisplayManager, primaryDisplayId: Int = Display.DEFAULT_DISPLAY): List<SecondScreenDisplay> = displayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION) diff --git a/app/src/main/java/com/droiddeck/launcher/input/SessionClipboard.kt b/app/src/main/java/com/droiddeck/launcher/input/SessionClipboard.kt index faa6e1ace..4ec08bcb4 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SessionClipboard.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SessionClipboard.kt @@ -11,7 +11,6 @@ import com.droiddeck.launcher.wayland.WaylandCompositor import java.io.File import java.io.IOException -/** Text selections shared by Android and the guest while the session is visible. */ class SessionClipboard(context: Context, private val hasFocus: () -> Boolean) { private val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager private val handler = Handler(Looper.getMainLooper()) @@ -20,8 +19,6 @@ class SessionClipboard(context: Context, private val hasFocus: () -> Boolean) { private val androidListener = ClipboardManager.OnPrimaryClipChangedListener { refresh() } private val guestListener = object : WaylandCompositor.ClipboardListener { override fun onGuestClipboardText(text: String) { - // Native callbacks arrive on the compositor thread. Do not publish a delayed callback - // after this activity has paused or been replaced. handler.post { if (active && hasFocus() && WaylandCompositor.isClipboardListener(this)) { val current = clipboard.primaryClip?.takeIf { it.itemCount > 0 }?.getItemAt(0)?.text @@ -49,11 +46,9 @@ class SessionClipboard(context: Context, private val hasFocus: () -> Boolean) { handler.removeCallbacksAndMessages(null) } - /** Android 10+ permits clipboard reads only with window focus. Resume can precede focus. */ fun refresh() { if (!active || !hasFocus()) return val clip = clipboard.primaryClip - // Only share literal text, never resolve content URIs or load copied files. if (clip == null) { publish(null) } else if (clip.itemCount > 0) { @@ -63,8 +58,6 @@ class SessionClipboard(context: Context, private val hasFocus: () -> Boolean) { private fun publish(text: String?) { WaylandCompositor.setClipboardText(text) - // Gamescope needs the same selection in its own Xwayland. Rename complete UTF-8 text - // into place so its helper never sees half a launch command. Kept in private app data. val pending = File(hostFile.parentFile, "android-clipboard.pending") try { hostFile.parentFile?.mkdirs() diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamControllerBle.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamControllerBle.kt index 34357e48a..ee84cffe4 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamControllerBle.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamControllerBle.kt @@ -18,20 +18,11 @@ import java.nio.ByteOrder import java.util.ArrayDeque import java.util.UUID -/** - * Reads the Steam Controller (2025, Triton) through its Valve BLE service. State reports remain - * native Triton packets and are published to the session's separate 28DE:1303 HID device. Steam's - * output reports travel back to their matching BLE characteristics, preserving grip capacitance, - * trackpads, sensors, and controller haptics without folding the controller into the Deck target. - * - * Lizard mode is refreshed while connected; when a session ends, the controller restores it itself. - */ -@SuppressLint("MissingPermission") // BLUETOOTH is granted at install below targetSdk 31 +@SuppressLint("MissingPermission") class SteamControllerBle(private val context: Context) { private val thread = HandlerThread("steam-controller-ble").apply { start() } private val handler = Handler(thread.looper) private val outputReports = mutableMapOf<Int, UUID>() - /** GATT allows one operation at a time: writes carry a value, a feature read none. */ private val queuedWrites = ArrayDeque<Pair<UUID, ByteArray?>>() private var gatt: BluetoothGatt? = null private var running = false @@ -42,10 +33,8 @@ class SteamControllerBle(private val context: Context) { private var discoveries = 0 private var outputSequence = 0L private var outputWritePending = false - /** The Steam client's feature read being answered, so the poll queues each one once. */ private var featureRead = 0L - /** Looks for a paired Steam Controller and keeps a connection to it until [stop]. */ fun start() = handler.post { if (running) return@post running = true @@ -53,7 +42,6 @@ class SteamControllerBle(private val context: Context) { connect() } - /** Lets the controller go (it returns to lizard mode by itself); this object is done after. */ fun stop() = handler.post { running = false handler.removeCallbacksAndMessages(null) @@ -118,7 +106,6 @@ class SteamControllerBle(private val context: Context) { if (running) handler.postDelayed(::connect, RETRY_MS) return } - // The Triton needs data length extensions, which Android enables for a large MTU. if (!g.requestMtu(517)) subscribe(g) } @@ -160,7 +147,6 @@ class SteamControllerBle(private val context: Context) { if (g === gatt) onFeatureRead(characteristic.value.takeIf { status == BluetoothGatt.GATT_SUCCESS }) } - // Android 13 hands the value over with the call; older ones only through the characteristic. override fun onCharacteristicChanged(g: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) { if (g === gatt && characteristic.uuid == input) onState(value) } @@ -177,8 +163,6 @@ class SteamControllerBle(private val context: Context) { if (service == null) { Log.w(TAG, "no Valve service on \"${g.device.name}\" (attempt ${discoveries + 1}); services: " + g.services.joinToString { it.uuid.toString().take(8) }) - // Android may answer from the service list its HID host cached, which can lack the - // vendor service: ask the controller again before giving up on it. if (++discoveries < 3) { refreshCache(g) handler.postDelayed({ if (g === gatt) g.discoverServices() }, 500) @@ -213,13 +197,11 @@ class SteamControllerBle(private val context: Context) { g.writeDescriptor(cccd) } - /** BluetoothGatt.refresh(): hidden, but used to drop stale service caches. */ private fun refreshCache(g: BluetoothGatt) { runCatching { g.javaClass.getMethod("refresh").invoke(g) } .onFailure { Log.w(TAG, "could not refresh the service cache", it) } } - /** SDL's DisableSteamTritonLizardMode: ID_SET_SETTINGS_VALUES (0x87), setting 9 = off. */ private fun refreshLizardMode() { if (!connected) return val msg = ByteArray(FEATURE_BYTES) @@ -228,18 +210,15 @@ class SteamControllerBle(private val context: Context) { handler.postDelayed(::refreshLizardMode, LIZARD_REFRESH_MS) } - /** Preserve the full 0x45 or 0x47 packet so Triton-only bits reach Steam's native driver. */ private fun onState(data: ByteArray) { if (data.size < STATE_BYTES) return FakeInputWriter.writeTritonState(SLOT, reportId, data) } - /** A neutral report prevents a link lost mid-press from leaving controls held. */ private fun release() { FakeInputWriter.writeTritonState(SLOT, if (reportId == 0x47) 0x47 else 0x45, ByteArray(STATE_BYTES)) } - /** Drain Steam HID output reports and write each to Triton's matching BLE characteristic. */ private fun pollOutput() { if (!running || !connected || gatt == null) return FakeInputWriter.readTritonOutput(SLOT, outputSequence)?.let { output -> @@ -249,14 +228,12 @@ class SteamControllerBle(private val context: Context) { val request = FakeInputWriter.readTritonFeatureRequest(SLOT) if (request != 0L && request != featureRead) { featureRead = request - // Behind any queued write, so a read answers the feature report written before it. queuedWrites.addLast(REPORT to null) writeNextOutput() } handler.postDelayed(::pollOutput, OUTPUT_POLL_MS) } - /** The controller's answer to a feature read (its info, for one), or null when the read failed. */ private fun onFeatureRead(value: ByteArray?) { outputWritePending = false if (featureRead != 0L) { @@ -277,7 +254,6 @@ class SteamControllerBle(private val context: Context) { return } } - // SDL's BLE driver removes the HID report ID and trailing padding byte before transmission. enqueueOutput(characteristic, report.copyOfRange(1, report.size - 1)) } @@ -319,13 +295,11 @@ class SteamControllerBle(private val context: Context) { companion object { private const val TAG = "SteamControllerBle" - /** The paired controller's native Triton reports live in slot 0's extended ring. */ private const val SLOT = 0 private const val RETRY_MS = 5_000L private const val LIZARD_REFRESH_MS = 3_000L private const val OUTPUT_POLL_MS = 4L private const val OUTPUT_RETRY_MS = 10L - /** SDL's 64-byte feature report less the report ID and the trailing byte. */ private const val FEATURE_BYTES = 62 private const val STATE_BYTES = 45 private const val OUTPUT_UUID_PREFIX = "100f6c" @@ -341,7 +315,6 @@ class SteamControllerBle(private val context: Context) { device.type and BluetoothDevice.DEVICE_TYPE_LE != 0 && (device.name ?: "").let { it.startsWith("Steam Ctrl") || it.startsWith("SteamController") } - /** Whether a session should expose a Triton HID node for a paired controller. */ @JvmStatic fun hasPairedController(context: Context): Boolean { val adapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter ?: return false @@ -350,12 +323,10 @@ class SteamControllerBle(private val context: Context) { .getOrDefault(false) } - /** A Steam Controller is connected and feeding its native HID reports. */ @Volatile @JvmStatic var connected = false private set - /** Notifies the activity when [connected] changes. */ @Volatile @JvmStatic var listener: (() -> Unit)? = null } diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchBindings.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchBindings.kt index f5f46d88c..4d19cb87d 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchBindings.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchBindings.kt @@ -6,21 +6,8 @@ import android.graphics.BitmapFactory import android.util.LruCache import java.io.File -/** - * What each touch control is bound to, and the icon it shows - Steam's, as its configurator keeps - * them. - * - * A Steam Input binding is one string: `<action>, <label>, <icon>, <#foreground #background>`, e.g. - * `xinput_button A, Jump, ghost_045_move_0400.png, #232323 #E4E4E4`. The icon is a file name looked - * up as Steam does (steamui.so): the game's own `TouchMenuIcons` folder, then the binding icon - * library `tenfoot/resource/images/library/controller/binding_icons`. Steam draws it filled with - * the foreground colour through its own alpha (mask-image) on a button of the background colour. - * Steam Link is sent these per control by the client it streams from; here they are read from the - * game's touch config, and editing one writes the binding string back, as the configurator does. - */ object SteamTouchBindings { - /** Steam's binding icon colours (the configurator's palette, steamui sp.js), and its defaults. */ val PALETTE = listOf( "#FFFFFF", "#E4E4E4", "#AAAAAA", "#787878", "#434343", "#222222", "#0000AD", "#0045AD", "#0074AD", "#00ADAD", "#33AD69", "#00AD3D", "#00AD00", "#48B119", "#74AD00", "#96AD00", "#ADA200", "#AD5D00", @@ -29,7 +16,6 @@ object SteamTouchBindings { const val DEFAULT_FOREGROUND = "#232323" const val DEFAULT_BACKGROUND = "#E4E4E4" - /** A parsed binding string. Empty strings for absent fields. */ data class Binding(val action: String, val label: String, val icon: String, val foreground: String, val background: String) { fun compose(): String = if (icon.isEmpty() && foreground.isEmpty() && background.isEmpty()) "$action, $label, " @@ -45,10 +31,8 @@ object SteamTouchBindings { colors.getOrNull(0).orEmpty(), colors.getOrNull(1).orEmpty()) } - /** Where a control's binding lives: its input in the group the preset puts on its source. */ data class Ref(val groupId: String, val input: String) - /** The source and input names a control reads (D-pad: its four directions). */ private fun inputsOf(type: Int): List<Pair<String, String>> = when (type) { SteamTouchConfig.A -> listOf("button_diamond" to "button_a") SteamTouchConfig.B -> listOf("button_diamond" to "button_b") @@ -69,13 +53,8 @@ object SteamTouchBindings { else -> emptyList() } - /** D-pad directions, in the order [refsFor] returns them: up, down, left, right. */ val DPAD_INPUTS = listOf("dpad_north", "dpad_south", "dpad_west", "dpad_east") - /** - * Where control [type]'s bindings are for preset [preset] (the action set less one) and its - * active [layers]: a layer's group wins over the base's for the same source. - */ fun refsFor(mappings: KeyValues.Node, type: Int, preset: Int, layers: List<Int> = emptyList()): List<Ref?> { val presets = mappings.children("preset").associateBy { it.string("id")?.toIntOrNull() ?: -1 } val stack = layers.reversed().mapNotNull { presets[it - 1] } + listOfNotNull(presets[preset] ?: presets.values.firstOrNull()) @@ -90,7 +69,6 @@ object SteamTouchBindings { parts.getOrNull(0) == source && parts.getOrNull(1) == "active" && !parts.contains("modeshift") }?.first - /** The binding at [ref]: its first activator's first binding (Full_Press when there is one). */ fun bindingAt(mappings: KeyValues.Node, ref: Ref): Binding? { val group = mappings.children("group").firstOrNull { it.string("id") == ref.groupId } ?: return null val activators = group.child("inputs")?.child(ref.input)?.child("activators") ?: return null @@ -98,7 +76,6 @@ object SteamTouchBindings { return activator?.child("bindings")?.string("binding")?.let(::parse) } - /** A short glyph for a binding with no icon or label: what it presses. */ fun glyph(binding: Binding?): String? { val words = binding?.action?.split(' ') ?: return null val arg = words.getOrNull(1)?.uppercase() ?: return null @@ -123,12 +100,7 @@ object SteamTouchBindings { "LEFT_ARROW" to "←", "RIGHT_ARROW" to "→", ) - // ---- Writing a binding back ---- - /** - * Rewrites every binding of [ref]'s input in config [text] (VDF) with [change], keeping the rest - * of the file byte for byte. Returns the new text, or null when the input is not there. - */ fun rewrite(text: String, ref: Ref, change: (Binding) -> Binding): String? { val tokens = Vdf.tokens(text) val group = Vdf.blocks(tokens, 0, tokens.size, "group").firstOrNull { (s, e) -> @@ -145,7 +117,6 @@ object SteamTouchBindings { return out.toString() } - /** A VDF tokenizer that keeps where each token sits, for edits that leave the rest untouched. */ private object Vdf { class Token(val text: String, val quoted: Boolean, val start: Int, val end: Int) @@ -175,8 +146,6 @@ object SteamTouchBindings { return out } - /** The bodies (token index ranges inside the braces) of direct children named [key] of the - * block spanning [from, to). */ fun blocks(tokens: List<Token>, from: Int, to: Int, key: String): List<Pair<Int, Int>> { val found = mutableListOf<Pair<Int, Int>>() var i = from @@ -200,7 +169,6 @@ object SteamTouchBindings { } i++ } - // The whole file is one top-level block ("controller_mappings"): look inside it too. if (found.isEmpty() && from == 0) { val top = tokens.indexOfFirst { it.text == "{" && !it.quoted } if (top > 0) { @@ -217,7 +185,6 @@ object SteamTouchBindings { return found } - /** A direct key-value of the block spanning [from, to). */ fun value(tokens: List<Token>, from: Int, to: Int, key: String): String? { var depth = 0 var i = from @@ -232,7 +199,6 @@ object SteamTouchBindings { } } - // ---- Icons ---- private val cache = object : LruCache<String, Bitmap>(24 * 1024 * 1024) { override fun sizeOf(key: String, value: Bitmap) = value.byteCount @@ -241,7 +207,6 @@ object SteamTouchBindings { fun libraryDir(context: Context) = File(SteamTouchConfig.steamRoot(context), "tenfoot/resource/images/library/controller/binding_icons") - /** The game's own icons folder (`TouchMenuIcons` in its install directory), when it has one. */ fun gameIconsDir(context: Context, appId: Int): File? = try { val manifest = File(SteamTouchConfig.steamRoot(context), "steamapps/appmanifest_$appId.acf") val installDir = KeyValues.parse(manifest.readText()).let { it.child("AppState") ?: it }.string("installdir") @@ -257,7 +222,6 @@ object SteamTouchBindings { return File(SteamTouchConfig.steamRoot(context), "steamui/images/controller/$name").takeIf { it.isFile } } - /** An icon, decoded at about [size] px. Blocking (reads a file); cached. */ fun icon(context: Context, appId: Int, name: String, size: Int = 128): Bitmap? { val key = "$appId/$name/$size" cache.get(key)?.let { return it } @@ -276,7 +240,6 @@ object SteamTouchBindings { fun cachedIcon(appId: Int, name: String, size: Int = 128): Bitmap? = cache.get("$appId/$name/$size") - /** Icon names for the picker: the game's own first, then Steam's library by category. */ fun iconNames(context: Context, appId: Int): List<String> { val own = gameIconsDir(context, appId)?.list()?.filter { it.endsWith(".png", true) }?.sorted().orEmpty() val library = libraryDir(context).list()?.filter { it.endsWith(".png", true) && it != "special_blank.png" }?.sorted().orEmpty() diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchConfig.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchConfig.kt index 844fe147e..882a3a3dd 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchConfig.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchConfig.kt @@ -7,25 +7,9 @@ import java.io.ByteArrayOutputStream import java.io.File import java.io.RandomAccessFile -/** - * Steam's touch configs, read and written where Steam keeps them. - * - * A touch config is a Steam Input config (`controller_mobile_touch`) like any other - bindings per - * action set, synced to the account, shared as community configs - with one extra key, - * `touch_layout`: where each on-screen control sits, per action set, as a hex-encoded - * `CVirtualControllerLayouts` protobuf (steammessages_virtualcontroller.proto). Steam Link is - * handed both by the client it streams from; here the client tells the device only the app and - * action set it is on (SteamTouchDevice), so the config is found on disk: the file the client's - * console log says it loaded for the touch controller, else the app's entry in - * `configset_controller_mobile_touch.vdf`. With no layout in it, controls go where Steam Link puts - * them by default. Saving a layout writes `touch_layout` into the app's autosave config, as the - * client does when Steam Link saves one; Steam Cloud picks that file up like any autosave. - * (docs/development/steam-touch-controller.md.) - */ object SteamTouchConfig { private const val TAG = "SteamTouch" - // EControllerElementType. const val THUMB = 0 const val STEAM = 1 const val JOYSTICK_LEFT = 2 @@ -43,7 +27,7 @@ object SteamTouchConfig { const val TRIGGER_RIGHT = 14 const val BUMPER_LEFT = 15 const val BUMPER_RIGHT = 16 - const val MACRO_0 = 17 // 17..24 = macros 0-7 + const val MACRO_0 = 17 const val TRACKPAD_CENTER = 25 const val TRACKPAD_LEFT = 26 const val TRACKPAD_RIGHT = 27 @@ -53,27 +37,19 @@ object SteamTouchConfig { const val MAGNIFYING_GLASS = 29 const val PASTE = 34 - /** Controls a layout may place though no binding asks for them (Steam Link's tray). */ val OPTIONAL = setOf(PASTE) - /** A control's place: centre as a fraction of the screen, and its scale. */ data class Element(val type: Int, val visible: Boolean, val x: Float, val y: Float, val xScale: Float = 1f, val yScale: Float = 1f) data class Layout(val actionSet: Int, val elements: List<Element>, val color: FloatArray?, val version: Int?) - /** A decoded `touch_layout`; [rest] is every other top-level field, written back untouched. */ class Layouts(val layouts: List<Layout>, val rest: ByteArray) { fun forActionSet(id: Int): Layout? = layouts.firstOrNull { it.actionSet == id } - /** The per-game options Steam Link keeps beside the layouts (CVirtualControllerLayouts - * fields 2-4): input mode, mouse mode, trackpad sensitivity. */ val options: Options get() = Options.decode(rest) } - /** EInputMode: 1 mouse, 2 controller, 3 both. EMouseMode: 2 absolute cursor (direct touch), - * 3 touch, 4 relative (trackpad); 0 = not set. */ data class Options(val inputMode: Int = INPUT_CONTROLLER, val mouseMode: Int = 0, val trackpadSensitivity: Float = 1f) { - /** [rest] with these options in place of its own fields 2-4, the rest kept as it was. */ fun encodeInto(rest: ByteArray): ByteArray { val out = ByteArrayOutputStream() val p = Proto(rest) @@ -116,7 +92,6 @@ object SteamTouchConfig { const val MOUSE_TOUCH = 3 const val MOUSE_RELATIVE = 4 - /** The config's action sets in preset order: id as the device is told (preset id + 1) and title. */ fun actionSets(config: Config): List<Pair<Int, String>> { val mappings = config.mappings ?: return listOf(1 to "Default") val titles = mappings.child("actions")?.entries?.mapNotNull { (key, v) -> @@ -130,40 +105,30 @@ object SteamTouchConfig { return sets.ifEmpty { listOf(1 to "Default") } } - /** A config as the overlay needs it. */ class Config( val file: File?, val text: String?, val layouts: Layouts?, - /** Bound controls per preset (action set id - 1). */ val available: Map<Int, Set<Int>>, - /** The parsed config, for its bindings (SteamTouchBindings). */ val mappings: KeyValues.Node? = null, ) { - /** The config's own title, as Steam Link's tray shows it ("Default", "Gamepad"…). */ val title: String? get() = mappings?.string("title")?.let { t -> if (t.startsWith("#")) mappings.child("localization")?.child("english")?.string("title") ?: "Default" else t } - /** The controls to show for [actionSet] (as the device is told it) and [layers]. */ fun availableFor(actionSet: Int, layers: List<Int>): Set<Int> { val base = available[presetOf(actionSet)] ?: available.values.firstOrNull() ?: DEFAULT_SET return layers.fold(base) { set, layer -> set + (available[presetOf(layer)] ?: emptySet()) } + ALWAYS } } - /** Big Picture's own config uses Steam's basic UI bindings: the default set. */ private val DEFAULT_SET = setOf(DPAD, A, B, X, Y, SELECT, START) private val ALWAYS = setOf(STEAM, THUMB, KEYBOARD) - /** The layout entry for the action set the device was told about (the client counts from 1, - * 0 for an app with none). */ fun layoutIdOf(actionSet: Int) = if (actionSet <= 0) 1 else actionSet private fun presetOf(actionSet: Int) = layoutIdOf(actionSet) - 1 - // ---- Where Steam keeps them ---- - /** A game's name from its app manifest, when it is installed. */ fun gameName(context: Context, appId: Int): String? = try { val kv = KeyValues.parse(File(steamRoot(context), "steamapps/appmanifest_$appId.acf").readText()) (kv.child("AppState") ?: kv).string("name") @@ -181,7 +146,6 @@ object SteamTouchConfig { ?.maxByOrNull { it.lastModified() }?.let { File(it, "config") } } - /** The signed-in account (loginusers.vdf's MostRecent) as a 32-bit account id. */ private fun accountId(context: Context): Long? = try { val users = KeyValues.parse(File(steamRoot(context), "config/loginusers.vdf").readText()) val node = users.child("users") @@ -192,11 +156,8 @@ object SteamTouchConfig { null } - /** The config the client has for app [appId] on the touch controller. Blocking: reads files. */ fun load(context: Context, appId: Int): Config { val dir = configDir(context) - // The app's own saved config is what the client loads when there is one (its configset - // entry); otherwise what it logged loading - a template, an official or a workshop config. val file = dir?.let { autosaveConfig(it, appId) } ?: loggedConfig(context, appId)?.takeIf { isTouchConfig(it) } ?: dir?.let { configsetConfig(context, it, appId) } @@ -220,9 +181,6 @@ object SteamTouchConfig { private fun isTouchConfig(file: File) = file.isFile && file.bufferedReader().use { r -> generateSequence { r.readLine() }.take(40).any { it.contains("controller_mobile_touch") } } - /** From the client's console log: the last config it loaded for [appId] on the touch controller - * (`Loaded Config for ... App ID <n>, Controller <i>: <path>`, where controller i reported - * type 43). Guest paths come back as the app's own. */ private fun loggedConfig(context: Context, appId: Int): File? = try { val log = File(steamRoot(context), "logs/console_log.txt") val tail = RandomAccessFile(log, "r").use { raf -> @@ -249,7 +207,6 @@ object SteamTouchConfig { null } - /** The app's entry in configset_controller_mobile_touch.vdf. */ private fun configsetConfig(context: Context, dir: File, appId: Int): File? = try { val set = KeyValues.parse(File(dir, CONFIGSET).readText()).child("controller_config")?.child(appId.toString()) when { @@ -264,9 +221,7 @@ object SteamTouchConfig { private const val CONFIGSET = "configset_controller_mobile_touch.vdf" - // ---- What a config binds ---- - /** Per preset, the controls its active sources bind - what Steam Link shows (UpdateElementAvailability). */ private fun availability(mappings: KeyValues.Node): Map<Int, Set<Int>> { val groups = mappings.children("group").associateBy({ it.string("id") ?: "" }) { group -> group.child("inputs")?.entries?.filter { (_, v) -> (v as? KeyValues.Node)?.let(::hasBinding) == true } @@ -312,11 +267,7 @@ object SteamTouchConfig { (activator as? KeyValues.Node)?.child("bindings")?.entries?.any { (k, v) -> k == "binding" && (v as? String)?.isNotBlank() == true } == true } == true - // ---- Steam Link's defaults ---- - /** Where Steam Link puts a control with no layout (CVirtualController::BInitializeDefaultElement, - * on a 1280x720 screen), and the same scheme for the controls its table leaves out, made room - * for when a config binds sticks. */ fun defaultElement(type: Int, available: Set<Int>): Element? { val leftStick = JOYSTICK_LEFT in available val rightStick = JOYSTICK_RIGHT in available @@ -352,8 +303,6 @@ object SteamTouchConfig { return Element(type, true, p.first, p.second) } - /** The controls to draw: the layout's own where it has one, Steam Link's defaults otherwise, - * only those the config binds. */ fun elementsFor(config: Config, actionSet: Int, layers: List<Int>): List<Element> { val available = config.availableFor(actionSet, layers) val layout = config.layouts?.forActionSet(layoutIdOf(actionSet)) @@ -371,14 +320,7 @@ object SteamTouchConfig { fun layoutColor(config: Config, actionSet: Int): FloatArray? = config.layouts?.forActionSet(layoutIdOf(actionSet))?.color - // ---- Saving ---- - /** - * Saves [elements] as the layout of [actionSet] in app [appId]'s touch config: into the app's - * autosave config (`<appid>/controller_mobile_touch.vdf`, made from the config in use when the - * app has none) and selected for the app in the configset, as the client saves a layout Steam - * Link sends it. Returns the file written, or null. - */ fun saveLayout( context: Context, appId: Int, @@ -389,10 +331,6 @@ object SteamTouchConfig { bindings: Map<SteamTouchBindings.Ref, (SteamTouchBindings.Binding) -> SteamTouchBindings.Binding> = emptyMap(), ): File? = saveLayouts(context, appId, config, mapOf(layoutIdOf(actionSet) to elements), color?.let { c -> setOf(layoutIdOf(actionSet)).associateWith { c } } ?: emptyMap(), bindings) - /** - * Saves the layouts of several action sets (by layout id) at once, with their colours, any - * binding edits and the per-game [options], into the game's autosave touch config. - */ fun saveLayouts( context: Context, appId: Int, @@ -403,8 +341,6 @@ object SteamTouchConfig { options: Options? = null, ): File? { val dir = configDir(context) ?: return null - // Icons, labels and colours live in the binding strings (SteamTouchBindings), as the - // configurator writes them. val source = bindings.entries.fold(config.text ?: return null) { text, (ref, change) -> SteamTouchBindings.rewrite(text, ref, change) ?: text } @@ -414,7 +350,7 @@ object SteamTouchConfig { val written = sets.map { (id, elements) -> Layout(id, elements, colors[id] ?: old?.forActionSet(id)?.color ?: floatArrayOf(1f, 1f, 1f, 0.4f), old?.forActionSet(id)?.version) } - val rest = old?.rest ?: byteArrayOf(0x10, 0x02) // input_mode = controller + val rest = old?.rest ?: byteArrayOf(0x10, 0x02) val layouts = Layouts((keep + written).sortedBy { it.actionSet }, options?.encodeInto(rest) ?: rest) val hex = bytesToHex(encodeLayouts(layouts)) val line = "\t\"touch_layout\"\t\t\"$hex\"" @@ -431,7 +367,6 @@ object SteamTouchConfig { return target } - /** Points the app's configset entry at its autosave config. */ private fun selectAutosave(dir: File, appId: Int) { val file = File(dir, CONFIGSET) val text = if (file.isFile) file.readText() else "\"controller_config\"\n{\n}\n" @@ -442,7 +377,6 @@ object SteamTouchConfig { File(file.path + ".tmp").apply { writeText(next) }.renameTo(file) } - // ---- touch_layout: CVirtualControllerLayouts ---- fun decodeLayouts(bytes: ByteArray): Layouts { val layouts = mutableListOf<Layout>() @@ -591,7 +525,6 @@ object SteamTouchConfig { fun bytesToHex(b: ByteArray) = b.joinToString("") { "%02x".format(it.toInt() and 0xff) } } -/** Valve's KeyValues text format, as far as controller configs use it. */ class KeyValues private constructor() { class Node(val entries: MutableList<Pair<String, Any>> = mutableListOf()) { fun child(key: String): Node? = entries.firstOrNull { it.first.equals(key, true) && it.second is Node }?.second as Node? diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt index 2995896d1..5a717150e 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchControls.kt @@ -22,18 +22,6 @@ import kotlin.math.hypot import kotlin.math.max import kotlin.math.min -/** - * Steam's touch controls, drawn by the app. - * - * The layout is Steam's: the game's touch config for the action set the client says is active - * (SteamTouchDevice.State, SteamTouchConfig), placed where the config's layout puts each control or - * where Steam Link would by default, and only the controls the config binds. Touches become the - * touch controller's input report; the client does the rest - bindings, action sets, the virtual - * pad the game reads. Each control shows its binding's icon, label and colours, as Steam keeps them - * (SteamTouchBindings). [startEditing] moves, resizes and hides controls, picks their icons and the - * layout's colour, and saves all of it into the game's touch config, as Steam Link and Steam's - * configurator do. - */ @SuppressLint("ViewConstructor") class SteamTouchControls( context: Context, @@ -42,19 +30,14 @@ class SteamTouchControls( private val onKeyboard: () -> Unit, ) : View(context) { - /** The game's input and mouse modes changed (from its config or the menu): the session routes - * touches outside the controls by them. */ var onOptions: (SteamTouchConfig.Options) -> Unit = {} - /** The Paste control: types the clipboard into the session. */ var onPaste: () -> Unit = {} - // Steam Link's own settings, for every game (CVirtualControllerGlobalConfig). private val prefs = context.getSharedPreferences("steam_touch", Context.MODE_PRIVATE) private var feedback = prefs.getBoolean("feedback", false) private var autoFade = prefs.getBoolean("autoFade", true) private var gyroOn = prefs.getBoolean("gyro", true).also { device.motionEnabled = it } - // Shake the device to hide the controls and again to bring them back (Steam Link's shake_fade). private var shakeToHide = prefs.getBoolean("shake", false) private var shakenAway = false private val sensors by lazy { context.getSystemService(Context.SENSOR_SERVICE) as android.hardware.SensorManager } @@ -69,7 +52,6 @@ class SteamTouchControls( val now = android.os.SystemClock.uptimeMillis() jolts = if (now - lastJolt < 400) jolts + 1 else 1 lastJolt = now - // Three jolts in quick succession, at most once a second. if (jolts >= 3 && now - lastShake > 1000) { lastShake = now jolts = 0 @@ -113,15 +95,12 @@ class SteamTouchControls( private var tint = Color.WHITE private var alphaScale = 0.45f - // What each control is bound to (its icon, label, colours), per element type; the D-pad has four. private class Visual(val ref: SteamTouchBindings.Ref?, val binding: SteamTouchBindings.Binding?) private var visuals: Map<Int, List<Visual>> = emptyMap() - // Touch state. private class Finger(val element: Element?, var x: Float, var y: Float, val toolbar: Int = -1) private val fingers = HashMap<Int, Finger>() - // Editing. private var editing = false private var editElements: MutableList<Element> = mutableListOf() private var hidden: MutableList<Element> = mutableListOf() @@ -130,7 +109,6 @@ class SteamTouchControls( private var pinchStart = 0f private var pinchScale = 1f private var saving = false - // Icon and colour edits not saved yet. private val pendingBindings = HashMap<SteamTouchBindings.Ref, SteamTouchBindings.Binding>() private var pendingColor: FloatArray? = null private var selectedArm = 0 @@ -162,12 +140,9 @@ class SteamTouchControls( super.onDetachedFromWindow() } - /** Follows what the client tells the device: a new app loads its config, a new action set or - * layer redraws. */ private fun refreshState() { val state = device.state() val app = if (state.appId == 0) BIG_PICTURE else state.appId - // The client reads the config afresh when the controller (re)connects: so do we. val reconnected = state.opened > 0 && opened == 0 opened = state.opened if (app != configApp || reconnected) { @@ -195,7 +170,6 @@ class SteamTouchControls( } } - /** Re-reads the config for the current app (after a save). */ fun reloadConfig() { configApp = -1 refreshState() @@ -216,12 +190,9 @@ class SteamTouchControls( private fun applyColor(color: FloatArray?) { tint = if (color != null) Color.rgb((color[0] * 255).toInt(), (color[1] * 255).toInt(), (color[2] * 255).toInt()) else Color.WHITE - // Steam Link's opacity: its default layout colour (alpha about 0.4) draws solid white - // outlines; lower values fade them. alphaScale = ((color?.get(3) ?: 0.45f) * 2.2f).coerceIn(0.15f, 1f) } - /** Each control's bindings for the action set and layers in use, with edits not saved yet. */ private fun rebuildVisuals() { val mappings = config.mappings val preset = SteamTouchConfig.layoutIdOf(actionSet) - 1 @@ -240,7 +211,6 @@ class SteamTouchControls( } } - // ---- Geometry ---- private fun unit() = min(width / 1280f, height / 720f) @@ -248,7 +218,6 @@ class SteamTouchControls( private fun radiusY(e: Element): Float = baseRadius(e.type) * e.yScale * unit() private fun baseRadius(type: Int) = when (type) { - // Steam Link's sizes on its 1280x720 reference. SteamTouchConfig.DPAD -> 112f SteamTouchConfig.JOYSTICK_LEFT, SteamTouchConfig.JOYSTICK_RIGHT -> 90f SteamTouchConfig.TRACKPAD_LEFT, SteamTouchConfig.TRACKPAD_RIGHT, SteamTouchConfig.TRACKPAD_CENTER -> 100f @@ -263,17 +232,14 @@ class SteamTouchControls( private fun cx(e: Element) = e.x * width private fun cy(e: Element) = e.y * height - /** How far (x, y) is from [e]'s centre, in its own radii (it may be an oval). */ private fun reach(e: Element, x: Float, y: Float) = hypot((x - cx(e)) / radius(e), (y - cy(e)) / radiusY(e)) private fun hit(list: List<Element>, x: Float, y: Float): Element? = list.filter { reach(it, x, y) <= 1.15f }.minByOrNull { reach(it, x, y) } - /** What is drawn and touchable: in mouse mode only the menu button, as on Steam Link. */ private fun live(): List<Element> = if (shakenAway) emptyList() else if (options.inputMode == SteamTouchConfig.INPUT_MOUSE) elements.filter { it.type == SteamTouchConfig.THUMB } else elements - // ---- Drawing ---- override fun onDraw(canvas: Canvas) { val shown = if (editing) editElements else live() @@ -281,7 +247,6 @@ class SteamTouchControls( val x = cx(e) val y = cy(e) canvas.save() - // An oval control: drawn as its circle, stretched to its height. if (e.yScale != e.xScale && e.xScale > 0f) canvas.scale(1f, e.yScale / e.xScale, x, y) draw(canvas, e, editing && editElements.indexOf(e) == selected) canvas.restore() @@ -297,10 +262,6 @@ class SteamTouchControls( private fun pressedTypes(): Set<Int> = fingers.values.mapNotNull { it.element?.type }.toSet() - // ---- Steam Link's look (measured from Steam Link 1.3 on the Thor) ---- - // Outlines only: 3 px white strokes on a 1280x720 reference, no fill; a pressed control fills - // white with its content dark. Face letters in Steam's colours; Select and Start are ◀ ▶ pills - // beside a solid white Steam disc; bumpers, triggers, the menu and keyboard are rounded squares. private fun lineWidth() = 3f * unit() @@ -340,7 +301,6 @@ class SteamTouchControls( } } SteamTouchConfig.STEAM -> { - // The guide button: DroidDeck's split D and orb, in a ring. if (pressed) canvas.drawCircle(x, y, r, fill) canvas.drawCircle(x, y, r, stroke) drawMark(canvas, x, y, r * 0.5f, if (pressed) Color.argb(alpha, 23, 26, 33) else ink, alpha) @@ -364,7 +324,6 @@ class SteamTouchControls( } } - /** Controls drawn as rounded squares on Steam Link; the face buttons and the rest are rings. */ private fun isSquare(type: Int) = type in setOf(SteamTouchConfig.BUMPER_LEFT, SteamTouchConfig.BUMPER_RIGHT, SteamTouchConfig.TRIGGER_LEFT, SteamTouchConfig.TRIGGER_RIGHT, SteamTouchConfig.THUMB, SteamTouchConfig.KEYBOARD, SteamTouchConfig.PASTE, SteamTouchConfig.MACRO_1_FINGER, SteamTouchConfig.MACRO_2_FINGER) || @@ -372,8 +331,6 @@ class SteamTouchControls( private val markD by lazy { androidx.core.graphics.PathParser.createPathFromPathData(MARK_D) } - /** DroidDeck's mark (artwork/droiddeck-mark.svg): the split D in [ink] around the blue orb, - * centred at (x, y), [r] its half height. */ private fun drawMark(canvas: Canvas, x: Float, y: Float, r: Float, ink: Int = onSurface, alpha: Int = 255) { val k = r / 111.86f canvas.save() @@ -384,7 +341,6 @@ class SteamTouchControls( canvas.restore() } - /** The Steam logo's mark, drawn: a wheel and a piston rod. */ private fun drawSteamMark(canvas: Canvas, x: Float, y: Float, r: Float, color: Int) { val p = Paint(Paint.ANTI_ALIAS_FLAG).apply { this.color = color; style = Paint.Style.STROKE; strokeCap = Paint.Cap.ROUND } p.strokeWidth = r * 0.16f @@ -396,11 +352,6 @@ class SteamTouchControls( canvas.drawLine(x - r * 0.45f, y + r * 0.28f, x - r * 0.95f, y + r * 0.1f, p) } - /** - * A button as Steam Link draws it: a ring (or rounded square) outline with its name - face - * letters in Steam's colours. With a binding icon, the icon on a disc of its background colour, - * as Steam draws binding icons; with a binding label, the label. - */ private fun drawFace(canvas: Canvas, type: Int, binding: SteamTouchBindings.Binding?, x: Float, y: Float, r: Float, alpha: Int, pressed: Boolean) { val square = isSquare(type) @@ -412,7 +363,6 @@ class SteamTouchControls( val s = r * 0.62f val custom = binding.background.isNotEmpty() && !binding.background.equals(SteamTouchBindings.DEFAULT_BACKGROUND, true) if (custom) { - // Colours chosen for this icon: drawn as Steam draws binding icons. shape(Paint(Paint.ANTI_ALIAS_FLAG).apply { color = parseColor(binding.background, SteamTouchBindings.DEFAULT_BACKGROUND) this.alpha = if (pressed) alpha else (alpha * 0.85f).toInt() @@ -420,7 +370,6 @@ class SteamTouchControls( canvas.drawBitmap(tinted(binding.icon, icon, binding.foreground), null, RectF(x - s, y - s, x + s, y + s), Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG).apply { this.alpha = alpha }) } else { - // Steam's default colours: Steam Link's own look, the icon in the outline's ink. if (pressed) shape(fill) val ink = if (pressed) Color.argb(alpha, 23, 26, 33) else stroke.color canvas.drawBitmap(inked(binding.icon, icon), null, RectF(x - s, y - s, x + s, y + s), @@ -435,7 +384,6 @@ class SteamTouchControls( shape(stroke) if (type == SteamTouchConfig.THUMB) { drawDots(canvas, x, y, r, if (pressed) Color.argb(alpha, 23, 26, 33) else text.color); return } if (type == SteamTouchConfig.KEYBOARD) { drawKeyboardGlyph(canvas, x, y, r, if (pressed) Color.argb(alpha, 23, 26, 33) else text.color); return } - // Steam Link names a control by itself, whatever its binding is labelled ("B", not "Back"). val own = label(type) val shown = own text.textSize = when { @@ -469,14 +417,11 @@ class SteamTouchControls( private val iconsLoading = HashSet<String>() - // Icons as a mask (their alpha), tinted when drawn. private val inkedIcons = android.util.LruCache<String, android.graphics.Bitmap>(48) private fun inked(name: String, icon: android.graphics.Bitmap): android.graphics.Bitmap { val key = "$name|${icon.width}" inkedIcons.get(key)?.let { return it } - // Steam's icons are drawn in greys on clear; their dark parts are the glyph's detail, so the - // mask keeps what is opaque and not near white. val out = icon.copy(android.graphics.Bitmap.Config.ARGB_8888, true) val px = IntArray(out.width * out.height) out.getPixels(px, 0, out.width, 0, 0, out.width, out.height) @@ -491,8 +436,6 @@ class SteamTouchControls( return out } - // Icons already in their foreground colour. Drawn plain: a colour filter on the icon bitmap - // itself stopped drawing on the hardware canvas once a dialog had been over the view. private val tintedIcons = android.util.LruCache<String, android.graphics.Bitmap>(48) private fun tinted(name: String, icon: android.graphics.Bitmap, foreground: String): android.graphics.Bitmap { @@ -505,7 +448,6 @@ class SteamTouchControls( return out } - /** An icon from the cache, or null while it is (re)loaded; the view redraws when it comes in. */ private fun iconOrLoad(name: String): android.graphics.Bitmap? { SteamTouchBindings.cachedIcon(appId, name, ICON_PX)?.let { return it } if (iconsLoading.add(name)) { @@ -521,17 +463,15 @@ class SteamTouchControls( private fun parseColor(hex: String, fallback: String) = try { Color.parseColor(hex.ifEmpty { fallback }) } catch (_: IllegalArgumentException) { Color.parseColor(fallback) } - /** Steam Link's d-pad: four shield-shaped arms pointing at the centre, outlined. */ private fun drawDpad(canvas: Canvas, e: Element, x: Float, y: Float, r: Float) { val dirs = if (editing) 0 else dpadBits(e) val armVisuals = visuals[SteamTouchConfig.DPAD].orEmpty() val isSelected = editing && selected >= 0 && editElements.indexOf(e) == selected - val half = r * 0.36f // half the arm's width - val outer = r // the arm's far end - val inner = r * 0.1f // the point, near the centre - val shoulder = r * 0.42f // where the sides turn into the point + val half = r * 0.36f + val outer = r + val inner = r * 0.1f + val shoulder = r * 0.42f val corner = r * 0.12f - // Arms in report order: up, down, left, right, as (bit, rotation). val arms = listOf(DPAD_UP to 0f, DPAD_DOWN to 180f, DPAD_LEFT to 270f, DPAD_RIGHT to 90f) val oldStroke = stroke.color for ((armIndex, armEntry) in arms.withIndex()) { @@ -561,7 +501,6 @@ class SteamTouchControls( canvas.drawPath(path, stroke) canvas.restore() if (icon != null) { - // Upright, in the middle of the arm. val c = (outer + shoulder) / 2f val (ax, ay) = when (armIndex) { 0 -> x to y - c; 1 -> x to y + c; 2 -> x - c to y; else -> x + c to y } val s = half * 0.8f @@ -595,7 +534,6 @@ class SteamTouchControls( else -> "?" } - // ---- The report ---- private fun dpadBits(e: Element): Long { var bits = 0L @@ -604,7 +542,6 @@ class SteamTouchControls( val dy = f.y - cy(e) if (hypot(dx, dy) < radius(e) * 0.22f) return@forEach val angle = Math.toDegrees(atan2(-dy, dx).toDouble()).let { if (it < 0) it + 360 else it } - // Eight sectors: each direction covers 67.5°, diagonals press two. if (angle < 67.5 || angle > 292.5) bits = bits or DPAD_RIGHT if (angle in 22.5..157.5) bits = bits or DPAD_UP if (angle in 112.5..247.5) bits = bits or DPAD_LEFT @@ -633,7 +570,7 @@ class SteamTouchControls( buttons = buttons or if (right) STICK_RIGHT_TOUCHED else STICK_LEFT_TOUCHED } SteamTouchConfig.TRACKPAD_CENTER, SteamTouchConfig.TRACKPAD_LEFT, SteamTouchConfig.TRACKPAD_RIGHT -> { - val index = e.type - SteamTouchConfig.TRACKPAD_CENTER // centre, left, right + val index = e.type - SteamTouchConfig.TRACKPAD_CENTER val nx = ((f.x - (cx(e) - r)) / (2 * r)).coerceIn(0f, 1f) val ny = ((f.y - (cy(e) - radiusY(e))) / (2 * radiusY(e))).coerceIn(0f, 1f) pads[2 * index] = (((nx * 65535).toInt().coerceIn(0, 65535)) xor 0x8000).toShort() @@ -653,7 +590,6 @@ class SteamTouchControls( invalidate() } - // ---- Touch ---- @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { @@ -694,7 +630,6 @@ class SteamTouchControls( val f = fingers[event.getPointerId(i)] ?: continue f.x = event.getX(i) f.y = event.getY(i) - // A finger on a button can slide onto another, as on Steam Link. val e = f.element ?: continue if (BUTTON_BITS.containsKey(e.type)) { val over = hit(live(), f.x, f.y) @@ -726,12 +661,9 @@ class SteamTouchControls( return true } - // ---- Editing ---- val isEditing get() = editing - /** Moves, resizes and hides controls, action set by action set; Save writes them to the - * game's touch config. */ fun startEditing() { releaseAll() editing = true @@ -744,7 +676,6 @@ class SteamTouchControls( invalidate() } - // The action set being edited (its layout id), and the edits of every set visited. private var editSet = 1 private class EditState(val elements: MutableList<Element>, val hidden: MutableList<Element>) private val editSets = HashMap<Int, EditState>() @@ -752,7 +683,6 @@ class SteamTouchControls( private fun loadEditSet(id: Int) { editSet = id val state = editSets.getOrPut(id) { - // The device's action set numbering is the layout id. val available = config.availableFor(id, emptyList()) val shown = SteamTouchConfig.elementsFor(config, id, emptyList()) val els = available.mapNotNull { type -> @@ -816,7 +746,6 @@ class SteamTouchControls( val e = hit(editElements, x, y) selected = if (e != null) editElements.indexOf(e) else -1 if (e != null) dragOffset = (x - cx(e)) to (y - cy(e)) - // The D-pad's icons are per direction: a tap picks the one under the finger. if (e?.type == SteamTouchConfig.DPAD) { val dx = x - cx(e) val dy = y - cy(e) @@ -832,7 +761,6 @@ class SteamTouchControls( MotionEvent.ACTION_MOVE -> if (selected >= 0 && !trayOpen) { val e = editElements[selected] if (event.pointerCount >= 2 && pinchStart > 0f) { - // A pinch along one axis stretches that axis; a diagonal one scales both. val sx = abs(event.getX(0) - event.getX(1)) val sy = abs(event.getY(0) - event.getY(1)) val min = 40f * unit() @@ -850,7 +778,6 @@ class SteamTouchControls( MotionEvent.ACTION_POINTER_UP -> pinchStart = 0f MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { pinchStart = 0f - // Dropped back on the menu button: back into the tray, as on Steam Link. val e = editElements.getOrNull(selected) if (e != null && editMenuButton().contains(cx(e), cy(e)) && e.type != SteamTouchConfig.THUMB) { hidden += editElements.removeAt(selected).copy(visible = false) @@ -893,7 +820,6 @@ class SteamTouchControls( else -> label(type) } - /** Reset, copy and paste a layout, the layout's colour. */ private fun moreMenu() { val host = parent as? android.view.ViewGroup ?: return val density = resources.displayMetrics.density @@ -918,7 +844,6 @@ class SteamTouchControls( close = SteamTouchIconPicker.showPanel(host, context.getString(R.string.ctrl_layout), list, listOf(context.getString(R.string.common_close) to {})) } - /** A copied layout onto this action set: each control the set has takes the copied place. */ private fun pasteLayout(copied: String) { val placed = copied.split(';').mapNotNull { row -> val f = row.split(',') @@ -932,30 +857,21 @@ class SteamTouchControls( selected = -1 } - // ---- Steam Link's menu ---- - /** The menu button: Steam Link's touch menu. */ private fun openMenu() { releaseAll() menuOpen = true invalidate() } - // ---- Steam Link's panels: the touch menu and the layout tray ---- - // - // Drawn here, on this view, to Steam Link's measurements (a 624x400 panel in its reference - // pixels, navy with a blue top rule, blue tiles), so that controls can be dragged out of the tray - // onto the screen as on Steam Link. private var menuOpen = false private var trayOpen = false - /** A hit area on a panel and what it does. */ private class Hot(val rect: RectF, val onTap: () -> Unit, val onDown: ((Float, Float) -> Unit)? = null) private val hots = mutableListOf<Hot>() private var trayDrag: Element? = null - // DroidDeck's own palette (ui/Theme.kt, the default theme). private val surface = Color.rgb(18, 20, 23) private val surfaceVariant = Color.rgb(26, 29, 34) private val line = Color.rgb(38, 42, 49) @@ -965,7 +881,6 @@ class SteamTouchControls( private val accent = Color.rgb(26, 159, 255) private val onAccent = Color.rgb(3, 17, 31) - /** The panel's rectangle and its scale (screen pixels per reference pixel). */ private fun panelFrame(): Pair<RectF, Float> { val s = min(width * 0.8f / 624f, height * 0.78f / 400f) val w = 624f * s @@ -995,8 +910,6 @@ class SteamTouchControls( canvas.drawText(str, x, y - (p.descent() + p.ascent()) / 2, p) } - /** A button in DroidDeck's style: the accent filled for the main action, a quiet outlined - * surface otherwise. */ private fun blueButton(canvas: Canvas, r: RectF, text: String, s: Float, enabled: Boolean = true, primary: Boolean = false, onTap: () -> Unit) { val radius = 12f * s if (primary && enabled) { @@ -1010,7 +923,6 @@ class SteamTouchControls( if (enabled) hots += Hot(r, onTap) } - /** A toggle: quiet when off, accent-tinted with an accent glyph when on. */ private fun iconButton(canvas: Canvas, r: RectF, active: Boolean, onTap: () -> Unit, glyph: (Canvas, RectF, Int) -> Unit) { val radius = 10f * (r.height() / 40f) canvas.drawRoundRect(r, radius, radius, Paint(Paint.ANTI_ALIAS_FLAG).apply { @@ -1055,12 +967,10 @@ class SteamTouchControls( } } - // -- The touch menu (Steam Link: CVirtualController's choose-mode screen) -- private fun drawMenu(canvas: Canvas) { val (f, s) = panelFrame() canvas.panel(f, s) - // Header: power (DroidDeck's own menu: stop, settings), the Steam mark, what is running, close. iconButton(canvas, ref(f, s, 494f, 18f, 531f, 55f), false, { menuOpen = false; onMenu() }) { c, r, col -> drawPower(c, r, col) } drawMark(canvas, f.left + 104f * s, f.top + 37f * s, 13f * s) text(canvas, context.getString(R.string.touch_title), f.left + 124f * s, f.top + 30f * s, 14f * s, onSurface, align = Paint.Align.LEFT) @@ -1079,7 +989,6 @@ class SteamTouchControls( tile(canvas, ref(f, s, 271f, 81f, 475f, 271f), s, context.getString(R.string.touch_mouse), mouse, { setModes(controller || mouse, !mouse) }) { c, r, col -> drawMouse(c, r, col, crossed = false) } - // Under the controller: gyroscope, fade, vibrate on touch. iconButton(canvas, ref(f, s, 45f, 278f, 91f, 318f), gyroOn, { gyroOn = !gyroOn; device.motionEnabled = gyroOn; prefs.edit().putBoolean("gyro", gyroOn).apply() }) { c, r, col -> drawGyro(c, r, col) } @@ -1092,7 +1001,6 @@ class SteamTouchControls( iconButton(canvas, ref(f, s, 204f, 278f, 249f, 318f), feedback, { feedback = !feedback; prefs.edit().putBoolean("feedback", feedback).apply() }) { c, r, col -> drawHaptic(c, r, col) } - // Under the mouse: direct touch, trackpad, trackpad speed. val relative = options.mouseMode == SteamTouchConfig.MOUSE_RELATIVE iconButton(canvas, ref(f, s, 271f, 278f, 333f, 318f), mouse && !relative, { setOptions(options.copy(mouseMode = SteamTouchConfig.MOUSE_ABSOLUTE)) @@ -1116,8 +1024,6 @@ class SteamTouchControls( private val audio by lazy { context.getSystemService(Context.AUDIO_SERVICE) as android.media.AudioManager } private var volumeSlider = RectF() - /** Steam Link's audio column: the speaker (mute), its volume slider - the device's media - * volume here - and the microphone, which DroidDeck does not stream, shown off. */ private fun drawVolume(canvas: Canvas, f: RectF, s: Float) { val stream = android.media.AudioManager.STREAM_MUSIC val maxVol = audio.getStreamMaxVolume(stream).coerceAtLeast(1) @@ -1136,7 +1042,6 @@ class SteamTouchControls( canvas.drawRoundRect(RectF(inner.left, knobY, inner.right, inner.bottom), inner.width() / 2, inner.width() / 2, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = accent }) canvas.drawCircle(inner.centerX(), knobY, inner.width() / 2 * 1.05f, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = onSurface }) - // The mic's slider, dimmed. val micTrack = ref(f, s, 539f, 133f, 577f, 372f) canvas.drawRoundRect(micTrack, micTrack.width() / 2, micTrack.width() / 2, Paint(stroke).apply { color = Color.argb(60, 255, 255, 255) }) hots += Hot(track, {}) { _, y -> setVolumeAt(y) } @@ -1171,7 +1076,6 @@ class SteamTouchControls( c.drawLine(x - u * 2.4f, y - u * 2.4f, x + u * 2.4f, y + u * 2.4f, glyphPaint(Color.rgb(200, 40, 40), r.height() * 0.07f)) } - /** A mode card: tap to turn it on or off; the accent marks it on. */ private fun tile(canvas: Canvas, r: RectF, s: Float, title: String, on: Boolean, toggle: () -> Unit, art: (Canvas, RectF, Int) -> Unit) { val radius = 14f * s canvas.drawRoundRect(r, radius, radius, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = surfaceVariant }) @@ -1179,7 +1083,6 @@ class SteamTouchControls( color = if (on) accent else line; style = Paint.Style.STROKE; strokeWidth = (if (on) 2f else 1.2f) * s }) text(canvas, title, r.left + 16f * s, r.top + 22f * s, 14f * s, onSurface, align = Paint.Align.LEFT, maxWidth = r.width() - 72f * s) - // A switch, top right. val sw = RectF(r.right - 50f * s, r.top + 12f * s, r.right - 14f * s, r.top + 32f * s) canvas.drawRoundRect(sw, sw.height() / 2, sw.height() / 2, Paint(Paint.ANTI_ALIAS_FLAG).apply { color = if (on) accent else line2 }) canvas.drawCircle(if (on) sw.right - sw.height() / 2 else sw.left + sw.height() / 2, sw.centerY(), sw.height() * 0.38f, @@ -1190,7 +1093,6 @@ class SteamTouchControls( hots += Hot(r, toggle) } - // -- The layout tray (Steam Link: CVirtualController's tray) -- private fun drawTray(canvas: Canvas) { val (f, s) = panelFrame() @@ -1246,7 +1148,6 @@ class SteamTouchControls( item(SteamTouchConfig.MACRO_0 + i, 519f, 228f + 43f * i, 14f) item(SteamTouchConfig.MACRO_0 + 4 + i, 562f, 228f + 43f * i, 14f) } - // The colour wheel: the layout's colour and opacity. val wheel = ref(f, s, 511f, 91f, 573f, 153f) canvas.drawCircle(wheel.centerX(), wheel.centerY(), wheel.width() / 2f, Paint(Paint.ANTI_ALIAS_FLAG).apply { shader = android.graphics.SweepGradient(wheel.centerX(), wheel.centerY(), @@ -1270,7 +1171,6 @@ class SteamTouchControls( hots += Hot(RectF(r.left - r.height() * 0.4f, r.top - r.height() * 0.6f, r.right + r.height() * 0.4f, r.bottom + r.height() * 0.6f), onTap) } - /** A control taken out of the tray: on screen under the finger, following it until it lifts. */ private fun beginTrayDrag(type: Int, x: Float, y: Float) { val available = config.availableFor(editSet, emptyList()) val existing = editElements.indexOfFirst { it.type == type } @@ -1286,7 +1186,6 @@ class SteamTouchControls( invalidate() } - // -- While editing: Steam Link's menu button, and a small bar for the selected control -- private fun editMenuButton(): RectF { val r = 37f * unit() @@ -1340,7 +1239,6 @@ class SteamTouchControls( invalidate() } - // -- Glyphs for the menu's buttons -- private fun glyphPaint(col: Int, w: Float) = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = col; style = Paint.Style.STROKE; strokeWidth = w; strokeCap = Paint.Cap.ROUND; strokeJoin = Paint.Join.ROUND @@ -1430,8 +1328,6 @@ class SteamTouchControls( Paint(Paint.ANTI_ALIAS_FLAG).apply { color = col }) } - /** New per-game input options: applied now, and saved into the game's touch layout as Steam - * Link saves them, so they follow the game and the account. */ private fun setOptions(next: SteamTouchConfig.Options) { pendingOptions = next options = next @@ -1449,7 +1345,6 @@ class SteamTouchControls( } } - /** The selected control's icon, label and colours (a D-pad direction: the one tapped). */ private fun pickIcon(e: Element) { val list = visuals[e.type].orEmpty() val visual = list.getOrNull(if (e.type == SteamTouchConfig.DPAD) selectedArm else 0) @@ -1470,7 +1365,6 @@ class SteamTouchControls( }.show() } - /** The layout's colour and opacity, as Steam Link's colour picker sets them. */ private fun pickColor() { val current = pendingColor ?: SteamTouchConfig.layoutColor(config, actionSet) ?: floatArrayOf(1f, 1f, 1f, 0.45f) val density = resources.displayMetrics.density @@ -1521,8 +1415,6 @@ class SteamTouchControls( } val color = pendingColor val colors = if (color != null) sets.keys.associateWith { color } else emptyMap() - // A picked icon replaces the label, icon and colours of every binding of that input; what - // the binding does stays as it is. val bindings = pendingBindings.mapValues { (_, picked) -> { old: SteamTouchBindings.Binding -> old.copy(label = picked.label, icon = picked.icon, foreground = picked.foreground, background = picked.background) @@ -1550,7 +1442,6 @@ class SteamTouchControls( } } - /** Called after a save: the client reads the config again when the controller reconnects. */ var onSaved: () -> Unit = {} companion object { @@ -1558,7 +1449,6 @@ class SteamTouchControls( private const val POLL_MS = 150L const val BIG_PICTURE = 769 private const val ICON_PX = 128 - // artwork/droiddeck-mark.svg: the D's two halves, the orb's ring cut out (Wordmark.kt). private const val MARK_D = "M-31.98 0H56.055V40.489A71.81 71.81 0 0 0 56.055 183.231V223.72H-31.98Z" + "M71.885 0.28A111.86 111.86 0 0 1 71.885 223.44V183.234A71.81 71.81 0 0 0 71.885 40.486Z" private const val FADE_AFTER_MS = 5000L @@ -1573,7 +1463,6 @@ class SteamTouchControls( private const val STICK_RIGHT_TOUCHED = 1L shl 47 private val TRACKPAD_TOUCHED = longArrayOf(1L shl 27, 1L shl 19, 1L shl 20) - /** Steam Link's button bits (CVirtualController's table, by element type). */ private val BUTTON_BITS: Map<Int, Long> = buildMap { put(SteamTouchConfig.STEAM, 1L shl 13) put(SteamTouchConfig.JOYSTICK_LEFT_BUTTON, 1L shl 22) @@ -1591,7 +1480,6 @@ class SteamTouchControls( for (i in 0 until 8) put(SteamTouchConfig.MACRO_0 + i, 1L shl (32 + i)) put(SteamTouchConfig.MACRO_1_FINGER, 1L shl 48) put(SteamTouchConfig.MACRO_2_FINGER, 1L shl 49) - // Not bits: the menu and the keyboard are the app's own. put(SteamTouchConfig.THUMB, 0L) put(SteamTouchConfig.KEYBOARD, 0L) put(SteamTouchConfig.PASTE, 0L) diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchDevice.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchDevice.kt index f0c30b971..2d63358d3 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchDevice.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchDevice.kt @@ -7,20 +7,8 @@ import java.nio.ByteOrder import java.nio.ByteBuffer import java.nio.channels.FileChannel -/** - * Steam's touch controller, as the app's side of it. - * - * libfakeinput offers the Steam client a HID device with the ids of Steam Link's touch controls - * (0000:11fb), and the client builds its own touch controller for it: Steam Input's Mobile Touch - * type, with the game's touch config, action sets, community configs and cloud sync all Steam's - * (docs/development/steam-touch-controller.md). This class is the file the two share - * (fakeinput_steam.cpp: TouchRingFile): the app writes the device's 40-byte input report and - * whether it is plugged in; libfakeinput writes back what the client told the device - the app and - * action set it is on, its layers, rumble. - */ class SteamTouchDevice private constructor(private val buffer: ByteBuffer) { - /** What the client last told the device. */ data class State( val opened: Int, val appId: Int, @@ -29,13 +17,9 @@ class SteamTouchDevice private constructor(private val buffer: ByteBuffer) { val actionSeq: Int, ) - // The input report (Steam Link's CVirtualController, docs/development/steam-touch-controller.md): - // bytes 0-7 buttons, 8-15 sticks, 16-27 trackpads, 28-33 accelerometer, 34-39 gyro. private val report = ByteArray(REPORT_BYTES) private var reportSeq = 0L - /** The controls part of the report: button bits, sticks (LX, LY, RX, RY, already in the report's - * units) and trackpads (centre, left, right: x, y each). */ @Synchronized fun setControls(buttons: Long, sticks: ShortArray, pads: ShortArray) { val next = report.copyOf() @@ -45,27 +29,19 @@ class SteamTouchDevice private constructor(private val buffer: ByteBuffer) { publish(next) } - /** Motion in the Deck's axes and units (PadMotion), which the touch report shares. */ @Synchronized fun setMotion(accel: ShortArray, gyro: ShortArray) { - // Sent only while the client asks for it (setting report 0x30, which Steam Link reads as - // "gyro on") and the player has not turned motion off - as Steam Link does. if (!plugged || !gyroRequested || !motionEnabled) return val next = report.copyOf() for (i in 0 until 3) { - // Accelerometer: the Deck's 1 g = 16384 is the touch report's ±2 g = ±32767 already. putShort(next, 28 + 2 * i, accel[i]) - // Gyro: the Deck's 2000 °/s = 32768; the touch report's full scale is 1000 °/s - // (Steam Link: rad/s × 180/π / 1000 × 32767). putShort(next, 34 + 2 * i, (gyro[i] * 2).coerceIn(-32767, 32767).toShort()) } publish(next) } - /** Whether the client has asked the device for motion (setting report 0x30 non-zero). */ val gyroRequested get() = buffer.getInt(OFF_SETTING30) != 0 - /** The player's motion switch (Steam Link's gyroscope option). */ @Volatile var motionEnabled = true set(value) { field = value @@ -81,7 +57,6 @@ class SteamTouchDevice private constructor(private val buffer: ByteBuffer) { into[at + 1] = (value.toInt() shr 8).toByte() } - /** Writes the report under the seqlock libfakeinput reads it with. */ private fun publish(next: ByteArray) { if (next.contentEquals(report)) return next.copyInto(report) @@ -122,7 +97,7 @@ class SteamTouchDevice private constructor(private val buffer: ByteBuffer) { private const val TAG = "SteamTouch" const val REPORT_BYTES = 40 private const val SIZE = 256 - private const val MAGIC = 0x31484354 // TCH1 + private const val MAGIC = 0x31484354 private const val OFF_REPORT_SEQ = 8 private const val OFF_REPORT = 16 private const val OFF_PLUGGED = 56 @@ -140,10 +115,8 @@ class SteamTouchDevice private constructor(private val buffer: ByteBuffer) { var current: SteamTouchDevice? = null private set - /** The shared file in the session's pad directory, made fresh each session. */ fun ringFile(fakeInputDir: File) = File(fakeInputDir, "touchctl") - /** Creates the file for a new session (unplugged) and maps it. */ fun prepare(fakeInputDir: File): File? = try { val file = ringFile(fakeInputDir) file.delete() diff --git a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt index fdd6f423f..17d665f50 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/SteamTouchIconPicker.kt @@ -24,11 +24,6 @@ import android.widget.TextView import com.droiddeck.launcher.R import java.util.concurrent.Executors -/** - * Picks a touch control's icon, label and colours the way Steam's configurator does: an icon from - * the game's own `TouchMenuIcons` and Steam's binding icon library, a foreground and a background - * from Steam's palette. [onPicked] gets the binding with the new fields (the action untouched). - */ class SteamTouchIconPicker( private val context: Context, private val host: ViewGroup, @@ -47,7 +42,6 @@ class SteamTouchIconPicker( private fun dp(v: Int) = (v * density).toInt() fun show() { - // Side by side, for a landscape screen: what is picked on the left, the icons on the right. val root = LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL; setPadding(dp(16), dp(8), dp(16), 0) } val left = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL } val label = EditText(context).apply { @@ -132,8 +126,6 @@ class SteamTouchIconPicker( } } - /** The icon as Steam draws it: on a disc of the background colour, filled with the foreground - * through its own alpha. */ private fun render(icon: Bitmap, size: Int): Bitmap { val out = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) val canvas = Canvas(out) @@ -148,7 +140,6 @@ class SteamTouchIconPicker( private val loading = HashSet<String>() private var refreshQueued = false - /** Loads an icon in the background; the grid redraws from the cache once some have come in. */ private fun load(name: String) { if (!loading.add(name)) return loader.execute { @@ -189,7 +180,6 @@ class SteamTouchIconPicker( return view } - // One thumbnail per icon and colour, reused when the grid rebinds. private val thumbs = android.util.LruCache<String, Bitmap>(64) private fun tinted(name: String, icon: Bitmap): Bitmap { @@ -204,10 +194,6 @@ class SteamTouchIconPicker( } companion object { - /** - * A panel over the session, in the session's own window: a dialog's window of its own left - * new bitmaps and layers undrawn in the session window afterwards on the Thor. - */ fun showPanel(host: ViewGroup, title: String, content: View, buttons: List<Pair<String, () -> Unit>>, onClose: () -> Unit = {}): () -> Unit { val context = host.context val density = context.resources.displayMetrics.density @@ -217,7 +203,6 @@ class SteamTouchIconPicker( isClickable = true isFocusable = true } - // DroidDeck's panel: dark surface, rounded, a hairline border. val card = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL background = GradientDrawable().apply { @@ -266,8 +251,6 @@ class SteamTouchIconPicker( return { if (scrim.parent != null) close() } } - /** Steam's binding icon look (steamui: the icon multiplied with its foreground colour, - * background-blend-mode: multiply, keeping the icon's own alpha). */ fun drawSteamIcon(canvas: Canvas, icon: Bitmap, into: android.graphics.RectF, foreground: Int, paint: Paint) { val tint = Paint(paint).apply { colorFilter = PorterDuffColorFilter(foreground, PorterDuff.Mode.MULTIPLY) } canvas.drawBitmap(icon, null, into, tint) diff --git a/app/src/main/java/com/droiddeck/launcher/input/TouchpadGestures.kt b/app/src/main/java/com/droiddeck/launcher/input/TouchpadGestures.kt index a3ab87e5a..2dc403a43 100644 --- a/app/src/main/java/com/droiddeck/launcher/input/TouchpadGestures.kt +++ b/app/src/main/java/com/droiddeck/launcher/input/TouchpadGestures.kt @@ -5,30 +5,10 @@ import android.os.Looper import android.view.MotionEvent import kotlin.math.abs -/** - * The screen as a touchpad, the way Bannerlator's desktop containers work: the arrow is where it - * was, and a finger dragged anywhere moves it from there. - * - * - drag one finger .................. moves the pointer (relative, with a little acceleration) - * - tap .............................. left click, where the pointer is - * - second finger taps while one holds right click - * - two-finger tap ................... right click - * - two fingers up / down ............ scroll wheel - * - hold still, then drag ............ left-button drag - * - double-tap, hold, drag ........... left-button drag as well - * - one finger held, another slides .. left-button drag, as on a laptop's pad: the held finger is - * the button, the other moves the pointer (a window by its - * title bar or edge, a desktop icon). The sliding finger may - * lift and slide again; lifting the held one drops. - * - * Positions handed to the listener are the pointer's, in the view's pixels; the caller keeps them - * inside the picture. - */ class TouchpadGestures( private val slopPx: Float, private val listener: PointerGestures.Listener, ) { - /** The pointer, tracked here since every move is relative. */ var x = 0f private set var y = 0f @@ -49,7 +29,6 @@ class TouchpadGestures( private var twoFingers = false private var scrollAnchorY = 0f private var scrolled = false - // Hold-and-slide: the first finger stays as the button, the second moves the pointer. private var anchorId = -1 private var anchorStartX = 0f private var anchorStartY = 0f @@ -106,7 +85,6 @@ class TouchpadGestures( downAt = event.eventTime travelled = 0f; held = false; twoFingers = false; scrolled = false; secondWasTap = false anchorId = event.getPointerId(0); moverId = -1; holdDrag = false - // A tap shortly before this touch: holding now drags (double-tap-and-hold). if (event.eventTime - lastTapAt < DOUBLE_TAP_MS) { dragging = true listener.onButton(PointerGestures.BTN_LEFT, true, x, y) @@ -127,7 +105,6 @@ class TouchpadGestures( anchorStartX = event.getX(it); anchorStartY = event.getY(it) } } - // Mid-drag, a finger coming back only picks the sliding up again. if (holdDrag) return true twoFingers = true secondDownAt = event.eventTime @@ -140,8 +117,6 @@ class TouchpadGestures( val mi = if (moverId == -1) -1 else event.findPointerIndex(moverId) if (ai >= 0 && mi >= 0) { val mx = event.getX(mi); val my = event.getY(mi) - // One finger still, the other on the move: that is a drag, not a scroll (which - // moves both). Decided once, before anything has scrolled. if (!holdDrag && !scrolled) { val anchorMoved = abs(event.getX(ai) - anchorStartX) + abs(event.getY(ai) - anchorStartY) val moverMoved = abs(mx - moverStartX) + abs(my - moverStartY) @@ -165,7 +140,6 @@ class TouchpadGestures( return true } } else if (holdDrag) { - // Only the held finger is down: the drag waits for the next slide. return true } if (twoFingers && event.pointerCount >= 2) { @@ -186,7 +160,6 @@ class TouchpadGestures( travelled += abs(dx) + abs(dy) if (travelled > slopPx) { handler.removeCallbacks(longPress) - // A little acceleration: a slow finger is precise, a fast one crosses the screen. val speed = (abs(dx) + abs(dy)) val gain = sensitivity * (1f + minOf(speed / 40f, 1.5f)) place(x + dx * gain, y + dy * gain) @@ -198,7 +171,6 @@ class TouchpadGestures( val gone = event.getPointerId(event.actionIndex) if (holdDrag) { if (gone == anchorId) { - // The held finger lifted: drop. The other finger carries on as the pointer. listener.onButton(PointerGestures.BTN_LEFT, false, x, y) dragging = false; holdDrag = false; twoFingers = false anchorId = moverId; moverId = -1 @@ -213,13 +185,11 @@ class TouchpadGestures( } if (gone == moverId) moverId = -1 if (gone == anchorId) { - // The first finger left a two-finger gesture: the other one is the pointer now. anchorId = event.getPointerId(if (event.actionIndex == 0) 1 else 0) event.findPointerIndex(anchorId).takeIf { it >= 0 }?.let { lastX = event.getX(it); lastY = event.getY(it) } } - // The second finger lifting quickly, with nothing scrolled: a right click. if (twoFingers && secondWasTap && event.eventTime - secondDownAt < TAP_MS && !scrolled) { click(PointerGestures.BTN_RIGHT) secondWasTap = false @@ -232,7 +202,6 @@ class TouchpadGestures( when { dragging -> listener.onButton(PointerGestures.BTN_LEFT, false, x, y) twoFingers -> { - // Both fingers down and up together, no scroll: a right click. if (secondWasTap && !scrolled && !cancelled && event.eventTime - downAt < TAP_MS * 2) { click(PointerGestures.BTN_RIGHT) } diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/AppImageManager.kt b/app/src/main/java/com/droiddeck/launcher/runtime/AppImageManager.kt index 201acb1eb..4fe191a21 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/AppImageManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/AppImageManager.kt @@ -11,14 +11,6 @@ import com.droiddeck.launcher.core.FileUtils import java.io.File import java.io.RandomAccessFile -/** - * AppImages the user brings: picked from storage, checked, and extracted once into the runtime at - * /opt/appimages/user/<id>/app. proot has no FUSE to mount an image, and extracting it at every - * start would cost its whole size in /tmp each time. Each one gets a menu entry on the Linux - * desktop, and the front end starts it under gamescope through droiddeck-appimage-run. - * - * <id>/name holds the name to show, <id>/icon.png the icon when the image has a PNG one. - */ object AppImageManager { private const val TAG = "AppImageManager" const val GUEST_DIR = "/opt/appimages/user" @@ -29,7 +21,6 @@ object AppImageManager { val guestDir: String get() = "$GUEST_DIR/$id" } - /** Why a file cannot be imported: [text] with its [args]. */ internal class Problem(@StringRes val text: Int, vararg val args: Any) { fun describe(context: Context): String = context.getString(text, *args) } @@ -47,10 +38,6 @@ object AppImageManager { ) }?.sortedBy { it.name.lowercase() } ?: emptyList() - /** - * What is wrong with [file] as an AppImage for this device, or null when it can be imported. - * An AppImage is an ELF program (its runtime) with "AI" and the format version at byte 8. - */ internal fun problem(file: File): Problem? { if (!file.isFile) return Problem(R.string.user_apps_file_gone) val head = ByteArray(20) @@ -71,7 +58,6 @@ object AppImageManager { } } - /** A directory name from the image's file name: "Some_App-1.2-aarch64.AppImage" -> "some-app-1-2-aarch64". */ internal fun idFor(fileName: String, taken: Set<String>): String { val base = fileName.removeSuffix(".AppImage").removeSuffix(".appimage").lowercase() .replace(Regex("[^a-z0-9]+"), "-").trim('-').take(48).ifEmpty { "appimage" } @@ -83,17 +69,12 @@ object AppImageManager { fun dir(context: Context, id: String) = File(root(context), id) - /** - * Imports [file]; null on success, else what went wrong. [name] and [icon] replace the image's own; - * each of [meta] is written as a file beside them. - */ fun import( context: Context, file: File, name: String?, icon: File?, meta: Map<String, String> = emptyMap(), onProgress: (String) -> Unit, ): String? { if (!LinuxRuntime.isInstalled(context)) return context.getString(R.string.user_apps_runtime_required) problem(file)?.let { return it.describe(context) } val base = root(context).apply { mkdirs() } - // Room for the image while it is extracted, and the extracted tree (about the same again). if (StatFs(base.path).availableBytes < file.length() * 3) { return context.getString(R.string.appimg_no_space_import, FileUtils.sizeToString(file.length() * 3)) } @@ -114,7 +95,6 @@ object AppImageManager { } } - /** Swaps the program of imported [id] for [file], keeping its name and icon; the old one stays if this fails. */ fun replace(context: Context, id: String, file: File, meta: Map<String, String>, onProgress: (String) -> Unit): String? { problem(file)?.let { return it.describe(context) } val dir = dir(context, id) @@ -134,14 +114,11 @@ object AppImageManager { } } - /** Extracts [file] to [dir]/app, replacing what is there only once the extraction has worked. */ private fun extract(context: Context, dir: File, file: File, onProgress: (String) -> Unit): String? { val image = File(dir, "image.AppImage") val links = File(dir, ".links").apply { mkdirs() } try { dir.mkdirs() - // Copied in, not run where it lies: shared storage is mounted noexec, and proot's loader - // maps a program executable. onProgress(context.getString(R.string.user_apps_copying, file.name)) file.inputStream().use { input -> image.outputStream().use { FileUtils.copy(input, it) } } image.setExecutable(true, false) @@ -149,8 +126,6 @@ object AppImageManager { val unpack = if (LinuxFex.isX86(LinuxFex.elfMachine(image))) "${LinuxFex.TOOL} extract ./image.AppImage" else "./image.AppImage --appimage-extract || { rm -rf squashfs-root AppDir; ${LinuxFex.TOOL} extract ./image.AppImage; }" val out = StringBuilder() - // uruntime's DwarFS images unpack into AppDir with squashfs-root a link to it, and keep - // one program under several names as hard links (sharun), which Android denies apps. val status = GuestCommand.run(context, listOf( "/bin/bash", "-c", "cd \"$1\" && rm -rf squashfs-root AppDir && { $unpack; } >/dev/null && " + @@ -170,10 +145,6 @@ object AppImageManager { } } - /** - * Each name proot's link2symlink left for a hard link becomes a file of its own: a program run - * through a link would see the shared file's name, and sharun picks what to start by its own. - */ private fun copyLinkedFiles(app: File, links: File) { val roots = setOf(links.path + "/", links.canonicalPath + "/") app.walkTopDown().onEnter { !Files.isLink(it) }.forEach { f -> @@ -187,7 +158,6 @@ object AppImageManager { } } - /** Name, comment and icon from the image's own desktop entry; the file name when it has none. */ private fun describe(dir: File, source: File) { val app = File(dir, "app") val entry = app.listFiles { f -> f.isFile && f.name.endsWith(".desktop") }?.firstOrNull() @@ -200,11 +170,6 @@ object AppImageManager { restoreIcon(dir) } - /** - * The image's own icon as [dir]'s: of the files its entry names, beside it or anywhere in its - * icon theme, a PNG of at least [SHARP_ICON] px, else the SVG, else the largest PNG; .DirIcon - * without any. - */ internal fun restoreIcon(dir: File) { val app = File(dir, "app") val entry = app.listFiles { f -> f.isFile && f.name.endsWith(".desktop") }?.firstOrNull() @@ -229,7 +194,6 @@ object AppImageManager { f.inputStream().use { val b = ByteArray(4); it.read(b) == 4 && b[1] == 'P'.code.toByte() && b[2] == 'N'.code.toByte() && b[3] == 'G'.code.toByte() } } catch (e: Exception) { false } - /** A key of a desktop entry's main group, unlocalised. */ internal fun desktopKey(desktop: String, key: String): String? { var inMain = false for (line in desktop.lineSequence()) { @@ -241,11 +205,8 @@ object AppImageManager { } private fun menuEntry(context: Context, id: String) = - // A system directory, not ~/.local/share/applications: the desktop copies game entries from - // here into the user's with a GPU launcher (droiddeck-desktop-gpu), since it composites in software. File(LinuxRuntime.rootDir(context), "usr/local/share/applications/droiddeck-appimage-$id.desktop") - /** The Linux desktop's menu entry: the desktop's own Exec rules (and droiddeck-gpu) apply to it. */ internal fun writeMenuEntry(context: Context, dir: File) { val id = dir.name val name = FileUtils.readString(File(dir, "name"))?.trim() ?: id @@ -256,7 +217,6 @@ object AppImageManager { "Terminal=false\nCategories=$categories\nX-DroidDeck-AppImage=$id\n") } - /** The desktop's import of [key]; older ones of the image at [path], since replaced, are removed. */ fun desktopImport(context: Context, key: String, path: String): String? { var found: String? = null root(context).listFiles()?.forEach { dir -> diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt b/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt index c733c66da..777507a7a 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt @@ -18,26 +18,12 @@ import java.io.IOException import java.nio.file.Files import java.util.concurrent.atomic.AtomicInteger -/** - * Flatpak's sandboxes, started for the runtime's bwrap stand-in (droiddeck-bwrap). - * - * A Flatpak app runs with its runtime as /usr and itself as /app, which bwrap arranges with mount - * namespaces Android gives no app. proot can show the same picture, and started here - beside the - * session's proot rather than inside it - it traces the app once instead of twice. The stand-in - * sends the sandbox Flatpak asked for (its binds, directories, symlinks and files, in guest paths), - * this builds it under the cache and starts it, then streams the output, the pid and the exit - * status back until one side goes away: the stand-in ending takes the sandbox down with it. - * - * The socket is abstract, so anything on the device could connect: only this app's own uid - the - * uid every program in the runtime already runs as - is served. - */ object BwrapSpawner { private const val TAG = "BwrapSpawner" fun socket(context: Context): String = "${context.packageName}.bwrap" private val seq = AtomicInteger() @Volatile private var server: LocalServerSocket? = null - /** Listens for sandboxes for the rest of the process's life; later calls do nothing. */ @Synchronized fun start(context: Context) { if (server != null) return @@ -89,7 +75,6 @@ object BwrapSpawner { } } - /** What proot is started with: the sandbox root, its binds (host to guest), and the program. */ class Plan(val root: File, val binds: List<Bind>, val cwd: String, val argv: List<String>, val env: Map<String, String>) data class Bind(val host: String, val guest: String) { @@ -103,14 +88,9 @@ object BwrapSpawner { } } - /** True when [path] is [dir] or inside it. */ internal fun under(path: String, dir: String): Boolean = dir == "/" || path == dir || path.startsWith(if (dir.endsWith("/")) dir else "$dir/") - /** - * Where a path the session's programs see lives on the host: through the session's own binds - * (the most specific one), or else inside the rootfs. - */ internal fun hostPath(sessionBinds: List<Bind>, rootfs: String, guest: String): String { val bind = sessionBinds.filter { under(guest, it.guest) }.maxByOrNull { it.guest.length } ?: return rootfs.trimEnd('/') + guest @@ -118,11 +98,6 @@ object BwrapSpawner { return if (rest.isEmpty()) bind.host else bind.host.trimEnd('/') + "/" + rest } - /** - * Guest [src] bound at [dest]: the bind itself, then every session bind below [src] moved - * under [dest] - /dev brings the session's /dev/shm and GPU node, /proc its stand-ins for what - * Android keeps apps from reading. - */ internal fun bindsFor(sessionBinds: List<Bind>, rootfs: String, src: String, dest: String): List<Bind> { val out = ArrayList<Bind>() out.add(Bind(hostPath(sessionBinds, rootfs, src), dest)) @@ -146,12 +121,6 @@ object BwrapSpawner { } fun coveringBind(dest: String) = binds.filter { under(dest, it.guest) }.maxByOrNull { it.guest.length } fun newScratch(): File = File(dir, "x${scratch++}") - /** - * Where [dest] can be created on the host: inside the sandbox root, or inside a directory - * of ours bound into it (a --tmpfs under the runtime's /usr, which Flatpak fills with the - * GL extension's links). Null inside anything else bound in - the runtime itself, a host - * directory - which must not be written to. - */ fun writable(dest: String): File? { val cover = coveringBind(dest) ?: return inRoot(root, dest) if (!under(cover.host, dir.path)) return null @@ -199,8 +168,6 @@ object BwrapSpawner { link.parentFile?.mkdirs() if (!Files.isSymbolicLink(link.toPath()) && !link.exists()) Files.createSymbolicLink(link.toPath(), java.nio.file.Paths.get(target)) } else { - // A link inside the runtime's own tree cannot be made; what it points at is - // bound in its place. val abs = if (target.startsWith("/")) target else normalize(dest.substringBeforeLast('/') + "/" + target) val host = hostPath(binds, root.path, abs) if (File(host).exists()) addBind(Bind(host, dest)) else Log.w(TAG, "symlink $dest -> $target: nothing there; left out") @@ -218,24 +185,13 @@ object BwrapSpawner { return Plan(root, binds, request.optString("cwd", "/").ifEmpty { "/" }, argv, env) } - /** Where a sandbox finds the runtime's own GPU driver. */ private const val GPU_DIR = "/run/droiddeck-gpu" - /** - * The GPU, for an app that is given /dev. Flathub's Mesa has Turnip, but built for the DRM - * kernel driver: Adreno on Android is KGSL, so apps fell back to drawing on the CPU. The - * runtime's own Turnip has the KGSL backend and needs nothing the Freedesktop runtime lacks - * but three libraries, which come with it; Mesa's GL then runs on it through Zink. An app's - * own choice of driver (VK_ICD_FILENAMES, MESA_LOADER_DRIVER_OVERRIDE) is left alone. - */ private fun gpu(rootfs: String, root: File, env: MutableMap<String, String>): List<Bind>? { val lib = File(rootfs, "usr/lib") val driver = File(lib, "libvulkan_freedreno.so") if (!driver.isFile || !File("/dev/kgsl-3d0").exists()) return null if (env.containsKey("VK_ICD_FILENAMES") || env.containsKey("VK_DRIVER_FILES")) return null - // A window on the desktop gets shared-memory buffers only (KWin composites in software); - // Zink cannot present there, so such an app keeps Flathub's own Mesa (llvmpipe). From the - // desktop, games go through droiddeck-gpu's gamescope and do get the GPU. if (env["BL_DESKTOP"] == "1" && env["GAMESCOPE_WAYLAND_DISPLAY"] == null) return null val binds = ArrayList<Bind>() for (name in listOf("libvulkan_freedreno.so", "libdisplay-info.so.3", "libSPIRV-Tools.so", "libSPIRV-Tools-opt.so")) { @@ -243,7 +199,6 @@ object BwrapSpawner { if (!f.exists()) { Log.w(TAG, "gpu: the runtime has no $name; apps draw on the CPU"); return null } binds.add(Bind(f.canonicalPath, "$GPU_DIR/$name")) } - // Beside the sandbox root, in the sandbox's own directory: removed with it. val icd = File(root.parentFile, "freedreno_icd.json") icd.writeText("{\"file_format_version\": \"1.0.0\", \"ICD\": {\"library_path\": \"$GPU_DIR/libvulkan_freedreno.so\", \"api_version\": \"1.4.0\"}}\n") binds.add(Bind(icd.path, "$GPU_DIR/freedreno_icd.json")) @@ -253,18 +208,10 @@ object BwrapSpawner { env["MESA_LOADER_DRIVER_OVERRIDE"] = "zink" env["GALLIUM_DRIVER"] = "zink" } - // The driver's own libraries, found beside it; nothing else is in that directory. env["LD_LIBRARY_PATH"] = listOf(env["LD_LIBRARY_PATH"], GPU_DIR).filter { !it.isNullOrEmpty() }.joinToString(":") return binds } - /** - * What every program in the session preloads (SessionFiles writes /etc/ld.so.preload): the - * session shim - SysV shared memory for X11, the network calls Android refuses - and the - * controller reader. A sandbox's /etc is the runtime's, so the same libraries go in through - * LD_PRELOAD instead, with the directory the controllers' rings live in; without them an app - * saw no gamepad. Both are built against an older glibc than any Flathub runtime has. - */ private fun preload(session: List<Bind>, rootfs: String, env: MutableMap<String, String>): List<Bind> { val libs = FileUtils.readString(File(rootfs, "etc/ld.so.preload"))?.lines() ?.map { it.trim() }?.filter { it.startsWith("/") && File(rootfs + it).isFile } ?: return emptyList() @@ -272,9 +219,6 @@ object BwrapSpawner { val binds = ArrayList<Bind>() val paths = libs.map { lib -> "$PRELOAD_DIR/${lib.substringAfterLast('/')}".also { binds.add(Bind(rootfs + lib, it)) } } env["LD_PRELOAD"] = (paths + listOfNotNull(env["LD_PRELOAD"]?.takeIf { it.isNotBlank() })).joinToString(":") - // Not through ZYPAK_LD_PRELOAD to Chromium's children: the session shim's stat hooks then - // hide the setuid bit zypak fakes on chrome-sandbox, and Chromium refuses to start. - // The controllers: FAKE_EVDEV_DIR is <session>/dev/input, the rings beside it in <session>/dev. env["FAKE_EVDEV_DIR"]?.let { File(it).parent }?.let { dev -> binds.addAll(bindsFor(session, rootfs, dev, dev)) } @@ -283,45 +227,22 @@ object BwrapSpawner { private const val PRELOAD_DIR = "/run/droiddeck-preload" - /** - * Browsers sandbox their own children, and neither way works in here. Firefox wants - * namespaces and seccomp from the kernel; its content processes died without them (the - * desktop sets the same for the runtime's own Firefox). Chromium and Electron apps start - * their zygote through Flatpak's portal (zypak's spawn strategy), and the portal refuses: - * it knows a Flatpak by /proc/<pid>/root/.flatpak-info, and a proot sandbox's root is the - * host's. zypak's older strategy mimics the zygote inside the app's own sandbox instead. - */ private fun browserSandboxes(env: MutableMap<String, String>) { env.putIfAbsent("ZYPAK_ZYGOTE_STRATEGY_SPAWN", "0") - // Qt WebEngine is Chromium without zypak: it sets up its own namespace sandbox. env.putIfAbsent("QTWEBENGINE_DISABLE_SANDBOX", "1") - // Chromium on Wayland asks the render node the compositor names for its DRM version; - // KGSL has none, and the GPU process aborts until Chromium gives up ("GPU process isn't - // usable"). Electron picks Wayland from XDG_SESSION_TYPE; with an X server there (every - // session has Xwayland) it stays on X11, which works. GTK and Qt go by their own settings. if (env.containsKey("DISPLAY")) env["XDG_SESSION_TYPE"] = "x11" for (name in listOf("MOZ_DISABLE_CONTENT_SANDBOX", "MOZ_DISABLE_GMP_SANDBOX", "MOZ_DISABLE_RDD_SANDBOX", "MOZ_DISABLE_SOCKET_PROCESS_SANDBOX", "MOZ_DISABLE_UTILITY_SANDBOX", - // Android 16 refuses the size seal Firefox puts on a tab's shared - // memory, and the tab then dies on every page (droiddeck-desktop). "MOZ_SHM_NO_SEALS")) { env.putIfAbsent(name, "1") } } - /** - * /proc/cpuinfo without the cores' names. Snapdragon's ARMv9 cores (Cortex-A510/A715/X3) have - * SVE2 by architecture, and LLVM - llvmpipe's JIT in Flathub's Mesa, which is what apps draw - * with here - takes the CPU part to mean it may use it. Qualcomm leaves SVE off, so the first - * shader compiled killed the app with SIGILL. Without a part number LLVM targets generic - * ARMv8 plus the "Features" line, which lists what the kernel really enabled. - */ private fun genericCpuinfo(context: Context): File? = try { val out = File(context.cacheDir, "cpuinfo-generic") val text = File("/proc/cpuinfo").readLines() .filterNot { it.startsWith("CPU implementer") || it.startsWith("CPU part") || it.startsWith("CPU variant") || it.startsWith("CPU revision") } .joinToString("\n", postfix = "\n") - // Sandboxes start side by side; one must never read another's half-written copy. if (!out.isFile || out.readText() != text) { val staged = File.createTempFile("cpuinfo", null, context.cacheDir) staged.writeText(text) @@ -332,7 +253,6 @@ object BwrapSpawner { Log.w(TAG, "cpuinfo", e); null } - /** [path] with "." and ".." resolved lexically. */ internal fun normalize(path: String): String { val out = ArrayList<String>() for (part in path.split('/')) when (part) { @@ -343,10 +263,6 @@ object BwrapSpawner { return "/" + out.joinToString("/") } - /** - * [guest] inside the sandbox root, or null when the way there crosses a symlink: one of - * Flatpak's (var/run -> ../run) may point anywhere once followed on the host. - */ private fun inRoot(root: File, guest: String): File? { var f = root for (part in guest.split('/').filter { it.isNotEmpty() }) { @@ -358,17 +274,14 @@ object BwrapSpawner { } private fun chmod(f: File, mode: Int) { - try { Os.chmod(f.path, mode) } catch (e: Exception) { /* the default mode serves */ } + try { Os.chmod(f.path, mode) } catch (e: Exception) { } } private fun run(context: Context, plan: Plan, client: LocalSocket, out: DataOutputStream, id: Int) { val cmd = LinuxRuntime.prootPrefix(context, plan.root, plan.cwd) - // DROIDDECK_PROOT_VERBOSE=N in the app's environment (flatpak run --env=...) has proot - // say what it does, to logcat in full: for a sandbox that dies without a word. val verbose = plan.env["DROIDDECK_PROOT_VERBOSE"]?.toIntOrNull() if (verbose != null) { cmd.add(1, "-v"); cmd.add(2, verbose.toString()) } plan.binds.forEach { cmd.add("-b"); cmd.add(it.spec) } - // proot hands its own environment on; the sandbox gets exactly what Flatpak asked for. cmd.add("/usr/bin/env") cmd.add("-i") plan.env.forEach { (k, v) -> cmd.add("$k=$v") } @@ -383,23 +296,18 @@ object BwrapSpawner { } val process = builder.start() val pid = HostProcess.pidOf(process) - // A sandbox lives exactly as long as its bwrap stand-in, which ends it; a session starting - // meanwhile (OrphanReaper) must not take it - an install's extra-data step, say. com.droiddeck.launcher.session.OrphanReaper.keep(pid) Log.i(TAG, "sandbox $id pid $pid: ${plan.argv.joinToString(" ")} (${plan.binds.size} binds)") synchronized(out) { frame(out, 'P', pid.toString().toByteArray()) } - // The stand-in going away (Flatpak killed, the session over) ends the sandbox. Thread({ - try { while (client.inputStream.read() >= 0) { /* nothing is sent after the request */ } } - catch (e: IOException) { /* closed */ } + try { while (client.inputStream.read() >= 0) { } } + catch (e: IOException) { } if (process.isAlive) { Log.i(TAG, "sandbox $id: its bwrap is gone; stopping it") process.destroy() } }, "bwrap-$id-watch").apply { isDaemon = true; start() } val buffer = ByteArray(16384) - // The start of what the sandbox says goes to logcat too: a sandbox that fails at once - // (proot refusing a bind, a missing library) says why there even when nothing reads it. var logged = 0 process.inputStream.use { stream -> while (true) { diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/DeckyManager.kt b/app/src/main/java/com/droiddeck/launcher/runtime/DeckyManager.kt index 60058b72c..3e90398e2 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DeckyManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DeckyManager.kt @@ -16,7 +16,6 @@ import java.util.zip.ZipInputStream import org.json.JSONArray import org.json.JSONObject -/** Optional Decky Loader installation for the ARM64 Linux guest. */ object DeckyManager { private const val TAG = "DeckyManager" const val REPOSITORY = "Droid-Deck/decky-loader" @@ -25,7 +24,6 @@ object DeckyManager { private const val VERSION = "root/homebrew/services/.droiddeck-decky-version" private const val CEF_REMOTE_DEBUG_MARKER = "root/.local/share/Steam/.cef-enable-remote-debugging" - /** True while a plugin's remote binary downloads: what the UI branches on, not the stage's words. */ @Volatile var downloadingBinary = false private set @@ -41,7 +39,6 @@ object DeckyManager { fun loader(context: Context) = File(LinuxRuntime.rootDir(context), PATH) fun supervisorEnabled(context: Context) = File(LinuxRuntime.rootDir(context), "root/.droiddeck-decky-enabled").isFile - /** Remove the legacy install-time debugger marker unless the user opted into Decky. */ fun syncCefMarker(context: Context) { val marker = File(LinuxRuntime.rootDir(context), CEF_REMOTE_DEBUG_MARKER) if (!supervisorEnabled(context)) { @@ -105,7 +102,6 @@ object DeckyManager { } catch (e: Exception) { Log.w(TAG, "release metadata", e); ReleaseChannels(emptyList(), emptyList()) } } - /** Downloads to a sibling temp file and atomically renames only after size, digest and ELF checks. */ fun install(context: Context, release: Release, progress: (String, Int) -> Unit): String? { if (SessionState.running) return context.getString(R.string.deckymgr_stop_session_loader) val target = loader(context) @@ -124,7 +120,6 @@ object DeckyManager { if (!validElf(temp, release.machine)) { temp.delete(); return context.getString(R.string.deckymgr_not_elf) } if (SessionState.running) { temp.delete(); return context.getString(R.string.deckymgr_session_started_loader) } if (!temp.setExecutable(true, false)) { temp.delete(); return context.getString(R.string.deckymgr_loader_not_executable) } - // Decky needs Steam's local CEF debugger only when its session supervisor is enabled. val cefMarker = File(LinuxRuntime.rootDir(context), CEF_REMOTE_DEBUG_MARKER) val cefReady = !supervisorEnabled(context) || cefMarker.isFile || runCatching { cefMarker.parentFile?.mkdirs() @@ -149,7 +144,6 @@ object DeckyManager { return null } - /** Imports a Decky distribution ZIP into homebrew/plugins for the next Steam session. */ fun installPluginZip(context: Context, archive: File, progress: (String, Int) -> Unit): String? { if (SessionState.running) return context.getString(R.string.deckymgr_stop_session_plugin) if (installed(context) == null) return context.getString(R.string.deckymgr_loader_required) @@ -216,8 +210,6 @@ object DeckyManager { out.write(buffer, 0, read) } } - // Decky distributions place plugin executables under bin/. Java's ZIP reader - // does not expose Unix mode bits, so restore execution on those payload files. if (relative.split('/').dropLast(1).any { it == "bin" } && !output.setExecutable(true, false)) { throw IllegalStateException(context.getString(R.string.deckymgr_bundled_not_executable)) } diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/DeckyPluginInstall.kt b/app/src/main/java/com/droiddeck/launcher/runtime/DeckyPluginInstall.kt index b4223b7af..19414a49e 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DeckyPluginInstall.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DeckyPluginInstall.kt @@ -6,9 +6,7 @@ import java.io.File import org.json.JSONArray import org.json.JSONObject -/** Offline equivalent of Decky's identity-based replacement and loader settings update. */ internal object DeckyPluginInstall { - /** A failure the user is told about: [text] is its string resource. */ class Failure(@StringRes val text: Int) : IllegalStateException() fun activate(staged: File, plugins: File, name: String, settingsFile: File, backupSuffix: String) { @@ -26,7 +24,6 @@ internal object DeckyPluginInstall { .distinct()) if ((0 until order.length()).none { order.optString(it) == name }) order.put(name) settings.put("pluginOrder", order) - // Decky's reinstall removes these flags while retaining the plugin's order and data. for (key in listOf("frozenPlugins", "hiddenPlugins", "disabled_plugins")) { val values = settings.optJSONArray(key) ?: continue settings.put(key, JSONArray((0 until values.length()).map { values.get(it) }.filter { it != name })) diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/DesktopCatalog.kt b/app/src/main/java/com/droiddeck/launcher/runtime/DesktopCatalog.kt index 971608106..884b09ea9 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DesktopCatalog.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DesktopCatalog.kt @@ -10,28 +10,15 @@ import com.droiddeck.launcher.core.FileUtils import org.json.JSONObject import java.io.File -/** - * The hosted packages that go into the runtime on request - the desktop, the emulators, and the - * builds we mirror or make ourselves - described by desktop.json beside the runtime's own catalog. - * - * Two kinds. A `tar` extracts over the rootfs like the runtime itself (the same extractor, links - * and modes preserved). An `appimage` is one file dropped in /opt/appimages with a .desktop entry - * written for it; proot has no FUSE, so it runs extracted (APPIMAGE_EXTRACT_AND_RUN). - */ object DesktopCatalog { private const val TAG = "DesktopCatalog" const val CATALOG_URL = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/main/desktop.json" - /** - * Valve's Proton Experimental (ARM64) with its appmanifest, laid out as the client keeps it. - * In a catalog of its own so it never shows among the packages a user picks from. - */ const val STEAM_SEED_URL = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/main/steam-seed.json" const val PROTON_SEED_ID = "proton-arm64" class Entry( val id: String, val name: String, val tier: Int, val version: String, val kind: String, val url: String, val sha256: String, val size: Long, val notes: String, - /** For an appimage: the icon name and menu category of its .desktop entry. */ val icon: String, val category: String, ) @@ -40,8 +27,6 @@ object DesktopCatalog { return try { val json = JSONObject(body) val arr = json.getJSONArray("packages") - // A package built per memory page size (ARMSX2: 4K and 16K kernels) names the others - // under "pages"; the device's own size picks one, and the top-level file is the 4K one. val pageSize = try { android.system.Os.sysconf(android.system.OsConstants._SC_PAGESIZE).toString() } catch (e: Exception) { "4096" } @@ -62,42 +47,21 @@ object DesktopCatalog { private fun marker(context: Context, id: String) = File(LinuxRuntime.rootDir(context), ".droiddeck-pkg-$id") - /** The installed version of a package, or null. */ fun installed(context: Context, id: String): String? = FileUtils.readString(marker(context, id))?.trim()?.takeIf { it.isNotEmpty() } - /** - * True until the ARM64 Proton has been placed once. Its appmanifest counts too, so an install - * the client fetched itself is never overwritten; after the first placement the client owns it - * and updates it like any other app, and a user who removes it is not given it back. - */ fun protonSeedNeeded(context: Context): Boolean = installed(context, PROTON_SEED_ID) == null && !File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps/appmanifest_4427310.acf").isFile - /** - * The desktop: KDE Plasma (tools/desktop-kde). It replaced an LXQt/labwc package of id - * "desktop", which a runtime may still carry; that one does not count, so the first desktop - * after the update installs this one over it. - */ const val DESKTOP_ID = "desktop-kde" - /** - * The desktop package this apk installs: pinned in tools/desktop-kde/release.env and published - * in DroidDeck-Components, not read from the catalog, so the desktop scripts staged by this - * apk always get the package they were made for. - */ fun desktopEntry(context: Context) = Entry( DESKTOP_ID, context.getString(R.string.content_linux_desktop), 1, BuildConfig.DESKTOP_KDE_TAG, "tar", BuildConfig.DESKTOP_KDE_URL, BuildConfig.DESKTOP_KDE_SHA256, BuildConfig.DESKTOP_KDE_SIZE, "", "", "", ) - // The desktop package installed to the end: KWin is there and the marker install() writes once - // the whole package is extracted. An install cut short (the app killed, storage full) leaves - // KWin without the marker, and the next desktop installs it again. A newer pinned package also - // replaces the installed one, keeping the user's home and settings. The launcher is staged by - // the app at every session, so it cannot tell. SessionFiles uses the same test. fun desktopInstalled(context: Context): Boolean = desktopInstalled(LinuxRuntime.rootDir(context)) fun desktopInstalled(root: File): Boolean = @@ -110,7 +74,6 @@ object DesktopCatalog { return File(root, DesktopRemoval.PENDING).exists() || marker(context, DESKTOP_ID).exists() } - /** The installed desktop's size on disk; off the main thread. */ fun desktopSize(context: Context): Long = DesktopRemoval.installedSize(context) fun cleanLegacy(context: Context, listener: LinuxRuntimeInstaller.ProgressListener? = null) = @@ -119,11 +82,9 @@ object DesktopCatalog { fun removeDesktop(context: Context, listener: LinuxRuntimeInstaller.ProgressListener): String? = DesktopRemoval.remove(context, listener) - /** Downloads, verifies and installs one package. Returns null on success, else a message. */ fun install(context: Context, entry: Entry, listener: LinuxRuntimeInstaller.ProgressListener?): String? { val root = LinuxRuntime.rootDir(context) if (!root.isDirectory) return context.getString(R.string.deskpkg_runtime_missing) - // Every catalog row carries a sha256; one without is refused rather than trusted. if (entry.sha256.isEmpty()) return context.getString(R.string.deskpkg_no_checksum, entry.name) val download = File(context.cacheDir, "pkg-${entry.id}.download") try { @@ -141,7 +102,6 @@ object DesktopCatalog { "appimage" -> { val dir = File(root, "opt/appimages").apply { mkdirs() } val target = File(dir, "${entry.id}.AppImage") - // Some projects zip the AppImage (melonDS); the one file inside is what we want. val placed = if (entry.url.endsWith(".zip", ignoreCase = true)) unzipAppImage(download, target) else download.renameTo(target) if (!placed) return context.getString(R.string.deskpkg_place_failed) @@ -152,8 +112,6 @@ object DesktopCatalog { "Icon=${entry.icon}\nTerminal=false\nCategories=${entry.category};\n") } else -> { - // Not complete until written again at the end: a reinstall cut short must not - // leave the last install's marker saying it is. marker(context, entry.id).delete() if (!LinuxRuntimeInstaller.extract(context, download, root, listener)) return context.getString(R.string.deskpkg_extract_failed) } @@ -192,10 +150,6 @@ object DesktopCatalog { return false } - /** - * Removes an appimage package outright. A tar package is spread through the rootfs and is - * not tracked file by file; only its marker is dropped, which offers it again. - */ fun remove(context: Context, entry: Entry) { val root = LinuxRuntime.rootDir(context) if (entry.kind == "appimage") { diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/DesktopFileRemoval.kt b/app/src/main/java/com/droiddeck/launcher/runtime/DesktopFileRemoval.kt index 063a5c9a9..6d2c42b01 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DesktopFileRemoval.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DesktopFileRemoval.kt @@ -7,7 +7,6 @@ import java.nio.file.LinkOption.NOFOLLOW_LINKS import java.nio.file.attribute.BasicFileAttributes import java.security.MessageDigest -/** Removes only unchanged, recorded package files. Never visits home or follows a symlink. */ internal object DesktopFileRemoval { data class Entry(val kind: String, val size: Long, val identity: String, val path: String) { companion object { @@ -37,14 +36,13 @@ internal object DesktopFileRemoval { else -> attrs.isRegularFile && attrs.size() == entry.size && sha(file, buffer) == entry.identity } if (!matches) { kept++; continue } - Files.delete(file.toPath()) // A failure propagates; the pending marker makes retry possible. + Files.delete(file.toPath()) removed++ bytes += if (attrs.isRegularFile) attrs.size() else 0 var parent = file.parentFile while (parent != root && parent != null) { directories.add(parent); parent = parent.parentFile } if (index % 200 == 0) progress(if (total > 0) index * 99 / total else -1) } - // Only now-empty parents of removed files, never a recursive delete of a package directory. for (dir in directories.sortedByDescending { it.path.length }) { if (dir.list()?.isEmpty() == true && !dir.delete()) throw IOException("Cannot remove $dir") } @@ -56,7 +54,6 @@ internal object DesktopFileRemoval { if (Files.isSymbolicLink(root.toPath())) return null val parts = name.split('/') if (parts.firstOrNull() !in setOf("usr", "etc") || parts.any { it.isEmpty() || it == "." || it == ".." }) return null - // Even an in-root symlink in a parent can redirect deletion into a user's data. var parent = root for (part in parts.dropLast(1)) { parent = File(parent, part) @@ -76,7 +73,6 @@ internal object DesktopFileRemoval { } } - /** The base runtime's pacman database is still present: desktop archives never replace it. */ fun runtimeFiles(root: File): Set<String> = buildSet { val database = File(root, "var/lib/pacman/local") if (!database.isDirectory || Files.isSymbolicLink(database.toPath())) throw IOException("Runtime package database missing") diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/DesktopRemoval.kt b/app/src/main/java/com/droiddeck/launcher/runtime/DesktopRemoval.kt index 09c93d73d..3d031a85a 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/DesktopRemoval.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/DesktopRemoval.kt @@ -7,7 +7,6 @@ import com.droiddeck.launcher.core.FileUtils import java.io.File import java.util.zip.GZIPInputStream -/** Inventories are generated from pinned archives by tools/desktop-kde/removal-manifests.py. */ internal object DesktopRemoval { const val PENDING = ".droiddeck-desktop-removing" private const val CLEANING = ".droiddeck-lxqt-cleaning-v1" @@ -19,15 +18,9 @@ internal object DesktopRemoval { private fun protected(context: Context, root: File) = DesktopFileRemoval.runtimeFiles(root) + lines(context, "emulators-keep.txt.gzip") - /** - * What the desktop's own files take on disk, from the same inventory its removal uses (each - * file's size); the bundled apps' and emulators' shared files are not counted. Reads an asset: - * call it off the main thread. - */ fun installedSize(context: Context): Long = lines(context, "kde-remove.tsv.gzip").sumOf { line -> line.split('\t').getOrNull(1)?.toLongOrNull() ?: 0L } - /** Called before a guest starts or just after KDE extraction; retries after a process death. */ fun cleanLegacy(context: Context, listener: LinuxRuntimeInstaller.ProgressListener? = null) { val root = LinuxRuntime.rootDir(context) if (FileUtils.readString(File(root, CLEANED))?.trim() == "1") return @@ -54,7 +47,6 @@ internal object DesktopRemoval { val version = FileUtils.readString(pending)?.trim() ?: FileUtils.readString(marker)?.trim() val versions = context.assets.open("desktop-removal/versions.txt").bufferedReader().use { it.readLines() } check(version in versions) { context.getString(R.string.desktop_removal_unknown) } - // Validate all inventories and the runtime database before dropping the installed marker. val entries = lines(context, "kde-remove.tsv.gzip").map(DesktopFileRemoval.Entry::parse) val keep = protected(context, root) cleanLegacy(context, listener) @@ -63,7 +55,6 @@ internal object DesktopRemoval { if (marker.exists() && !marker.delete()) error("Could not clear desktop marker") val removing = context.getString(R.string.desktop_removing) val result = DesktopFileRemoval.remove(root, entries, keep) { listener.onProgress(removing, it) } - // APK-staged desktop files are matched against the APK too; keep user-modified copies. for ((asset, path) in com.droiddeck.launcher.session.SessionFiles.DESKTOP_FILES) { val target = DesktopFileRemoval.safeFile(root, path) ?: continue if (!isRegularFile(target)) continue diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/FlatpakManager.kt b/app/src/main/java/com/droiddeck/launcher/runtime/FlatpakManager.kt index c5c1fa6db..8dfafd42b 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/FlatpakManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/FlatpakManager.kt @@ -7,31 +7,20 @@ import com.droiddeck.launcher.core.FileUtils import org.json.JSONObject import java.io.File -/** - * Flatpak in the Linux runtime: putting it there (droiddeck-flatpak-setup), the apps installed - * from Flathub, and installing, removing and updating them (droiddeck-flatpak). One per-user - * installation at /root/.local/share/flatpak, so nothing needs the system helper or polkit. - * - * Every command runs in a proot of its own, outside any session: the store works whether or not - * Steam or the desktop is up, and a session starting meanwhile leaves it running (OrphanReaper). - */ object FlatpakManager { private const val TAG = "FlatpakManager" const val FLATPAK = "/usr/bin/flatpak" const val BWRAP = "/usr/local/bin/droiddeck-bwrap" - /** The program the front end starts a Flatpak app through, in a session of its own. */ const val LAUNCHER = "/usr/local/bin/droiddeck-flatpak-run" private const val HELPER = "/usr/local/bin/droiddeck-flatpak" private const val SETUP = "/usr/local/bin/droiddeck-flatpak-setup" private const val USER_DIR = "root/.local/share/flatpak" - /** One store operation at a time: Flatpak locks its installation per transaction anyway. */ @Volatile var busy: String? = null private set class App(val id: String, val name: String, val icon: File?, val branch: String, val summary: String?) - /** Flatpak is in the runtime, with Flathub to install from. */ fun ready(context: Context): Boolean { val root = LinuxRuntime.rootDir(context) return File(root, FLATPAK.substring(1)).isFile && @@ -44,10 +33,6 @@ object FlatpakManager { else -> "aarch64" } - /** - * The installed apps, read from the installation itself: each app's deployed files carry the - * desktop entry and icons it exports, so listing them needs no process in the runtime. - */ fun installedApps(context: Context): List<App> { val apps = File(LinuxRuntime.rootDir(context), "$USER_DIR/app") val arch = arch() @@ -67,7 +52,6 @@ object FlatpakManager { fun isInstalled(context: Context, id: String): Boolean = File(LinuxRuntime.rootDir(context), "$USER_DIR/app/$id/${arch()}").listFiles()?.any { File(it, "active").exists() } == true - /** A key of the desktop entry's main group ([Desktop Entry]), unlocalised. */ private fun entry(desktop: String, key: String): String? { var inMain = false for (line in desktop.lineSequence()) { @@ -78,7 +62,6 @@ object FlatpakManager { return null } - /** The largest PNG the app exports (the front end draws no SVG). */ private fun icon(export: File, id: String): File? { val hicolor = File(export, "icons/hicolor") return hicolor.listFiles() @@ -87,13 +70,11 @@ object FlatpakManager { ?.maxByOrNull { it.first }?.second } - /** Runs [argv] in the runtime, its output also in files/logs/tools/flatpak-<verb>.log. */ private fun runGuest(context: Context, argv: List<String>, fakeRoot: Boolean, onLine: (String) -> Unit): Int { val name = "flatpak-" + (argv.getOrNull(2)?.takeIf { argv.getOrNull(1) == HELPER } ?: "setup") return GuestCommand.run(context, argv, fakeRoot, name, onLine = onLine) } - /** [block]'s result, or [ifBusy]'s while another operation has the installation. */ private inline fun <T> exclusive(what: String, ifBusy: () -> T, block: () -> T): T { synchronized(this) { if (busy != null) return ifBusy() @@ -102,7 +83,6 @@ object FlatpakManager { try { return block() } finally { busy = null } } - /** Puts Flatpak into the runtime and adds Flathub. Null on success, else what went wrong. */ fun setup(context: Context, onProgress: (String, Int) -> Unit): String? { if (!LinuxRuntime.isInstalled(context)) return context.getString(R.string.user_apps_runtime_required) return exclusive("setup", { context.getString(R.string.flatpakmgr_busy) }) { @@ -123,17 +103,12 @@ object FlatpakManager { } } - /** - * One store operation - install, uninstall or update - with its progress as a stage line and - * an overall percentage across the steps Flatpak plans (a runtime, its extensions, the app). - */ private fun transaction( context: Context, verb: String, id: String?, onProgress: (String, Int) -> Unit, onDone: (JSONObject) -> Unit = {}, ): String? { if (!ready(context)) return context.getString(R.string.flatpakmgr_not_ready) return exclusive("$verb:${id ?: "all"}", { context.getString(R.string.flatpakmgr_busy) }) { var error: String? = null - // Progress lines carry no ref; they belong to the step the last "op" line started. var step = context.getString(R.string.flatpakmgr_starting) val argv = listOfNotNull("/usr/bin/python3", HELPER, verb, id) val status = runGuest(context, argv, fakeRoot = false) { line -> @@ -169,10 +144,6 @@ object FlatpakManager { }, name) } - /** - * A ref in words: "app/org.supertuxproject.SuperTux/aarch64/stable" is "SuperTux". [text] - * resolves a string resource with its arguments. - */ internal fun refLabel(ref: String, text: (Int, Array<out Any>) -> String): String { val parts = ref.split('/') val id = parts.getOrNull(1) ?: return ref @@ -190,7 +161,6 @@ object FlatpakManager { } } - /** The whole transaction's percentage: steps done, plus this one's share. */ private fun overall(o: JSONObject, percent: Int): Int { val n = o.optInt("n").coerceAtLeast(1) val i = o.optInt("i").coerceIn(1, n) @@ -200,7 +170,6 @@ object FlatpakManager { fun install(context: Context, id: String, onProgress: (String, Int) -> Unit) = transaction(context, "install", id, onProgress) fun uninstall(context: Context, id: String, onProgress: (String, Int) -> Unit) = transaction(context, "uninstall", id, onProgress) - /** Installs the .flatpak bundle at [guestPath], its runtimes from Flathub: the app's ID, and what went wrong if anything. */ fun installBundle(context: Context, guestPath: String, onProgress: (String, Int) -> Unit): Pair<String?, String?> { var id: String? = null val problem = transaction(context, "install-bundle", guestPath, onProgress) { id = it.optString("id").ifEmpty { null } } @@ -208,7 +177,6 @@ object FlatpakManager { } fun update(context: Context, id: String?, onProgress: (String, Int) -> Unit) = transaction(context, "update", id, onProgress) - /** Installed apps with an update on Flathub, or null when that could not be checked. */ fun updates(context: Context): Set<String>? { if (!ready(context)) return emptySet() var apps: Set<String>? = null diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/GuestCommand.kt b/app/src/main/java/com/droiddeck/launcher/runtime/GuestCommand.kt index ad63fc5bf..2db2c7041 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/GuestCommand.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/GuestCommand.kt @@ -9,18 +9,7 @@ import com.droiddeck.launcher.session.SessionPaths import com.droiddeck.launcher.session.SessionPrefs import java.io.File -/** - * One command in the Linux runtime outside any session - the store's Flatpak work, an AppImage - * import - in a proot of its own. A session starting meanwhile leaves it running (OrphanReaper). - */ object GuestCommand { - /** - * Runs [argv] and hands each output line to [onLine]; returns the exit status. [fakeRoot] is - * for the package tools, which refuse any uid but 0. With [logName], everything the command - * says also goes to files/logs/tools/<logName>.log beside the session logs, so a problem can - * be handed over like a session's. With [linkDir], hard links (which Android denies apps) - * become symlinks to files proot keeps there (its link2symlink). - */ fun run(context: Context, argv: List<String>, fakeRoot: Boolean = false, logName: String? = null, linkDir: File? = null, onLine: (String) -> Unit): Int { val log = logName?.let { @@ -40,7 +29,6 @@ object GuestCommand { val root = LinuxRuntime.rootDir(context) LinuxRuntime.writeAccounts(context) SessionFiles.stage(context, root) - // The resolver otherwise names the DNS of whatever network the last session was on. LinuxNetworkLinkComponent(context, root).publish() val runtimeDir = File(context.filesDir, ".flatpak-rt").apply { mkdirs() } val cmd = LinuxRuntime.prootPrefix(context, root, "/root", fakeRoot) diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxNetworkLinkComponent.kt b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxNetworkLinkComponent.kt index 5b5585c22..3bf43fc34 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxNetworkLinkComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxNetworkLinkComponent.kt @@ -26,18 +26,6 @@ import java.net.Inet6Address import org.json.JSONArray import org.json.JSONObject -/** - * The device's network link, published for the Linux runtime's processes. - * - * Android denies an app the rtnetlink dump behind getifaddrs() and if_nameindex(), the hardware - * address ioctl and every table under /proc/net. Wine builds its adapter, address, route and - * neighbour tables from those, so a game under the runtime saw no adapter, no address, no gateway - * and no MAC - and one that checks its adapters before going online waited for good. The link the - * app can see through ConnectivityManager is written here, kept current while the session runs, - * and the session shim answers whatever the kernel refuses from it (tools/linuxfs/preload/netif.c). - * - * Shape follows WinNative's LinuxNetworkLinkComponent (maxjivi05, f76a5c4a), GPL-3.0. - */ class LinuxNetworkLinkComponent( context: Context, private val rootDir: File, @@ -71,7 +59,6 @@ class LinuxNetworkLinkComponent( } } - /** Called before the session starts, so its first process already sees the link. */ fun publish() = synchronized(lock) { currentNetwork = connectivity.activeNetwork currentProperties = currentNetwork?.let(connectivity::getLinkProperties) @@ -102,7 +89,6 @@ class LinuxNetworkLinkComponent( } override fun onLost(network: Network) = synchronized(lock) { - // A handover can report the old link lost after the new default arrived. if (network == currentNetwork) { currentNetwork = null currentProperties = null @@ -174,7 +160,6 @@ class LinuxNetworkLinkComponent( } } - /** NetworkManager's view of Android. Kept separate from netif.c's legacy adapter format. */ @Suppress("DEPRECATION") private fun writeNetworkState(properties: LinkProperties?, capabilities: NetworkCapabilities?) { val namesAllowed = canReadWifiNames() @@ -238,8 +223,6 @@ class LinuxNetworkLinkComponent( connectionInfo } if (info != null) { - // SSIDs can be redacted by Android. No location permission is needed to - // report the transport; a hidden name simply displays as Wi-Fi in Steam. if (namesAllowed) { val namedInfo = connectionInfo ?: info namedInfo.ssid?.takeUnless { it == WifiManager.UNKNOWN_SSID || it.isEmpty() } @@ -262,15 +245,6 @@ class LinuxNetworkLinkComponent( } } - /** - * The guest's resolver, from the network it is actually on. glibc reads /etc/resolv.conf, - * and the image ships two public servers there, which is why Steam resolves at all - but a - * captive portal, a private-DNS network or a v6-only carrier wants the network's own - * servers. Those come first, IPv4 before IPv6 and never a link-local one (Android lists - * fe80:: resolvers a guest cannot reach; handing Bannerlator's Wine guests exactly that was - * the Pale Moon "no internet"). The public ones stay as fallback so nothing is ever worse - * than the image was. - */ private fun writeResolver(properties: LinkProperties?) { val own = properties?.dnsServers.orEmpty() .filter { !it.isLinkLocalAddress && !it.isLoopbackAddress && !it.isAnyLocalAddress } @@ -321,10 +295,6 @@ class LinuxNetworkLinkComponent( } } - /** - * A stable, locally administered address for this device. Android hides the real one, and a - * title should see the same hardware identity every session. - */ private fun seededMac(): String { val id = Settings.Secure.getString(appContext.contentResolver, Settings.Secure.ANDROID_ID) ?: "bannerlator" val bytes = java.security.MessageDigest.getInstance("SHA-1").digest(id.toByteArray()) diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java index 082bf814e..76ef0995c 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntime.java @@ -16,47 +16,23 @@ import java.nio.file.Files; import java.util.ArrayList; import java.util.List; -/** - * The glibc arm64 rootfs at {@code files/linuxfs} and the proot invocation that runs a program in - * it as this app's own uid. It is a second runtime beside the Wine imagefs, not a container: no - * Wine, no box64, no FEX. proot is packaged as {@code libproot.so} so the installer places it, with - * its loader, in the native library directory - the only place an app on targetSdk 28 may execute - * a file from. - * - * <p>Ported from WinNative's gamescope runtime (GPL-3.0). - */ public final class LinuxRuntime { public static final String DIR = "linuxfs"; public static final String SESSION_SCRIPT = "/usr/local/bin/droiddeck-session"; public static final String MODE_DESKTOP = "desktop"; public static final String MODE_STEAM = "steam"; public static final String MODE_RUN = "run"; - /** Shortcut extra naming which of the modes above a Linux entry launches. */ public static final String EXTRA_LINUX_MODE = "linux_mode"; private static final String KGSL_DEVICE = "/dev/kgsl-3d0"; - /** - * Where the guest sees a command's XDG_RUNTIME_DIR. A Unix socket's path must fit in 108 bytes, - * and libwayland checks the guest's path before proot ever sees it: with the app on an SD card - * adopted as internal storage the files directory is /mnt/expand/<uuid>/user/0/<package>/files, - * and gamescope-0 under it is one byte over, so gamescope could not open its socket and the - * session ended at once. A long host path behind it is proot's to shorten. - */ public static final String GUEST_RUNTIME_DIR = "/run/droiddeck"; private static final String SYSTEM_FONTS = "/system/fonts"; private static final String GUEST_SYSTEM_FONTS = "/usr/local/share/fonts/android"; - /** The folder under Downloads where builds before private logs wrote them; read only to move them. */ public static final String DEBUG_LOG_DIR = "DroidDeck"; - /** - * Where every log lands: app-private, {@code files/logs}. Session folders, tools/ (one-off - * commands) and stores/ sit under it; the files directory is bound into every session at the - * same path, so the guest writes there too. Share logs is how a folder leaves the device. - */ public static File logDir(Context context) { return new File(context.getFilesDir(), "logs"); } - /** {@code Download/DroidDeck}, where earlier builds wrote logs. Only LogMigration reads it. */ public static File legacyLogDir() { return new File(android.os.Environment.getExternalStoragePublicDirectory( android.os.Environment.DIRECTORY_DOWNLOADS), DEBUG_LOG_DIR); @@ -68,16 +44,10 @@ public final class LinuxRuntime { return new File(context.getFilesDir(), DIR); } - /** - * The session's own small tree beside the rootfs: the fake evdev nodes and their rings. In - * Bannerlator these lived in the Wine imagefs; here there is no Wine, so the session owns them. - * Bound into the guest at its own host path, so nothing needs translating. - */ public static File sessionRoot(Context context) { return new File(context.getFilesDir(), "session"); } - /** Where the runtime carries the host-side proot; see tools/linuxfs/prebuilt/proot/README.md. */ private static final String HOST_DIR = "opt/android-host"; public static File prootBinary(Context context) { @@ -92,13 +62,11 @@ public final class LinuxRuntime { return new File(rootDir(context), HOST_DIR + "/loader"); } - /** proot links against libtalloc, which sits beside it; empty when the apk copy is in use. */ public static String prootLibraryPath(Context context) { File dir = new File(rootDir(context), HOST_DIR); return dir.equals(prootBinary(context).getParentFile()) ? dir.getPath() : ""; } - /** The rootfs is present with gamescope and the session script the launcher hands control to. */ public static boolean isInstalled(Context context) { if (LinuxRuntimeInstaller.isRemoving() || LinuxRuntimeInstaller.hasRemovalPending(context)) return false; File root = rootDir(context); @@ -109,7 +77,6 @@ public final class LinuxRuntime { && prootLoader(context).isFile(); } - /** The Vulkan ICD manifest the rootfs ships for the device GPU, or null when it has none. */ public static File vulkanIcd(Context context) { File icdDir = new File(rootDir(context), "usr/share/vulkan/icd.d"); File[] manifests = icdDir.listFiles((dir, name) -> name.endsWith(".json")); @@ -120,25 +87,16 @@ public final class LinuxRuntime { return manifests.length > 0 ? manifests[0] : null; } - /** - * The proot command line running {@code guestCommand} inside the rootfs. Host paths the session - * needs - the app's files directory for the compositor and audio sockets, external storage for - * the user's games - are bound at their own paths, so nothing on either side needs translating - * and proot never touches the fds a dma-buf travels in. Android has no /dev/shm; a directory - * under the cache stands in, which glibc's shm_open and Chromium's shared memory accept. - */ public static List<String> command(Context context, File sessionRoot, File runtimeDir, File externalStorage, List<String> guestCommand) { return command(context, sessionRoot, runtimeDir, externalStorage, null, guestCommand); } - /** As above, plus {@code host:guest} bind specs - the installed games handed to Steam. */ public static List<String> command(Context context, File sessionRoot, File runtimeDir, File externalStorage, List<String> extraBinds, List<String> guestCommand) { File root = rootDir(context); List<String> binds = binds(context, sessionRoot, runtimeDir, externalStorage, extraBinds); - // A session's binds are the view its programs have; a one-off command's are not recorded. if (sessionRoot != null) lastBinds = binds; List<String> cmd = prootPrefix(context, root, "/root"); for (String spec : binds) bind(cmd, spec); @@ -146,10 +104,6 @@ public final class LinuxRuntime { return cmd; } - /** - * The binds of the last command built: the guest's view of the host, which the Flatpak - * sandboxes started beside a session (BwrapSpawner) translate their paths through. - */ private static volatile List<String> lastBinds; public static List<String> lastBinds(Context context) { @@ -158,25 +112,15 @@ public final class LinuxRuntime { return binds(context, null, null, android.os.Environment.getExternalStorageDirectory(), null); } - /** proot and its options up to the binds: the rootfs at {@code root}, starting in {@code cwd}. */ public static List<String> prootPrefix(Context context, File root, String cwd) { return prootPrefix(context, root, cwd, false); } - /** - * As above; {@code fakeRoot} has the guest see uid 0 (proot -0), for the package tools that - * refuse any other uid. Files it writes still belong to the app. - */ public static List<String> prootPrefix(Context context, File root, String cwd, boolean fakeRoot) { List<String> cmd = new ArrayList<>(); cmd.add(prootBinary(context).getPath()); cmd.add("--kill-on-exit"); - // Preserve the host kernel identity while giving the guest the app's branded host name. cmd.add("--kernel-release=" + guestUtsname(SessionPrefs.guestHostname(context))); - // Android's app seccomp policy traps the whole set*id family. Xwayland's Popen() calls - // setgid()/setuid() before it execs xkbcomp and _exit(127)s when they fail, so without - // this the keymap never compiles and Xwayland dies. -i makes proot answer those calls - // itself while still reporting our real ids, so nothing inside sees a different user. int uid = Process.myUid(); if (fakeRoot) { cmd.add("-0"); @@ -191,7 +135,6 @@ public final class LinuxRuntime { return cmd; } - /** The {@code host:guest} (or same-path) bind specs of a session; see {@link #command}. */ public static List<String> binds(Context context, File sessionRoot, File runtimeDir, File externalStorage, List<String> extraBinds) { File root = rootDir(context); @@ -221,39 +164,20 @@ public final class LinuxRuntime { bind(cmd, shm.getPath() + ":/dev/shm"); bindSystemFonts(cmd, root); - // Android denies apps these; glibc, Steam and libcap read them at startup. File fakeProc = new File(root, "etc/droiddeck/proc"); - // libpci picks its procfs backend on whether it can read the /proc/bus/pci directory, which - // the app can, then die()s - exit(1) on the calling process - on the devices file inside - // it, which the app cannot. Chromium loads libpci in its GPU process to name the video - // card, so that exit kills the process; after a few tries CEF gives up on hardware and - // draws the rest of the session on SwiftShader: the client's interface rendered on the - // CPU. An empty list is the truthful answer from in here - nothing the app can see is on - // a PCI bus. Created at session start rather than shipped in the rootfs so an installed - // runtime is fixed too, and the table's guard below binds it only when the real file - // cannot be read, so it can never stand in front of real data. (WinNative, maxjivi05, - // deff1ac6, via Bannerlator 8fb668d1: 44 -> 85 fps scrolling the Big Picture library on a - // OnePlus 15, GPU-process crashes 12 -> 0.) File pciDevices = new File(fakeProc, "pci_devices"); if (!pciDevices.isFile()) { try { - //noinspection ResultOfMethodCallIgnored pciDevices.getParentFile().mkdirs(); - //noinspection ResultOfMethodCallIgnored pciDevices.createNewFile(); } catch (java.io.IOException e) { - // It then fails the isFile() test below and the session runs as it did before. } } - // protonfixes, which GE-Proton and proton-cachyos run before every game, opens this to - // check the esync file limit and dies on the PermissionError, so no game starts with them. - // The value is the 64-bit kernel default. File fileMax = new File(fakeProc, "file-max"); if (!fileMax.isFile()) { try { Files.write(fileMax.toPath(), "9223372036854775807\n".getBytes(StandardCharsets.US_ASCII)); } catch (IOException e) { - // Left unbound, as above. } } String[][] procFiles = { @@ -270,7 +194,6 @@ public final class LinuxRuntime { }; for (String[] entry : procFiles) { File fake = new File(fakeProc, entry[0]); - // A live stand-in the session binds itself (CpuStatComponent's /proc/stat) wins. boolean live = false; if (extraBinds != null) for (String spec : extraBinds) live |= spec.endsWith(":" + entry[1]); if (!live && fake.isFile() && !new File(entry[1]).canRead()) { @@ -288,14 +211,6 @@ public final class LinuxRuntime { return specs; } - /** - * An app process may not open {@code /dev/dri} - the nodes exist but are labelled - * {@code graphics_device}, which stock policy grants surfaceflinger and not us - yet libdrm and - * everything built on it identify a GPU by its render node, and gamescope refuses to offer - * linux-dmabuf without one. The KGSL device Turnip actually drives ({@code gpu_device}, which we - * may open) stands in: it appears as a render node with the sysfs entries libdrm reads, and our - * Turnip build reports the same device numbers for it. - */ private static void bindGpuNode(Context context, List<String> cmd) { StructStat st; try { @@ -324,8 +239,6 @@ public final class LinuxRuntime { if (!Files.isSymbolicLink(subsystem.toPath())) { Os.symlink("/sys/bus/platform", subsystem.getPath()); } - // What MangoHud names a GPU's driver by; msm_drm is how an Adreno's render node reads - // on a mainline kernel, and the driver it reads the load of (bindAdrenoStats). File driver = new File(device, "driver"); if (!Files.isSymbolicLink(driver.toPath())) { Os.symlink("/sys/bus/platform/drivers/msm_drm", driver.getPath()); @@ -339,22 +252,11 @@ public final class LinuxRuntime { bindDrmClass(base, cmd, node, major + ":" + minor); } - /** - * MangoHud (Deck mode's performance overlay, mangoapp) finds the GPU by listing - * {@code /sys/class/drm}, and an exception from that listing ends the process: under an - * enforcing SELinux policy - every retail phone - an app may not list it, so mangoapp died on - * start, gamescope restarted it until the session gave up, and there was no overlay. Where the - * app cannot list it, the session's {@code /sys/class/drm} is ours: the render node above, - * named as an Adreno's is. MangoHud then reads the GPU's stats from KGSL's sysfs, which such a - * device refuses too - SessionService's GpuStatsComponent stands in for that. Where the - * listing is allowed it is left alone. - */ private static void bindDrmClass(File base, List<String> cmd, String node, String devNumbers) { if (new File("/sys/class/drm").list() != null) return; File drmClass = new File(base, "class"); try { if (!drmClass.isDirectory() && !drmClass.mkdirs()) return; - // Only symlinks are ever made here; deleting them must not follow one into its target. File[] old = drmClass.listFiles(); if (old != null) for (File f : old) Files.deleteIfExists(f.toPath()); Os.symlink("/sys/dev/char/" + devNumbers, new File(drmClass, node).getPath()); @@ -363,15 +265,12 @@ public final class LinuxRuntime { } bind(cmd, drmClass.getPath() + ":/sys/class/drm"); android.util.Log.i("LinuxRuntime", "hud: /sys/class/drm is not listable here; the session's lists " + node); - // Also refused to list, also asked for by MangoHud twice a second - an error line each - // time in the session log - and holding nothing it reads on an Adreno phone. if (new File("/sys/class/powercap").list() == null) { File empty = new File(base, "empty-powercap"); if (empty.isDirectory() || empty.mkdirs()) bind(cmd, empty.getPath() + ":/sys/class/powercap"); } } - /** The temperature of a CPU zone the app may read (the first by name), or null. */ public static String cpuTempSource() { for (java.util.Map.Entry<String, File> e : new java.util.TreeMap<>(thermalZones()).entrySet()) { File temp = new File(e.getValue(), "temp"); @@ -380,7 +279,6 @@ public final class LinuxRuntime { return null; } - /** The file the GPU's temperature reads from, or null where the app may read none. */ public static String gpuTempSource() { String kgsl = "/sys/class/kgsl/kgsl-3d0/"; return firstReadable(kgsl + "temp", kgsl + "devfreq/temp", thermalZone("gpu")); @@ -391,12 +289,6 @@ public final class LinuxRuntime { return firstReadable(kgsl + "gpu_busy_percentage", kgsl + "devfreq/gpu_load"); } - /** - * Valve's mangoapp (Deck mode's performance overlay) reads an Adreno GPU's load, clock and - * temperatures from where they are on Valve's own hardware; every Adreno under Android keeps - * them in KGSL's sysfs, readable by the app, though which files a kernel has differs between - * Snapdragon generations. - */ private static void bindAdrenoStats(List<String> cmd, List<String> extraBinds) { String kgsl = "/sys/class/kgsl/kgsl-3d0/"; String gpuTemp = gpuTempSource(); @@ -409,13 +301,10 @@ public final class LinuxRuntime { {thermalZone("ddr"), "/sys/class/thermal/thermal_zone22/temp"}, }; for (String[] stat : stats) { - // A value the session feeds itself (GpuStatsComponent, where KGSL is refused) wins. boolean fed = false; if (extraBinds != null) for (String spec : extraBinds) fed |= spec.endsWith(":" + stat[1]); if (stat[0] != null && !fed) bind(cmd, stat[0] + ":" + stat[1]); } - // Which of the overlay's GPU values this device lets the app read: a phone under an - // enforcing policy refuses some of them, and the overlay then has no line for that value. android.util.Log.i("LinuxRuntime", "hud sources: gpu load " + orNone(stats[0][0]) + ", gpu clock " + orNone(stats[1][0]) + ", gpu temp " + orNone(gpuTemp) + ", ddr temp " + orNone(stats[4][0]) + ", thermal zones " + thermalZones().size() @@ -426,12 +315,6 @@ public final class LinuxRuntime { return path != null ? path : "none"; } - /** - * mangoapp's CPU temperature is the mean of the thermal zones named cpuN-thermal or - * cpuN-top-thermal, as mainline kernels name them; Android kernels name the same sensors - * cpuss-0, cpu-1-0, cpu0-silver-usr, apc1-cpu0-usr and the like, so those zones are shown - * under the mainline name. - */ private static void bindCpuTemps(List<String> cmd, File root) { File name = new File(root, "etc/droiddeck/cpu-thermal-type"); try { @@ -454,7 +337,6 @@ public final class LinuxRuntime { return null; } - /** The temp file of the thermal zone for a sensor: the one named so, else the first whose name has it. */ private static String thermalZone(String sensor) { java.util.Map<String, File> zones = thermalZones(); File zone = zones.get(sensor); @@ -464,7 +346,6 @@ public final class LinuxRuntime { return zone != null ? new File(zone, "temp").getPath() : null; } - /** The device's thermal zones by their sensor's name, in lower case. */ private static java.util.Map<String, File> thermalZones() { java.util.Map<String, File> byType = new java.util.HashMap<>(); File[] zones = new File("/sys/class/thermal").listFiles((dir, n) -> n.startsWith("thermal_zone")); @@ -492,14 +373,12 @@ public final class LinuxRuntime { cmd.add(spec); } - /** PRoot's complex -k format: sysname, nodename, release, version, machine, domain, HWCAP. */ static String guestUtsname(String hostname) { StructUtsname host = Os.uname(); return "\\" + host.sysname + "\\" + hostname + "\\" + host.release + "\\" + host.version + "\\" + host.machine + "\\localdomain\\-1\\"; } - /** X access control and Steam look the session user up by uid: the app uid is root inside. */ public static void writeAccounts(Context context) throws IOException { File root = rootDir(context); int uid = Process.myUid(); diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntimeInstaller.java b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntimeInstaller.java index c6d0fb22c..b2d0571cf 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntimeInstaller.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/LinuxRuntimeInstaller.java @@ -29,30 +29,19 @@ import java.nio.file.StandardCopyOption; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; -/** - * Downloads and unpacks the Linux runtime rootfs into {@code files/linuxfs}. It is opt-in: nothing - * fetches it until the user asks for it, and a gamescope session refuses to start without it. - * - * <p>The catalog row names the build, its URL and its sha256, so an interrupted or corrupted - * download is caught before anything is written into the app's files directory. - */ public final class LinuxRuntimeInstaller { private static final String TAG = "LinuxRuntimeInstaller"; - /** Catalog row, beside the other component catalogs in winlator-contents. */ public static final String CATALOG_URL = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/main/linuxfs.json"; private static final String VERSION_FILE = ".version"; - /** What a stage is doing, for code that branches on it rather than on its (translated) words. */ public enum Step { DOWNLOADING, VERIFYING, OTHER } public interface ProgressListener { - /** {@code percent} is -1 while the size is unknown. */ void onProgress(String stage, int percent); - /** The same with its {@link Step}; a listener that branches on the step overrides this one. */ default void onProgress(Step step, String stage, int percent) { onProgress(stage, percent); } @@ -72,11 +61,6 @@ public final class LinuxRuntimeInstaller { } } - /** - * One runtime operation per process. The launcher and session can both install, and a - * launcher rebuilt mid-install (a pad plugged in, dark mode switched) forgets it had: a second - * install would share the first one's archive and staging directory and wreck both. - */ private static final class Job { final CopyOnWriteArrayList<ProgressListener> listeners = new CopyOnWriteArrayList<>(); final CountDownLatch done = new CountDownLatch(1); @@ -95,7 +79,6 @@ public final class LinuxRuntimeInstaller { private LinuxRuntimeInstaller() {} - /** The build currently unpacked, or null when the runtime is not installed. */ public static String installedVersion(Context context) { File marker = new File(LinuxRuntime.rootDir(context), VERSION_FILE); if (!marker.isFile() || !LinuxRuntime.isInstalled(context)) return null; @@ -103,7 +86,6 @@ public final class LinuxRuntimeInstaller { return v == null ? null : v.trim(); } - /** The catalog's current build, or null when it cannot be reached or read. */ public static Release fetchRelease() { String body = Downloader.downloadString(CATALOG_URL); if (body == null || body.isEmpty()) return null; @@ -120,22 +102,8 @@ public final class LinuxRuntimeInstaller { } } - /** - * The guest's home directory, carried across an update rather than replaced with the tarball's - * empty one. Steam installs itself here - the client, the signed-in account and every - * downloaded game - so replacing the rootfs wholesale used to delete all three and leave the - * user re-downloading Steam and signing in again, with their library gone. - */ private static final String USER_DATA = "root"; - /** - * Downloads {@code release} and replaces whatever is installed with it. Returns false and - * leaves the existing runtime alone if the download or the checksum fails; the new rootfs is - * only moved into place once it has been unpacked whole. - * - * <p>{@link #USER_DATA} survives the swap: the system is replaced, what the user put in it is - * not. An update therefore keeps Steam, the login and the installed games. - */ public static boolean install(Context context, Release release, ProgressListener listener) { Job job; boolean owner; @@ -169,7 +137,6 @@ public final class LinuxRuntimeInstaller { } } - /** True while an install is running in this process, whoever started it. */ public static boolean isInstalling() { synchronized (JOB_LOCK) { return running != null && !running.removal && !running.maintenance; @@ -188,7 +155,6 @@ public final class LinuxRuntimeInstaller { synchronized (JOB_LOCK) { return running != null && running.maintenance; } } - /** Reserve an in-place package operation with the same exclusion/progress as runtime installs. */ public static Maintenance beginMaintenance(Context context, String stage) { if (com.droiddeck.launcher.session.SessionState.INSTANCE.getRunning()) return null; com.droiddeck.launcher.session.SessionPhase phase = com.droiddeck.launcher.session.SessionState.INSTANCE.getPhase(); @@ -241,10 +207,6 @@ public final class LinuxRuntimeInstaller { return new File(root.getParentFile(), root.getName() + ".removing"); } - /** - * Follows the runtime operation already running, including removal, after a screen rebuild. - * Blocks until it ends and returns its result, or null when there is no current operation. - */ public static Boolean attach(ProgressListener listener) { Job job; synchronized (JOB_LOCK) { @@ -271,15 +233,12 @@ public final class LinuxRuntimeInstaller { private static boolean installOnce(Context context, Release release, ProgressListener listener) { File archive = new File(context.getCacheDir(), "linuxfs.tar.zst"); try { - // A failed/interrupted explicit removal must finish before a fresh installation. - // Its quarantined home is not user data to carry into an update. File pendingRemoval = removalDirectory(LinuxRuntime.rootDir(context)); if (pendingRemoval.exists() && listener != null) listener.onProgress(context.getString(R.string.rtinst_removing_leftovers), -1); if (!discard(pendingRemoval, null)) throw new IOException("Cannot clear the previous runtime: " + pendingRemoval); emptyTrash(LinuxRuntime.rootDir(context).getParentFile()); String downloading = context.getString(R.string.rtinst_downloading); if (listener != null) listener.onProgress(Step.DOWNLOADING, downloading, 0); - // Downloader reports a 0..1 fraction, or -1 while the total size is unknown. boolean ok = Downloader.downloadFile(release.url, archive, true, (fraction) -> { if (listener != null) { listener.onProgress(Step.DOWNLOADING, downloading, @@ -298,8 +257,6 @@ public final class LinuxRuntimeInstaller { return false; } - // Unpack beside the live rootfs and swap, so a failure here cannot leave a half - // runtime that isInstalled() would happily launch. File root = LinuxRuntime.rootDir(context); File staging = new File(root.getParentFile(), LinuxRuntime.DIR + ".new"); File old = new File(root.getParentFile(), LinuxRuntime.DIR + ".old"); @@ -321,16 +278,10 @@ public final class LinuxRuntimeInstaller { return false; } - // Carry the user's home over before the new rootfs takes the name. A rename inside the - // same filesystem, so a 30 GB library costs nothing and cannot half-copy; the tarball's - // own empty /root is dropped first so the rename has somewhere to land. If this fails - // the update is abandoned and the previous runtime is put back untouched - shipping a - // working system with the user's games gone is the worse outcome. try { RuntimeFileTree.carryHome(old, staging); } catch (IOException e) { Log.w(TAG, "could not carry root across the update; rolling back", e); - // Restore the live name even if cleaning the failed staging tree is denied. old.renameTo(root); try { RuntimeFileTree.delete(staging, null); } catch (IOException cleanup) { Log.w(TAG, "could not clear the failed staging tree", cleanup); } @@ -366,15 +317,8 @@ public final class LinuxRuntimeInstaller { } } - /** Where entries the app cannot unlink are set aside, out of the way of the runtime's names. */ static final String TRASH = ".runtime-trash"; - /** - * Gets {@code tree} out from under its name. What can be deleted is; if something inside - * cannot be (a file made by root or {@code su} carries another SELinux category, and the app - * may not unlink it), the remainder is moved aside into {@link #TRASH} instead of blocking - * every later install and removal on it. False only when even that move is refused. - */ static boolean discard(File tree, java.util.function.LongConsumer progress) { if (RuntimeFileTree.deleteWhatCan(tree, progress)) return true; File trash = new File(tree.getParentFile(), TRASH); @@ -388,7 +332,6 @@ public final class LinuxRuntimeInstaller { return !tree.exists(); } - /** Another try at what {@link #discard} set aside. Whatever still cannot go stays there. */ static void emptyTrash(File parent) { File trash = new File(parent, TRASH); if (trash.exists() && !RuntimeFileTree.deleteWhatCan(trash, null)) @@ -400,7 +343,6 @@ public final class LinuxRuntimeInstaller { return dir.isDirectory() && names != null && names.length == 0; } - /** Reserve removal before returning to the UI, so Play cannot race the worker. */ public static Removal beginUninstall(Context context) { if (com.droiddeck.launcher.session.SessionState.INSTANCE.getRunning()) return null; com.droiddeck.launcher.session.SessionPhase phase = com.droiddeck.launcher.session.SessionState.INSTANCE.getPhase(); @@ -408,7 +350,6 @@ public final class LinuxRuntimeInstaller { return beginUninstall(context.getApplicationContext(), LinuxRuntime.rootDir(context)); } - /** {@code app} words the progress; unit tests pass null and get none. */ static Removal beginUninstall(Context app, File root) { synchronized (JOB_LOCK) { if (running != null) return null; @@ -446,17 +387,12 @@ public final class LinuxRuntimeInstaller { } }; File pending = removalDirectory(root); - // Invalidate the installation before traversing it. A process killed halfway - // through leaves a named removal to resume, never a launchable partial runtime. if (root.exists()) { if (!discard(pending, progress) || !root.renameTo(pending)) throw new IOException("Cannot prepare the runtime for removal"); } else if (!pending.isDirectory() && !pending.mkdirs()) { throw new IOException("Cannot prepare the runtime for removal"); } - // Keep the quarantine until all leftovers are gone, so a failure stays retryable. - // An entry the app may not delete is set aside rather than failing the removal: - // a quarantine that can never clear would block reinstalling for good. for (File leftover : new File[]{ new File(root.getParentFile(), root.getName() + ".new"), new File(root.getParentFile(), root.getName() + ".old"), @@ -484,12 +420,6 @@ public final class LinuxRuntimeInstaller { } } - /** - * A whole-rootfs tar, which is not the shape the shared extractor handles: a distribution - * rootfs is full of hard links (one binary under several names), and an entry written as an - * empty file instead of its link target is a rootfs that boots to nothing. Symlinks, hard - * links and the executable bit are all carried over here. - */ public static boolean extract(Context context, File archive, File destination, ProgressListener listener) { long entries = 0; String extracting = context.getString(R.string.rtinst_extracting); @@ -498,7 +428,6 @@ public final class LinuxRuntimeInstaller { TarArchiveInputStream tar = new TarArchiveInputStream(in)) { TarArchiveEntry entry; while ((entry = tar.getNextTarEntry()) != null) { - // Refuse anything that would land outside the runtime directory. File file = ArchivePaths.inside(destination, entry.getName()); if (file == null) { Log.w(TAG, "skipping entry outside the rootfs: " + entry.getName()); @@ -515,10 +444,6 @@ public final class LinuxRuntimeInstaller { file.delete(); FileUtils.symlink(entry.getLinkName(), file.getAbsolutePath()); } else if (entry.isLink()) { - // A hard link to an earlier entry. Link where the filesystem allows it and - // fall back to a copy, which costs space but always works. - // The target is checked too: a link to "../../shared_prefs/x" would pull a - // file from outside the rootfs into it. File target = ArchivePaths.inside(destination, entry.getLinkName()); if (target == null) { Log.w(TAG, "skipping hard link outside the rootfs: " + entry.getLinkName()); @@ -540,7 +465,7 @@ public final class LinuxRuntimeInstaller { } if ((entry.getMode() & 0111) != 0) file.setExecutable(true, false); } else { - continue; // device nodes and fifos: the runtime binds the real ones + continue; } if (++entries % 2000 == 0 && listener != null) { listener.onProgress(extracting, -1); diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt b/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt index 10538c5bc..08ca03667 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt @@ -5,36 +5,14 @@ import com.droiddeck.launcher.session.SessionPrefs import java.io.File import java.security.MessageDigest -/** - * proot's fast path: the session's path lookups answered inside each process by libblfastpath - * (tools/proot/fastpath), which issues the translated call from a trampoline page proot's seccomp - * filter lets through (tools/proot/patches/0014). A stat or an open costs about a microsecond - * instead of a 15-60 us round trip through the tracer. - * - * The library maps guest paths to host paths itself, so it has to be told the rootfs and the - * binds exactly as proot was given them. Both sides carry a key derived from that view - proot - * PROOT_FASTPATH=<key>, the guest PROOT_FP_KEY=<key> - and the library answers nothing unless the - * process that traces it has the same key: a program that inherited the variables but runs under - * another proot (a Flatpak sandbox, a one-off command) leaves every call to its own proot. - * - * Needs proot's seccomp filter, so "Run proot without seccomp" turns it off as well. - */ object ProotFastPath { const val LIBRARY = "/usr/local/lib/libblfastpath.so" - /** MAX_BINDS in fastpath.c: past it the library turns itself off, so the session keeps it off. */ const val MAX_BINDS = 256 - /** - * Only the app's own proot (libproot.so, built with patches/0014) lets the trampoline through. - * The runtime's fallback copy (LinuxRuntime.prootBinary) is Termux's, which traps those calls - * too and translates their host paths a second time: every lookup then fails inside the - * process and goes to proot anyway, paying for both. - */ fun enabled(context: Context): Boolean = SessionPrefs.prootFastPath(context) && !SessionPrefs.prootNoSeccomp(context) && LinuxRuntime.prootBinary(context).parent == context.applicationInfo.nativeLibraryDir - /** The key for this rootfs and these binds, or null when the library cannot be told them. */ fun key(root: File, binds: List<String>): String? { if (binds.size > MAX_BINDS || binds.any { it.contains('|') } || root.path.contains('|')) return null val digest = MessageDigest.getInstance("SHA-256") @@ -42,10 +20,8 @@ object ProotFastPath { return digest.take(12).joinToString("") { "%02x".format(it) } } - /** What proot's own environment carries. */ fun hostEnv(key: String): Pair<String, String> = "PROOT_FASTPATH" to key - /** The guest's assignments (for `env -i`), describing the view proot was given. */ fun guestEnv(root: File, binds: List<String>, key: String): List<String> = listOf( "PROOT_FP_ROOT=" + root.path, "PROOT_FP_BINDS=" + binds.joinToString("|"), diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeFileTree.java b/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeFileTree.java index ca59389f2..44c78c84e 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeFileTree.java +++ b/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeFileTree.java @@ -12,11 +12,9 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.function.LongConsumer; import java.util.function.Predicate; -/** Checked removal of an app-owned runtime tree, including read-only directories. */ final class RuntimeFileTree { private RuntimeFileTree() {} - /** Unit tests stand in for an entry the app cannot unlink (a file another SELinux user made). */ static volatile Predicate<Path> refuseForTest; static void delete(File root, LongConsumer progress) throws IOException { @@ -28,8 +26,6 @@ final class RuntimeFileTree { try { attrs = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); } catch (NoSuchFileException e) { return; } if (attrs.isDirectory()) { - // Change directory permissions before opening it, so mode 000 also works. Links - // are inspected with lstat and unlinked directly; never chmod or visit their target. makeWritable(path.toFile()); try (DirectoryStream<Path> children = Files.newDirectoryStream(path)) { for (Path child : children) delete(child, progress, removed); @@ -39,10 +35,6 @@ final class RuntimeFileTree { if (progress != null) progress.accept(++removed[0]); } - /** - * Removes what it can of {@code root} and carries on past entries it cannot, rather than - * stopping at the first. Returns true when nothing is left. - */ static boolean deleteWhatCan(File root, LongConsumer progress) { return deleteWhatCan(root.toPath(), progress, new long[]{0}); } diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeInstallService.kt b/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeInstallService.kt index aef352baa..95a96b899 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeInstallService.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/RuntimeInstallService.kt @@ -12,15 +12,6 @@ import android.os.IBinder import com.droiddeck.launcher.MainActivity import com.droiddeck.launcher.R -/** - * Keeps the process in the foreground while the Linux runtime downloads and unpacks. The runtime - * is several hundred megabytes; without a foreground service Android may kill the app the moment - * the user switches away, and the install starts over from the resumable part file. - * - * The install itself is [LinuxRuntimeInstaller.install], which runs one job per process: the - * screen that asked for it joins the same job to show progress, so it does not matter which of the - * two gets there first. - */ class RuntimeInstallService : Service() { override fun attachBaseContext(newBase: android.content.Context) = super.attachBaseContext(com.droiddeck.launcher.core.AppLanguage.wrap(newBase)) @@ -39,7 +30,6 @@ class RuntimeInstallService : Service() { val manager = getSystemService(NotificationManager::class.java) var shown = "" val progress = LinuxRuntimeInstaller.ProgressListener { stage, percent -> - // Install percentages and throttled removal counts, including stage transitions. if ("$stage:$percent" != shown) { shown = "$stage:$percent" manager?.notify(NOTIFICATION_ID, notification(stage, percent)) @@ -87,7 +77,6 @@ class RuntimeInstallService : Service() { private const val EXTRA_SHA256 = "sha256" private const val EXTRA_SIZE = "size" - /** Starts the install of [release] in the foreground; a running install is joined instead. */ fun start(context: Context, release: LinuxRuntimeInstaller.Release) { val intent = Intent(context, RuntimeInstallService::class.java) .putExtra(EXTRA_VERSION, release.version) diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/UserApps.kt b/app/src/main/java/com/droiddeck/launcher/runtime/UserApps.kt index 21325b936..445a56348 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/UserApps.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/UserApps.kt @@ -19,20 +19,10 @@ import org.json.JSONObject import java.io.File import java.io.RandomAccessFile -/** - * The apps a user adds to the Desktop page: shell scripts, AppImages (from storage or a GitHub - * release) and Flatpak apps. Each opens full screen under gamescope, like an emulator. - * - * A script lives at /opt/droiddeck-apps/<id>: its folder at `files`, the script's name in `entry`. - * The folder is linked where it lies; shared storage is mounted noexec, so a folder holding Linux - * programs is copied in instead. A linked folder outside internal storage is bound into the - * session at its own path ([binds]), so the link resolves inside the guest. - */ object UserApps { private const val TAG = "UserApps" const val GUEST_DIR = "/opt/droiddeck-apps" const val SCRIPT_LAUNCHER = "/usr/local/bin/droiddeck-script-run" - /** An app the desktop asked for (droiddeck-open): its key, then the path it was started from there. */ const val DESKTOP_IMPORT = "desktop-import" private const val FLATPAK_OVERRIDES = ".flatpak" private const val ICON_SIZE = 256 @@ -47,10 +37,6 @@ object UserApps { enum class Kind { SCRIPT, APPIMAGE, FLATPAK } - /** - * [copied]: a script whose folder was copied in rather than linked. [repo]: the GitHub repository - * an AppImage came from, [version] the release it is. - */ class App( val key: String, val kind: Kind, val name: String, val icon: Any?, val detail: String?, val program: String, val args: List<String>, val copied: Boolean = false, @@ -74,10 +60,8 @@ object UserApps { class FlatpakBundle(val file: File) : Source() } - /** [icon]: a picked file's path or a suggested icon's URL. [meta]: files written into the added app's folder. */ class Request(val source: Source, val name: String?, val icon: String?, val meta: Map<String, String> = emptyMap()) - /** How a script's folder reaches the session: linked in place, or copied in ([bytes] to copy). */ class ScriptPlan(val folder: File, val copy: Boolean, val bytes: Long) private fun root(context: Context) = File(LinuxRuntime.rootDir(context), GUEST_DIR.substring(1)) @@ -117,7 +101,6 @@ object UserApps { private fun readName(dir: File) = FileUtils.readString(File(dir, "name"))?.trim()?.takeIf { it.isNotEmpty() } - /** Adds [request]; null on success, else what went wrong. */ fun add(context: Context, request: Request, onProgress: (String, Int) -> Unit): String? { if (!LinuxRuntime.isInstalled(context)) return context.getString(R.string.user_apps_runtime_required) val name = request.name?.trim()?.takeIf { it.isNotEmpty() } @@ -132,7 +115,6 @@ object UserApps { } } - /** [icon] as a file for [use]: a path as it is, a URL downloaded for the while. */ private inline fun <T> withIcon(context: Context, icon: String?, use: (File?) -> T): T { if (icon == null || !icon.startsWith("https://")) return use(icon?.let(::File)) val download = File(context.cacheDir, "icon-${System.nanoTime()}") @@ -143,10 +125,6 @@ object UserApps { } } - /** - * Renames [app] and sets its icon: [icon] a path or URL, "" for its default, null to keep it. - * The Linux desktop's menu entry follows. - */ fun edit(context: Context, app: App, name: String, icon: String?, fex: String? = null): String? { val id = app.key.substringAfter(':') if (id.isEmpty() || '/' in id || id.startsWith(".")) return null @@ -177,7 +155,6 @@ object UserApps { return when (app.kind) { Kind.SCRIPT -> { val dir = File(root(context), id) - // The link goes, never the folder it points to. File(dir, "files").delete() FileUtils.delete(dir) menuEntry(context, id).delete() @@ -190,7 +167,6 @@ object UserApps { } } - /** Folders linked from outside internal storage, bound into a session at their own paths. */ fun binds(context: Context): List<String> { val internal = Environment.getExternalStorageDirectory().canonicalPath return root(context).listFiles().orEmpty().mapNotNull { dir -> @@ -199,7 +175,6 @@ object UserApps { }.distinct() } - /** The desktop's import of [key] as the session sees it: an added script's folder, or an AppImage's. */ fun desktopImport(context: Context, key: String, path: String): String? { root(context).listFiles()?.firstOrNull { dir -> File(dir, "entry").isFile && FileUtils.readString(File(dir, DESKTOP_IMPORT))?.lineSequence()?.firstOrNull() == key @@ -263,7 +238,6 @@ object UserApps { "Terminal=false\nCategories=Game;\nX-DroidDeck-App=$id\n") } - /** Copies [from] into [to], keeping Linux programs and scripts executable. */ private fun copyTree(from: File, to: File) { from.walkTopDown().forEach { src -> val dst = File(to, src.relativeTo(from).path) @@ -275,7 +249,6 @@ object UserApps { } } - /** [source] (PNG, JPEG, WebP or SVG) as the app's icon: a PNG no larger than [ICON_SIZE] on a side. */ internal fun saveIcon(source: File, target: File): Boolean { val bitmap = (if (isSvg(source)) renderSvg(source) else decodeScaled(source)) ?: return false return try { @@ -306,7 +279,6 @@ object UserApps { f.inputStream().use { val head = ByteArray(1024); val n = it.read(head); n > 0 && String(head, 0, n).contains("<svg") } } catch (e: Exception) { false } - /** An SVG drawn at [ICON_SIZE], centred in its own proportions. */ private fun renderSvg(source: File): Bitmap? = try { val svg = source.inputStream().use { SVG.getFromInputStream(it) } if (svg.documentViewBox == null && svg.documentWidth > 0 && svg.documentHeight > 0) { @@ -319,7 +291,6 @@ object UserApps { Log.w(TAG, "svg ${source.path}", e); null } - /** "owner/repo" from what was typed: any link into the repository, its clone address, or the short form. */ fun githubRepo(input: String): String? { val t = input.trim() val m = Regex("^(?:https?://)?(?:www\\.)?github\\.com[/:]([\\w.-]+)/([\\w.-]+)", RegexOption.IGNORE_CASE).find(t) @@ -329,10 +300,6 @@ object UserApps { return if (repo.isEmpty() || repo == "." || repo == "..") null else "${m.groupValues[1]}/$repo" } - /** - * The AppImage among a release's files: named for aarch64/arm64, else for no other CPU, else the - * x86_64 one, which runs through FEX. - */ internal fun pickAsset(names: List<String>): String? { val images = names.filter { it.endsWith(".appimage", ignoreCase = true) } return images.firstOrNull { Regex("aarch64|arm64", RegexOption.IGNORE_CASE).containsMatchIn(it) } @@ -340,7 +307,6 @@ object UserApps { ?: images.firstOrNull { Regex("x86_64|x86-64|amd64|x64", RegexOption.IGNORE_CASE).containsMatchIn(it) } } - /** The newest release of [repo] with an AppImage this device runs, preferring full releases; else what went wrong. */ private fun latestRelease(context: Context, repo: String): Pair<Release?, String?> { val body = Downloader.downloadString("https://api.github.com/repos/$repo/releases?per_page=15") val releases = body?.let { runCatching { JSONArray(it) }.getOrNull() } @@ -358,7 +324,6 @@ object UserApps { return pick to if (pick == null) context.getString(R.string.user_apps_github_no_appimage, repo) else null } - /** Downloads [release] and hands it to [use]; null on success, else what went wrong. */ private fun withDownload(context: Context, release: Release, onProgress: (String, Int) -> Unit, use: (File) -> String?): String? { val download = File(context.cacheDir, "github-${release.asset}") try { @@ -382,10 +347,6 @@ object UserApps { } } - /** - * Images in [repo] that look like its icon, best first, then its owner's picture: links to show - * as suggestions and to download once one is picked. - */ fun githubIcons(repo: String): List<String> { val info = Downloader.downloadString("https://api.github.com/repos/$repo")?.let { runCatching { JSONObject(it) }.getOrNull() } ?: return emptyList() val branch = info.optString("default_branch").ifEmpty { return emptyList() } @@ -410,13 +371,11 @@ object UserApps { return urls + listOfNotNull(avatar) } - /** Whether [app], added from GitHub, has a newer release than the one it is. */ fun checkUpdate(context: Context, app: App): UpdateCheck { val repo = app.repo ?: return UpdateCheck.Failed(context.getString(R.string.user_apps_update_failed)) val (release, problem) = latestRelease(context, repo) if (release == null) return UpdateCheck.Failed(problem ?: context.getString(R.string.user_apps_update_failed)) val published = FileUtils.readString(File(AppImageManager.dir(context, app.key.substringAfter(':')), GITHUB_PUBLISHED))?.trim().orEmpty() - // Releases are compared by date, so a newest one without an ARM64 build never reads as a downgrade. val newer = if (published.isNotEmpty() && release.published.isNotEmpty()) release.published > published else release.tag != app.version return if (newer) UpdateCheck.Available(release) else UpdateCheck.Current(app.version ?: release.tag) } @@ -428,7 +387,6 @@ object UserApps { } } - /** A Flathub app ID from what was typed: the ID itself or its Flathub page. */ fun flatpakId(input: String): String? { val t = input.trim().removeSuffix("/").substringAfterLast('/') return t.takeIf { Regex("^[A-Za-z][\\w-]*(\\.[A-Za-z0-9_][\\w-]*){2,}$").matches(it) } @@ -444,7 +402,6 @@ object UserApps { private fun addFlatpakBundle(context: Context, file: File, name: String?, icon: File?, onProgress: (String, Int) -> Unit): String? { if (!file.isFile) return context.getString(R.string.user_apps_file_gone) if (!FlatpakManager.ready(context)) FlatpakManager.setup(context, onProgress)?.let { return it } - // Shared storage is bound into the runtime at its own path; a file anywhere else is copied in. val shared = Environment.getExternalStorageDirectory().canonicalPath val copy = File(LinuxRuntime.rootDir(context), "tmp/droiddeck-bundle.flatpak").takeIf { !file.canonicalPath.startsWith("$shared/") } try { diff --git a/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt index fc6526f59..3b3456139 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/BatteryComponent.kt @@ -8,30 +8,11 @@ import com.droiddeck.launcher.core.SessionPart import java.io.File import kotlin.math.abs -/** - * The battery the Steam client can see. - * - * The client reads `/sys/class/power_supply/BAT<n>/...` - a laptop's or a Deck's naming and files - * (`energy_now`, `power_now`, `time_to_empty_now`, a `uevent` of `POWER_SUPPLY_*` lines). Android - * names its supply `battery` and lays it out differently, and on many phones an app may not read - * it at all, so the client showed no battery. This writes one `BAT1` (the Deck's name) - * from Android's own battery API - the same on every device - into a directory the session binds - * over `/sys/class/power_supply`, refreshed every few seconds while the session runs. - * - * Steam's Quick Access Menu takes its time estimates from neither: it reads the files in `/run/vpower` (as on - * SteamOS), and with those absent showed "?h ?m" for projected battery life and for time to full. - * The same estimates are written there too, into [vpowerDir] in the guest's own `/run`. - * - * Values: charge in µAh from the charge counter; energy in µWh from charge × voltage; power in µW - * from |current| × voltage; time to empty from charge ÷ |average current| while discharging. Where - * a device reports no current, the client still gets status and percentage. - */ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart() { @Volatile private var running = false private var thread: Thread? = null override fun start() { - // Older builds exported the same aggregate battery twice; MangoHud counts both supplies. File(dir, "BAT0").deleteRecursively() running = true write() @@ -48,7 +29,6 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart running = false thread?.interrupt() thread = null - // Stale estimates would outlive the session in the rootfs; Steam falls back to "?h ?m". for (name in VPOWER_FILES) File(vpowerDir, name).delete() } @@ -67,7 +47,6 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart val chargeNowUah = bm?.getLongProperty(BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER)?.takeIf { it > 0 && it != Long.MIN_VALUE } ?: 0L val currentNowUa = bm?.getLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW)?.takeIf { it != Long.MIN_VALUE } ?: 0L val currentAvgUa = bm?.getLongProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE)?.takeIf { it != Long.MIN_VALUE && it != 0L } ?: currentNowUa - // Android's sign convention varies by vendor; the status says which way the current flows. val discharging = status == BatteryManager.BATTERY_STATUS_DISCHARGING || status == BatteryManager.BATTERY_STATUS_NOT_CHARGING val chargeFullUah = if (chargeNowUah > 0 && pct > 0) chargeNowUah * 100 / pct else 0L val energyNowUwh = chargeNowUah * voltageUv / 1_000_000L @@ -129,7 +108,6 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart "ac_status" to if (plugged) "Connected" else "Disconnected", "battery_percent" to "%.2f".format(java.util.Locale.ROOT, pct.toDouble()), "battery_status" to statusText, - // Steam reads a separate file for each direction; -1 = no estimate. "secs_until_battery_full" to (if (timeToFullS > 0) timeToFullS else -1L).toString(), "secs_until_shutdown_request" to (if (timeToEmptyS > 0) timeToEmptyS else -1L).toString(), ) @@ -139,7 +117,6 @@ class BatteryComponent(val dir: File, private val vpowerDir: File) : SessionPart } } - /** Atomic per file: a reader in the guest never sees a half-written value. */ private fun put(target: File, text: String) { val tmp = File(target.parentFile, "." + target.name + ".tmp") tmp.writeText(text) diff --git a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt index aa489198f..50d02c49b 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt @@ -22,30 +22,9 @@ import java.io.File import java.io.FileInputStream import java.io.FileOutputStream -/** - * FEX, DXVK and VKD3D-Proton per Proton, for the Linux Steam client. - * - * Every Proton the client runs games with keeps its own copy of these three inside its tree. This - * lists them, stores packages (.wcp: tar + zstd with a profile.json) downloaded from the Nightlies - * "-Linux" releases or imported, swaps one into a Proton, keeps the Proton's shipped files as - * "original bundles" - one per Proton build, so a Steam update adds a new one instead of replacing - * the last - restores those, and deletes stored packages. - * - * Where a Proton reads each component (its own `proton` script, ARM64 host): - * DXVK files/lib/wine/dxvk/{aarch64,i386}-windows -> the prefix's system32 / syswow64 - * VKD3D files/lib/wine/vkd3d-proton/{aarch64,i386}-windows -> the same, on every launch - * FEX files/lib/wine/aarch64-windows/lib{arm64ec,wow64}fex.dll - * files/lib/wine/aarch64-unix/lib{arm64ec,wow64}fex.so (FEX 2607 and newer) - * Proton copies DXVK and VKD3D into the prefix on every normal launch, so a swap applies the next - * time a game starts. A swap into a Proton a running game is using waits until that game exits. - * - * Same formats and data layout as the Decky "Components Manager" plugin, whose data is taken over - * once on first use. - */ object ComponentsManager { private const val TAG = "Components" private const val NIGHTLIES = "The412Banner/Nightlies" - /** Release tag -> component. Only the Linux repacks: the Android releases carry bionic unixlibs. */ private val RELEASES = linkedMapOf( "FexCore-Linux" to "fex", "Dxvk-Linux" to "dxvk", @@ -66,13 +45,7 @@ object ComponentsManager { "$WINE/aarch64-unix/libwow64fex.so", ) private val COMP_DIR = mapOf("dxvk" to "$WINE/dxvk", "vkd3d" to "$WINE/vkd3d-proton") - /** The folders a Proton reads on ARM64. */ private val PE_ARCHES = listOf("aarch64-windows", "i386-windows") - /** - * The DLLs that ARE the component. A swap clears exactly these before unpacking, so no file of - * the previous version is left mixed in; anything else a Proton keeps beside them - GE's - * openvr_api_dxvk.dll, which its launcher copies into every prefix and aborts without - stays. - */ private val COMP_DLLS = mapOf( "dxvk" to setOf("d3d8.dll", "d3d9.dll", "d3d10.dll", "d3d10_1.dll", "d3d10core.dll", "d3d11.dll", "dxgi.dll"), "vkd3d" to setOf("d3d12.dll", "d3d12core.dll"), @@ -93,7 +66,6 @@ object ComponentsManager { data class CatalogItem(val file: String, val comp: String, val release: String, val url: String, val size: Long, val digest: String) data class ProtonView( val proton: Proton, val components: Map<String, Component>, val originals: List<Original>, val inUseByGame: Boolean, - /** When a launch last had to put this Proton's chosen files back (epoch seconds), or 0. */ val reappliedAt: Long = 0, val sync: EsyncPacks.ToolState? = null, ) @@ -102,7 +74,6 @@ object ComponentsManager { private val lock = Any() - // ------------------------------------------------------------------ paths private fun root(context: Context) = LinuxRuntime.rootDir(context) private fun steam(context: Context) = File(root(context), "root/.local/share/Steam") @@ -112,10 +83,6 @@ object ComponentsManager { private fun originalsDir(context: Context) = File(dataDir(context), "originals").apply { mkdirs() } private fun stateFile(context: Context) = File(dataDir(context), "state.json") private fun catalogFile(context: Context) = File(dataDir(context), "catalog.json") - /** - * Inside the Linux runtime, read by the Proton launch wrappers (steam-compatibility's - * bl_components) before every game launch: desired.tsv + an unpacked copy of each package in use. - */ private const val LAUNCH_DIR = "root/.local/share/droiddeck-components" private const val LEGACY_LAUNCH_DIR = "root/.local/share/bannerlator-components" private fun launchDir(context: Context): File { @@ -135,7 +102,6 @@ object ComponentsManager { fun safeName(s: String): String = s.replace(Regex("[^A-Za-z0-9._+-]+"), "_").trim('_').ifEmpty { "unnamed" } - // ------------------------------------------------------------------ state private fun pendingStateFile(context: Context) = File(dataDir(context), "state.pending.json") @@ -173,7 +139,6 @@ object ComponentsManager { val expected = transaction.getString("launchSha256") val published = File(launchDir(context), "desired.tsv") val staged = File(launchDir(context), "desired.tsv.tmp") - // The launch-table rename is the commit point. Never read stale app selections after it. if (!staged.exists() && published.isFile && Hashes.sha256(published) == expected) { writeStateFile(stateFile(context), state.toString(2)) Log.i(TAG, "recovered published component selections") @@ -206,7 +171,6 @@ object ComponentsManager { private fun JSONObject.sub(key: String): JSONObject = optJSONObject(key) ?: JSONObject().also { put(key, it) } - // Keep launch-time restore rows after the last per-game selection is removed. private fun rememberProfiledComponent(state: JSONObject, comp: String) { val profiled = state.optJSONArray("profiledComponents")?.let { values -> (0 until values.length()).mapNotNull { values.optString(it).takeIf(COMPONENTS::contains) } @@ -252,7 +216,6 @@ object ComponentsManager { saveState(context, state) } - /** Takes over the Decky plugin's packages, originals and swap records, once. */ private fun migratePlugin(context: Context) { val marker = File(dataDir(context), ".plugin-imported") if (marker.isFile) return @@ -275,7 +238,6 @@ object ComponentsManager { marker.writeText("1\n") } - // ------------------------------------------------------------------ Protons private fun libraryRoots(context: Context): List<File> { val roots = mutableListOf(steam(context)) @@ -298,13 +260,10 @@ object ComponentsManager { if (parts.size > 1) parts[1] else parts.firstOrNull()?.ifEmpty { null } ?: "unknown" }.getOrDefault("unknown") - /** Every Proton tree with a Wine inside. Launch wrappers pointing at another Proton are left out. */ fun protons(context: Context): List<Proton> { val found = LinkedHashMap<String, Proton>() val bases = listOf(File(steam(context), "compatibilitytools.d") to false) + libraryRoots(context).map { File(it, "steamapps/common") to true } - // Not canonical: /data/user/0 is a link to /data/data on many devices, and the guest path is - // what is left after removing the rootfs prefix exactly as the paths were built. val rootPath = root(context).absolutePath.trimEnd('/') for ((base, valve) in bases) { val entries = base.listFiles()?.sortedBy { it.name } ?: continue @@ -322,17 +281,9 @@ object ComponentsManager { private fun proton(context: Context, id: String) = protons(context).firstOrNull { it.id == id } ?: throw IllegalStateException("No Proton with id $id") - /** - * Is anything running on this Proton right now - a game, or the client's own start-up runs, - * whose Wine processes can outlive them? Swaps wait while it is. Read from the command lines. - */ fun inUse(context: Context, proton: Proton): Boolean = anyProcess(context, proton) { paths, text -> processUsesProton(paths, text, gameOnly = false) } - /** - * Is a game running on this Proton? Only a launch runs proton with the verb waitforexitandrun - * (Proton's own script stays up until the game exits); the client's start-up runs do not. - */ fun gameRunning(context: Context, proton: Proton): Boolean = anyProcess(context, proton) { paths, text -> processUsesProton(paths, text, gameOnly = true) } @@ -357,14 +308,12 @@ object ComponentsManager { return false } - // ------------------------------------------------------------------ component files private fun readVersionFile(f: File): String = runCatching { val text = f.readText().trim() Regex("\\(([^)]+)\\)").find(text)?.groupValues?.get(1) ?: text.split(Regex("\\s+")).last() }.getOrDefault("") - /** The FEX build's version name; [unnamed] when its DLL carries none, "" when there is no FEX. */ private fun fexVersion(dir: File, unnamed: String = "FEX (build without a version name)"): String { val dll = File(dir, FEX_FILES[0]) if (!dll.isFile) return "" @@ -384,7 +333,6 @@ object ComponentsManager { private fun fingerprint(dir: File, comp: String): Map<String, String> = compFiles(dir, comp).filter { owned(comp, it) }.associateWith { Hashes.sha256(File(dir, it)) } - // ------------------------------------------------------------------ packages private fun normalize(name: String) = name.removePrefix("./").trimStart('/') @@ -406,14 +354,12 @@ object ComponentsManager { return Package(f.name, comp, prof.optString("versionName", f.nameWithoutExtension), prof.optString("description"), f.length()) } - /** The stored packages, newest version first. */ fun packages(context: Context): List<Package> = packagesDir(context).listFiles() ?.filter { it.isFile && it.name.endsWith(".wcp") } ?.mapNotNull { f -> runCatching { packageInfo(context, f) }.onFailure { Log.w(TAG, "skipping ${f.name}", it) }.getOrNull() } ?.sortedWith(newestFirst { it.version.ifEmpty { it.file } }) ?: emptyList() - /** Unpacks a package's files/ into a Proton; for DXVK/VKD3D the component's own DLLs are replaced as a set. */ private fun installFiles(context: Context, dir: File, comp: String, wcp: File): Map<String, String> { val staged = File(dataDir(context), "staging-${System.nanoTime()}") try { @@ -472,7 +418,6 @@ object ComponentsManager { check(tmp.renameTo(dest)) { "Could not save original bundle ${dest.name}" } } - // ------------------------------------------------------------------ originals private fun originalFile(context: Context, p: Proton, comp: String) = File(originalsDir(context), "${p.id}/${safeName(p.version)}/$comp.wcp").apply { parentFile?.mkdirs() } @@ -488,10 +433,6 @@ object ComponentsManager { } } - /** - * Saves a Proton's shipped files for this component before they are first replaced in this - * Proton build. A build already swapped into is not saved again: its files are ours. - */ private fun ensureOriginal(context: Context, p: Proton, comp: String, state: JSONObject) { val dest = originalFile(context, p, comp) if (dest.isFile) return @@ -520,7 +461,6 @@ object ComponentsManager { Log.i(TAG, "saved original $comp of ${p.name} ${p.version}") } - /** Saves the originals of every Proton build not seen yet (first open, and after a Steam update). */ fun snapshotAll(context: Context) = synchronized(lock) { migratePlugin(context) val state = loadState(context) @@ -530,7 +470,6 @@ object ComponentsManager { runCatching { saveState(context, state) }.onFailure { Log.w(TAG, "launch state", it) } } - // ------------------------------------------------------------------ view fun snapshot(context: Context): Snapshot = synchronized(lock) { migratePlugin(context) @@ -575,26 +514,15 @@ object ComponentsManager { Snapshot(views, packages(context)) } - /** - * The label of a chosen or queued original, in the app's language: the stored "label" was written - * in English when it was chosen. Null for a package, whose label is its version. - */ private fun originalLabel(context: Context, entry: JSONObject): String? = when { entry.optString("kind") == "original" -> context.getString(R.string.cmgr_original_version, entry.optString("protonVersion")) entry.optString("file").startsWith("original ") -> context.getString(R.string.cmgr_original_from, entry.optString("file").removePrefix("original ")) else -> null } - // ------------------------------------------------------------------ launch-time enforcement private data class PreparedLaunchState(val text: String, val keep: Set<String>) - /** - * Stages what each Proton should use for the launch wrappers. General rows use scope `*`; - * per-game rows use the game's compatdata id and replace General for that component. Deletion - * rows remove managed DLLs absent from the chosen package, so switching games cannot leave files - * from the previous game's component set behind. - */ private fun prepareLaunchState(context: Context, state: JSONObject): PreparedLaunchState { val dir = launchDir(context) val store = File(dir, "store").apply { mkdirs() } @@ -694,9 +622,7 @@ object ComponentsManager { return PreparedLaunchState(lines.toString(), keep) } - // ------------------------------------------------------------------ actions - /** Applies a package now, or queues it when a game is running on that Proton. Returns what happened. */ fun swap(context: Context, protonId: String, file: String): String = synchronized(lock) { val p = proton(context, protonId) val wcp = File(packagesDir(context), safeName(file)) @@ -749,7 +675,6 @@ object ComponentsManager { .put("protonVersion", p.version).put("files", JSONObject(fingerprint(p.dir, comp) as Map<*, *>)).put("at", System.currentTimeMillis() / 1000)) } - /** Runs the swaps that waited for a game to close. Cheap when nothing is queued. */ fun applyQueued(context: Context): List<String> = synchronized(lock) { val state = loadState(context) val queued = state.sub("queued") @@ -815,7 +740,6 @@ object ComponentsManager { context.getString(R.string.cmgr_deleted_original, protonVersion, LABEL[comp]) } - /** Imports a -linux .wcp from anywhere the app can read (the file picker hands over a copy). */ fun importPackage(context: Context, source: File, name: String): Package = synchronized(lock) { val info = packageInfo(context, source) val dest = File(packagesDir(context), safeName(name).let { if (it.endsWith(".wcp")) it else "$it.wcp" }) @@ -823,9 +747,7 @@ object ComponentsManager { info.copy(file = dest.name) } - // ------------------------------------------------------------------ catalog - /** The Nightlies listing: cached on disk, fetched again only when [force] (the refresh button). */ fun catalog(context: Context, force: Boolean): Catalog { val cache = catalogFile(context) if (!force && cache.isFile) runCatching { return parseCatalog(JSONObject(cache.readText())) } @@ -838,7 +760,6 @@ object ComponentsManager { val a = assets.getJSONObject(i) val name = a.optString("name") if (!name.endsWith(".wcp")) continue - // Only packages GitHub has a sha256 for are offered: the download is checked against it. if (!Hashes.isGithubSha256(a.optString("digest"))) continue items.put(JSONObject().put("file", name).put("comp", comp).put("release", tag) .put("url", a.optString("browser_download_url")).put("size", a.optLong("size")).put("digest", a.optString("digest"))) @@ -858,15 +779,9 @@ object ComponentsManager { return Catalog(items, o.optLong("fetchedAt")) } - /** By the version in [of], highest first: the newest builds lead a list, as they are what is picked. */ private fun <T> newestFirst(of: (T) -> String): Comparator<T> = Comparator { a, b -> ProtonExtras.compareVersions(of(b), of(a)) } - /** - * A package Proton does not ship, required on the GPUs listed here. An empty [models] set - * means every model in [families]. A row the user already chose in Components is left alone; - * a Steam update that puts the Proton's own files back is replaced on the next session. - */ internal data class ForcedPackage( val families: Set<GpuInfo.Family>, val comp: String, @@ -878,28 +793,15 @@ object ComponentsManager { } internal val FORCED_PACKAGES = listOf( - // DXVK 3 refuses to start unless the device exposes storageBuffer8BitAccess. Turnip on - // Adreno 6xx does not. 2.7.1 is the last DXVK 2 package on the Dxvk-Linux tag. ForcedPackage(setOf(GpuInfo.Family.A6XX), "dxvk", "dxvk-2.7.1-linux.wcp", "Dxvk-Linux"), ) - /** - * True when this Proton should receive the forced package. No active swap means the Proton's - * own copy. A record for a different Proton build means a Steam update put that copy back, - * whichever package was recorded. A record for this build is a Components choice and stays. - */ internal fun forcedPackageWanted(activeFile: String?, protonVersionMatches: Boolean): Boolean = activeFile.isNullOrEmpty() || !protonVersionMatches - /** The catalog row for this file and release, or null. */ internal fun catalogMatch(items: List<CatalogItem>, file: String, release: String): CatalogItem? = items.firstOrNull { it.file == file && it.release == release } - /** - * Install every [FORCED_PACKAGES] row this GPU matches into each Proton that - * [forcedPackageWanted] selects. Returns a line for the log, or null when this GPU or these - * Protons need nothing. A failed fetch throws and the session logs it without stopping. - */ fun ensureForcedPackages(context: Context): String? = ensureForcedPackages( context, GpuInfo.detect(), { file, release -> catalogItem(context, file, release) }, @@ -935,7 +837,6 @@ object ComponentsManager { return lines.joinToString("; ").ifEmpty { null } } - /** Null when [rule]'s package is stored and readable. Otherwise the log line, after a fetch. */ private fun missingPackage( context: Context, rule: ForcedPackage, @@ -951,11 +852,9 @@ object ComponentsManager { return null } - /** The cached Nightlies row for this file, fetching the listing when the cache does not have it. */ internal fun catalogItem(context: Context, file: String, release: String): CatalogItem? = resolveCatalogItem(file, release, { catalog(context, false).items }, { catalog(context, true).items }) - /** [cached] when it has the row, otherwise [refresh]. [refresh] is not called on a hit. */ internal fun resolveCatalogItem( file: String, release: String, @@ -963,7 +862,6 @@ object ComponentsManager { refresh: () -> List<CatalogItem>, ): CatalogItem? = catalogMatch(cached(), file, release) ?: catalogMatch(refresh(), file, release) - /** Downloads a catalog item into storage, verifying the release's sha256 and the package type. */ fun download(context: Context, item: CatalogItem, progress: (Int) -> Unit): Package { require(item.url.startsWith("https://github.com/$NIGHTLIES/releases/download/")) { context.getString(R.string.cmgr_nightlies_only) } require(Hashes.isGithubSha256(item.digest)) { context.getString(R.string.cmgr_list_predates_checksums) } diff --git a/app/src/main/java/com/droiddeck/launcher/session/CpuStatComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/CpuStatComponent.kt index 7c5aa9a28..40e459195 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/CpuStatComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/CpuStatComponent.kt @@ -5,19 +5,6 @@ import android.util.Log import com.droiddeck.launcher.core.SessionPart import java.io.File -/** - * A `/proc/stat` that moves. - * - * Android has refused apps `/proc/stat` since 8.0, and the session's stand-in for it was one - * `cpu0` line of zeros - enough for glibc and Steam to start, but every CPU load in the session - * read 0 % (the performance overlay showed one core, idle). The kernel still lets apps read each - * core's idle residency (`cpuidle/stateN/time`, µs, the same label as the cpufreq files), which is - * how overlays on Android work out CPU load: a core's busy time is the time that passed less the - * time it spent idle. This writes `/proc/stat` from that, one line per core, refreshed twice a - * second. Counters only grow - a sample's idle is never more than the time that passed - so a - * reader taking the difference of two reads never sees a negative. Where `/proc/stat` is readable, - * or the idle times are not, nothing is done. - */ class CpuStatComponent(val file: File) : SessionPart() { @Volatile private var running = false private var thread: Thread? = null @@ -28,7 +15,6 @@ class CpuStatComponent(val file: File) : SessionPart() { private var lastMs = 0L private val bootSeconds = (System.currentTimeMillis() - SystemClock.elapsedRealtime()) / 1000 - /** Writes the first sample; true when it should be bound over `/proc/stat`. */ fun prepare(): Boolean { if (File("/proc/stat").canRead() || cores.isEmpty() || idleUs(0) == null) return false lastMs = SystemClock.elapsedRealtime() @@ -62,7 +48,6 @@ class CpuStatComponent(val file: File) : SessionPart() { if (elapsed <= 0) return lastMs += elapsed * MS_PER_TICK for (i in cores) { - // An offline core has no idle residency to count and runs nothing: all idle. val us = idleUs(i) val idleTicks = if (us == null) elapsed else ((us - lastIdleUs[i]) / US_PER_TICK).coerceIn(0, elapsed) if (us != null) lastIdleUs[i] = us @@ -71,7 +56,6 @@ class CpuStatComponent(val file: File) : SessionPart() { } } - /** The whole file at once, renamed into place: a reader opens either the last sample or this one. */ private fun write() { val text = StringBuilder() text.append("cpu ${busy.sum()} 0 0 ${idle.sum()} 0 0 0 0 0 0\n") @@ -88,14 +72,12 @@ class CpuStatComponent(val file: File) : SessionPart() { companion object { private const val TAG = "SessionService" private const val PERIOD_MS = 500L - /** /proc/stat counts in USER_HZ, 100 a second on every Linux the guest runs. */ private const val MS_PER_TICK = 10L private const val US_PER_TICK = 10_000L private fun coreCount(): Int = File("/sys/devices/system/cpu").list()?.count { it.matches(Regex("cpu\\d+")) } ?: 0 - /** The core's total idle residency in µs, or null when it has none to read (offline). */ private fun idleUs(core: Int): Long? { val states = File("/sys/devices/system/cpu/cpu$core/cpuidle").listFiles { f -> f.name.startsWith("state") } ?: return null diff --git a/app/src/main/java/com/droiddeck/launcher/session/CrashHandler.kt b/app/src/main/java/com/droiddeck/launcher/session/CrashHandler.kt index 50d9213e9..3605db880 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/CrashHandler.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/CrashHandler.kt @@ -6,16 +6,6 @@ import java.io.File import java.io.PrintWriter import java.io.StringWriter -/** - * The last thing that runs when our own code throws on any thread: the stack trace into the - * session's folder, then the folder's ending ([SessionArtifacts.collect]), then Android's own - * handler so the crash is still reported and still lands in the crash buffer. Bounded, because - * a handler that hangs turns a crash into a frozen app: the collect gets a few seconds and the - * process dies regardless. - * - * A native crash (the compositor, a JNI call) never reaches here - the process is gone before - * Java sees anything. [SessionArtifacts.finishAbandoned] at the next start covers that case. - */ object CrashHandler { private const val TAG = "CrashHandler" @@ -40,10 +30,8 @@ object CrashHandler { worker.start() worker.join(8000) } - // The session's tree would outlive us otherwise, and block the next launch. OrphanReaper.reap("app crash") } catch (e: Throwable) { - // Nothing more to do for a crash inside the crash handler. } finally { previous?.uncaughtException(thread, error) ?: Runtime.getRuntime().halt(2) } diff --git a/app/src/main/java/com/droiddeck/launcher/session/GameEnvironmentStore.kt b/app/src/main/java/com/droiddeck/launcher/session/GameEnvironmentStore.kt index a87a6c096..023835fcc 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GameEnvironmentStore.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GameEnvironmentStore.kt @@ -11,7 +11,6 @@ import java.io.File object GameEnvironmentStore { private const val SETTINGS = "game-environment.json" private const val GUEST_FILE = "root/.config/droiddeck/game-environment.json" - /** The published file's extra DXVK options (droiddeck-game-env reads it; the app's own file has none). */ const val DXVK_CONFIG = "dxvkConfig" const val DXVK_CONFIG_GAMES = "dxvkConfigGames" @@ -76,8 +75,6 @@ object GameEnvironmentStore { fun publish(context: Context, config: GameEnvironment.Config = read(context)) { val resolved = config.copy(shared = GameEnvironment.defaults(SessionPrefs.fexPreset(context), SessionPrefs.forceSsbs(context)) + config.shared) val json = encode(resolved) - // The Display page's texture filtering, as DXVK options the launcher appends to - // DXVK_CONFIG after the profiles - a user's own DXVK_CONFIG entry keeps its options. val dxvk = TextureFiltering.dxvkOptions( SessionPrefs.textureAnisotropy(context), SessionPrefs.textureLodBias(context), TextureFiltering.autoLodBias(SessionState.upscaleRatio), diff --git a/app/src/main/java/com/droiddeck/launcher/session/GameProfileInventoryCache.kt b/app/src/main/java/com/droiddeck/launcher/session/GameProfileInventoryCache.kt index 1574c7d36..b140620e3 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GameProfileInventoryCache.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GameProfileInventoryCache.kt @@ -6,7 +6,6 @@ internal class GameProfileInventoryCache<T : Any> { private var revision: Long? = null private var value: List<T>? = null - // Called only by the refresh worker; the UI invalidates through request revisions. fun get(requestedRevision: Long, load: () -> List<T>): List<T> = synchronized(lock) { value?.takeIf { revision == requestedRevision } ?: load().toList().also { value = it diff --git a/app/src/main/java/com/droiddeck/launcher/session/GameSaves.kt b/app/src/main/java/com/droiddeck/launcher/session/GameSaves.kt index 8e9fe257d..3a31da011 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GameSaves.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GameSaves.kt @@ -17,30 +17,18 @@ import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream -/** - * Game saves in and out of a game's Proton prefix, as zips other apps read: GameHub keeps a prefix's - * drive_c rooted at "/drive_c/users/steamuser/…", Winlator / WinNative / Bannerlator at - * "drive_c/users/xuser/…". Proton here runs as steamuser too, so on the way in any user folder but - * Public becomes steamuser, and on the way out the chosen layout decides the user segment. - * - * Saves belong to the game's prefix (compatdata/<appid>/pfx), whichever Proton it runs with. - * Ported from Bannerlator's GameSaveBackup and SaveLocator. - */ object GameSaves { enum class Layout(val app: String, val rootPrefix: String, val user: String) { GAMEHUB("GameHub", "/drive_c/", "steamuser"), WINLATOR("Winlator", "drive_c/", "xuser"); - /** "GameHub zip", in the app's language. */ fun label(context: Context): String = context.getString(R.string.saves_layout_zip, app) } - /** A game, its prefix, and the save folders found in it. */ data class Game(val game: Library.SteamGame, val prefix: File?, val saves: List<SaveDir>) { val launched get() = prefix?.let { File(it, "drive_c").isDirectory } == true } - /** A folder of saves, relative to the steamuser profile. */ data class SaveDir(val relPath: String, val files: Int, val bytes: Long) private const val USER = "steamuser" @@ -48,21 +36,14 @@ object GameSaves { fun savesDir(): File = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "DroidDeck/Saves") private fun backupsDir(): File = File(savesDir(), "backups") - /** [game] with its prefix and the saves found there; reads files, so not on the main thread. */ fun game(game: Library.SteamGame): Game = Game(game, game.protonPrefix, game.protonPrefix?.let { locate(it, game) } ?: emptyList()) - // ------------------------------------------------------------------ finding saves - /** Roots of the steamuser profile that hold most PC game saves, in the order they are trusted. */ private val SAVE_ROOTS = listOf("Documents/My Games", "Saved Games", "AppData/Roaming", "Documents", "AppData/Local", "AppData/LocalLow") private const val KEEP_THRESHOLD = 50 private fun profile(prefix: File) = File(prefix, "drive_c/users/$USER") - /** - * Folders under the save roots, one and two deep, whose names match the game's name or its folder's. - * Nested matches keep the outer folder only. - */ fun locate(prefix: File, game: Library.SteamGame): List<SaveDir> { val profile = profile(prefix) if (!profile.isDirectory) return emptyList() @@ -102,7 +83,6 @@ object GameSaves { return Ident(cleaned.filter { it.isLetterOrDigit() }, cleaned.split(Regex("[^a-z0-9]+")).filter { it.isNotEmpty() }.toSet()) } - /** exact 100, one contains the other 70, half the words shared 50, close spelling 40. */ private fun score(a: Ident, b: Ident): Int { if (a.norm.isEmpty() || b.norm.isEmpty()) return 0 if (a.norm == b.norm) return 100 @@ -123,15 +103,10 @@ object GameSaves { return prev[b.length] } - // ------------------------------------------------------------------ export private fun stamp() = SimpleDateFormat("yyyy-MM-dd_HHmm", Locale.US).format(Date()) private fun safe(name: String) = name.replace(Regex("[/\\\\:*?\"<>|]"), "_").trim().ifEmpty { "game" } - /** - * Zips the game's saves into [dir] as "<Game>_<date>.zip" in [layout]. The found save folders when - * there are any, else the whole steamuser profile minus Temp and crash dumps. Returns the zip. - */ fun export(context: Context, game: Game, layout: Layout, dir: File): Pair<File, Int> { val prefix = game.prefix ?: error(context.getString(R.string.saves_not_launched, game.game.name)) dir.mkdirs() @@ -160,13 +135,7 @@ object GameSaves { return count } - // ------------------------------------------------------------------ import - /** - * Unzips a GameHub or Winlator save zip into the game's drive_c, after zipping what its steamuser - * profile holds now into Download/DroidDeck/Saves/backups. Entries outside drive_c, launcher - * shortcuts and anything escaping drive_c are skipped. Returns (files written, backup or null). - */ fun import(context: Context, game: Game, zip: File): Pair<Int, File?> { val prefix = game.prefix ?: error(context.getString(R.string.saves_not_launched, game.game.name)) val driveC = File(prefix, "drive_c") @@ -201,7 +170,6 @@ object GameSaves { return written to backup.takeIf { it.exists() } } - /** Which layout a zip was written in, from its first user folder; null when it has none. */ fun layoutOf(zip: File): Layout? = runCatching { ZipInputStream(BufferedInputStream(zip.inputStream())).use { zis -> var e = zis.nextEntry @@ -223,11 +191,9 @@ object GameSaves { } }.getOrDefault(false) - /** An entry's path below drive_c, with any user folder but Public renamed to steamuser. */ private fun remap(raw: String): String? { val segs = driveCSegments(raw)?.takeIf { it.isNotEmpty() } ?: return null if (segs.any { it == ".." }) return null - // GameHub also packs its own launcher shortcuts; they are never saves. if (segs[0].equals("proton_shortcuts", true)) return null val last = segs.last().lowercase() if ((last.endsWith(".lnk") || last.endsWith(".desktop") || last.endsWith(".url")) && segs.any { it.equals("Desktop", true) }) return null diff --git a/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt b/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt index df457d2e3..096f8e588 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt @@ -9,20 +9,9 @@ import com.droiddeck.launcher.core.FileUtils import com.droiddeck.launcher.frontend.AddedGames import java.io.File -/** - * Where a second Steam library can live on this device. - * - * An SD card is offered through the app's own folder on it ({@code Android/data/<pkg>/files/steam}): - * the one place on a card a targetSdk-28 app may write without a permission prompt, and a real - * filesystem path the session can bind. Any other folder comes from the File Manager's pick mode - * and is checked for writing before it is accepted. Both are FUSE-backed on Android and stream - * slowly (intro movies, big asset loads); internal stays the default and the dialog says so. - */ object GameStorage { - /** A library root: [label] as the menu lists it (a volume with its free space), [name] alone. */ class Option(val label: String, val path: String, val name: String = label) - /** Every removable volume, as its app folder, with the free space it has. */ fun options(context: Context): List<Option> { val sm = context.getSystemService(StorageManager::class.java) return context.getExternalFilesDirs(null).orEmpty().filterNotNull().mapNotNull { dir -> @@ -40,10 +29,6 @@ object GameStorage { "?" } - /** - * Makes [path] a library root the session can bind - the folder and its steamapps/ - and proves - * it writable. Returns why it cannot be used, or null when it can. - */ fun prepare(context: Context, path: String): String? { val root = File(path) val steamapps = File(root, "steamapps") @@ -56,7 +41,6 @@ object GameStorage { } } - /** The root the session binds, from the setting: automatic = the first card in the phone. */ fun effective(context: Context): Option? { val pref = SessionPrefs.gameStorage(context) return when { @@ -68,12 +52,6 @@ object GameStorage { class Library(val host: File, val guest: String, val label: String) - /** - * Games folders an earlier build registered as Steam libraries that Steam has installed games - * into (an appmanifest in their steamapps/): those stay libraries, so the games are not lost. - * Any other Games folder is no longer one; the session's library list leaves it out and the - * runtime's droiddeck-steam-library forgets it. - */ fun gamesFolderLibraries(context: Context): List<Library> { if (SessionPrefs.gameStorage(context).isNotEmpty()) return emptyList() val card = effective(context)?.let { canonical(File(it.path)) } @@ -90,13 +68,11 @@ object GameStorage { } } - /** Steam has installed a game into [root] as a library: a manifest in its steamapps/. */ internal fun hasInstalledGames(root: File): Boolean = File(root, "steamapps").listFiles { f -> f.isFile && f.name.startsWith("appmanifest_") && f.name.endsWith(".acf") }?.isNotEmpty() == true private fun canonical(file: File): String = runCatching { file.canonicalPath }.getOrDefault(file.absolutePath) - /** The label the client shows for a chosen folder: its last name, or the volume's. */ fun labelFor(context: Context, path: String): String = options(context).firstOrNull { it.path == path }?.name ?: File(path).name.ifEmpty { context.getString(R.string.gstore_folder) } diff --git a/app/src/main/java/com/droiddeck/launcher/session/GpuClockPin.kt b/app/src/main/java/com/droiddeck/launcher/session/GpuClockPin.kt index 8e0c7c8a0..7855c4959 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GpuClockPin.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GpuClockPin.kt @@ -5,18 +5,6 @@ import android.util.Log import com.droiddeck.launcher.wayland.WaylandCompositor import java.io.File -/** - * The GPU held at its top clock for a session, without root: KGSL's power-control property turned - * off on /dev/kgsl-3d0, the node the driver already opens (adrenotools' "turbo"). Left to itself the - * governor lets the GPU sit low between bursts, and a frame that lands on a low clock is a hitch; - * Bannerlator measured the clock going from 231 to 1000 MHz with this set. It costs power and heat, - * so it is opt-in, and the kernel's thermal limits still apply on top of it. - * - * The property belongs to the device, not to this process, and a killed app cannot unwind it: it - * is cleared when a session stops, and at the next app start when a marker file says this app set - * it and never cleared it - only then, so another app's own setting is not undone. - * (Bannerlator, PerfGpuTurbo.) - */ object GpuClockPin { private const val TAG = "GpuClockPin" private const val KGSL_NODE = "/dev/kgsl-3d0" @@ -28,7 +16,6 @@ object GpuClockPin { private val supported: Boolean by lazy { File(KGSL_NODE).exists() } - /** At a session's start: pins the clock when the user asked for it. The line is for the log. */ @Synchronized fun start(context: Context): String { if (!SessionPrefs.gpuClockPin(context)) return "gpu clock: governed" @@ -44,13 +31,11 @@ object GpuClockPin { } } - /** At a session's end. */ @Synchronized fun stop(context: Context) { if (pinned) clear(context) } - /** At the app's start: a process killed with the clock pinned left only the marker behind. */ @Synchronized fun clearLeftover(context: Context) { if (File(context.filesDir, MARKER).exists()) clear(context) diff --git a/app/src/main/java/com/droiddeck/launcher/session/GpuMemComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/GpuMemComponent.kt index efa6a7956..449c2bb7f 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GpuMemComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GpuMemComponent.kt @@ -4,32 +4,11 @@ import android.util.Log import com.droiddeck.launcher.core.SessionPart import java.io.File -/** - * The GPU memory in use, for the performance overlay's VRAM and PVRAM lines. - * - * Valve's mangoapp reads it from the kernel's gpu_mem_total tracepoint, which no Android app may - * reach, so both lines read 0. Our libtracefs for mangoapp (tools/mangoapp/libtracefs-shim.c) hands - * it what this writes to [file] - `/run/droiddeck-hud/gpu-mem` in the guest - every few seconds, in - * bytes, from the best of what KGSL lets the app read: - * - * - `/sys/class/kgsl/kgsl/page_alloc`, every GPU page on the device - what the tracepoint's total - * counts, where the policy allows it; - * - else KGSL's own accounting of each of the session's processes - * (`/sys/class/kgsl/kgsl/proc/<pid>/gpumem_mapped` and `gpumem_unmapped`); - * - else the size of the KGSL mappings in each of the session's processes' `maps` - only the - * buffers the CPU has mapped, so less than the GPU holds (about 40 % of KGSL's own count in a - * game on an AYN Thor), but always readable: every process the app can see in `/proc` is one of - * its own. `maps` rather than `smaps`, which gives the same sizes but walks page tables to do it: - * in a game, 180,000 lines and most of a second of CPU a pass. - * - * Sampled only while the overlay shows (OverlayShown). - */ class GpuMemComponent(private val file: File, private val root: File) : SessionPart() { @Volatile private var running = false private var thread: Thread? = null private var source = "" - /** True when there is a source to write from. */ fun prepare(): Boolean { val bytes = sample() ?: return false write(bytes) @@ -76,11 +55,9 @@ class GpuMemComponent(private val file: File, private val root: File) : SessionP return null } - /** The processes this app may see in /proc: Android hides every other uid's. */ private fun sessionPids(): List<String> = File("/proc").list()?.filter { it.isNotEmpty() && it.all(Char::isDigit) }.orEmpty() - /** Size of a process's mappings of the KGSL device, in kB; null where its maps cannot be read. */ private fun kgslMappedKb(pid: String): Long? = runCatching { var bytes = 0L File("/proc/$pid/maps").forEachLine { line -> @@ -95,7 +72,6 @@ class GpuMemComponent(private val file: File, private val root: File) : SessionP bytes / 1024 }.getOrNull() - /** Whole, renamed into place: the reader opens either the last value or this one. */ private fun write(bytes: Long) { try { file.parentFile?.mkdirs() @@ -109,7 +85,6 @@ class GpuMemComponent(private val file: File, private val root: File) : SessionP companion object { private const val TAG = "SessionService" private const val KGSL_ROOT = "/sys/class/kgsl/kgsl" - /** GPU memory moves slowly; a pass over every process's maps is ~40 ms of CPU in a game. */ private const val PERIOD_MS = 5_000L private fun readLong(path: String): Long? = diff --git a/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt index 205240fa2..fb6efe09f 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt @@ -6,34 +6,15 @@ import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File import java.io.RandomAccessFile -/** - * The Adreno's stats as mangoapp (Deck mode's performance overlay) reads them. - * - * MangoHud takes an Adreno's load, clock and temperature from `gpu_busy_percentage`, `clock_mhz` - * and `temp` in `/sys/class/kgsl/kgsl-3d0`, and ends the process on one of them it may not read - - * not on one that is missing, which it skips. An enforcing SELinux policy (every retail phone) - * refuses apps some or all of KGSL's sysfs, each vendor differently, so mangoapp died on start and - * the session ran with no overlay. Where any of the three is refused, a directory of our own is - * bound there instead. A value with no - * readable source is left out, and the overlay shows no line for it. Where all three are - * readable, nothing is done. - * - * Valve's build also reads load and clock where a mainline msm kernel has them (debugfs - * `perf_now`, the GPU's devfreq `cur_freq` in Hz) - the paths LinuxRuntime.bindAdrenoStats points - * at KGSL's own files when it may - so the same values are fed there too, in the same formats - * ([binds]). - */ class GpuStatsComponent(val dir: File) : SessionPart() { @Volatile private var running = false private var thread: Thread? = null private val feeds = ArrayList<Feed>() - /** One of MangoHud's files and where its value comes from. */ private class Feed( val file: File, val source: String, val read: () -> Long?, val format: String, val percent: Boolean = false, ) - /** Where Valve's msm reader looks, and the feed for it, as host:guest binds. */ fun binds(): List<String> { val list = mutableListOf(dir.path + ":" + KGSL) if (File(dir, "perf_now").exists()) list.add(File(dir, "perf_now").path + ":" + MSM_LOAD) @@ -41,7 +22,6 @@ class GpuStatsComponent(val dir: File) : SessionPart() { return list } - /** Writes the directory; true when it should be bound over [KGSL]. */ fun prepare(): Boolean { if (!File(KGSL).exists() || MANGOHUD_FILES.none { File(KGSL, it).let { f -> f.exists() && !f.canRead() } }) return false return try { @@ -50,8 +30,6 @@ class GpuStatsComponent(val dir: File) : SessionPart() { feeds.clear() loadFeed()?.let { load -> feeds.add(load) - // KGSL's gpu_busy_percentage reads "37 %"; padded after, so a shorter value - // written over a longer one leaves no digits behind. feeds.add(Feed(File(dir, "perf_now"), load.source, load.read, "%-6s\n", percent = true)) } pick("clock_mhz", clockSources(), "%5d\n") { mhz(it) }?.let { clock -> @@ -88,7 +66,6 @@ class GpuStatsComponent(val dir: File) : SessionPart() { thread = null } - /** Load: a file that states it in percent, else KGSL's busy and total of the last sample. */ private fun loadFeed(): Feed? { pick("gpu_busy_percentage", LOAD_SOURCES, "%3d %%\n") { it.coerceIn(0, 100) }?.let { return it } val gpubusy = "$KGSL/gpubusy" @@ -102,7 +79,6 @@ class GpuStatsComponent(val dir: File) : SessionPart() { return if (read() != null) Feed(File(dir, "gpu_busy_percentage"), gpubusy, read, "%3d %%\n") else null } - /** The first of [sources] that reads as a number, its value put in MangoHud's unit by [unit]. */ private fun pick(name: String, sources: List<String>, format: String, unit: (Long) -> Long?): Feed? { for (source in sources) { val read = { firstNumber(source)?.let(unit) } @@ -111,7 +87,6 @@ class GpuStatsComponent(val dir: File) : SessionPart() { return null } - /** In place and at one length, so a reader that keeps the file open sees each value whole. */ private fun write(feed: Feed, value: Long) { try { val text = if (feed.percent) String.format(feed.format, "$value %") else String.format(feed.format, value) @@ -123,17 +98,14 @@ class GpuStatsComponent(val dir: File) : SessionPart() { companion object { private const val TAG = "SessionService" const val KGSL = "/sys/class/kgsl/kgsl-3d0" - /** Must match LinuxRuntime.bindAdrenoStats. */ const val MSM_LOAD = "/sys/kernel/debug/dri/0/perf_now" const val MSM_CLOCK = "/sys/devices/platform/soc@0/3d00000.gpu/devfreq/3d00000.gpu/cur_freq" private const val PERIOD_MS = 500L private val MANGOHUD_FILES = listOf("gpu_busy_percentage", "clock_mhz", "temp") - /** Qualcomm's own GPU summary, beside KGSL in vendor kernels, under the plain sysfs label. */ private const val QCOM_GPU = "/sys/kernel/gpu" private val LOAD_SOURCES = listOf( "$KGSL/gpu_busy_percentage", "$KGSL/devfreq/gpu_load", "$KGSL/gpuload", "$QCOM_GPU/gpu_busy", ) - /** The GPU's devfreq node is named by its register address, which differs between Snapdragons. */ private val DEVFREQ_NODES = listOf( "kgsl-3d0", "3d00000.qcom,kgsl-3d0", "2c00000.qcom,kgsl-3d0", "5000000.qcom,kgsl-3d0", "5900000.qcom,kgsl-3d0", ) @@ -152,11 +124,9 @@ class GpuStatsComponent(val dir: File) : SessionPart() { private fun tempSources(): List<String> = listOfNotNull("$KGSL/temp", "$KGSL/devfreq/temp", "$QCOM_GPU/temp", LinuxRuntime.gpuTempSource()).distinct() - /** The first integer in the file ("550", "37 %", "550000000"), or null. */ private fun firstNumber(path: String): Long? = runCatching { Regex("-?\\d+").find(File(path).readText())?.value?.toLong() }.getOrNull() - /** Hz, kHz or MHz to MHz, as the files differ; a stopped clock reads 0. */ private fun mhz(raw: Long): Long? = when { raw < 0 -> null raw > 10_000_000 -> raw / 1_000_000 @@ -164,7 +134,6 @@ class GpuStatsComponent(val dir: File) : SessionPart() { else -> raw } - /** °C or m°C to MangoHud's m°C; nothing a GPU reads at is below 1 °C or above 1000. */ private fun milliCelsius(raw: Long): Long? = when { raw <= 0 -> null raw < 1000 -> raw * 1000 diff --git a/app/src/main/java/com/droiddeck/launcher/session/HwmonComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/HwmonComponent.kt index fb0048933..2cd84747d 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/HwmonComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/HwmonComponent.kt @@ -8,25 +8,6 @@ import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File import java.io.RandomAccessFile -/** - * The session's `/sys/class/hwmon`, as mangoapp (Deck mode's performance overlay) looks there. - * - * MangoHud finds a CPU's temperature by an hwmon sensor named `cpuN_thermal`, and an Adreno's - * temperature, fan and power by one whose name has `gpuss` in it - a mainline Arm kernel's names. - * An Android kernel has neither (its hwmon holds the PMIC's sensors, or nothing), and where the - * app may not list hwmon at all the overlay logged an error twice a second. So the overlay showed - * no CPU temperature and no fan. This is two sensors named so: - * - * - `hwmon0` (`cpu0_thermal`): `temp1_input` linked to a CPU thermal zone the app can read; - * - `hwmon1` (`gpuss0_thermal`): `temp1_input` linked to the GPU's zone, and `fan1_input` with - * the device's fan speed where it has a fan the app can read - an AYN handheld's tachometer - * (`/sys/class/gpio5_pwm2/speed`), or a `fan*_input` in the device's own hwmon; - * - `hwmon2` (`slg4ax46073v`): the same fan, for Valve's own Qualcomm reader (msm.cpp), which - * looks for its fan controller by that name and halves what it reads - that part reports twice - * the real speed - so this one holds twice it. - * - * Nothing in a phone's own hwmon is read by the overlay, so standing in for it loses nothing. - */ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { @Volatile private var running = false private var thread: Thread? = null @@ -36,7 +17,6 @@ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { private var lastRpm = 0L private var lastRpmAt = 0L - /** Writes the sensors; true when the directory should be bound over `/sys/class/hwmon`. */ fun prepare(): Boolean = try { dir.mkdirs() clear(dir) @@ -65,7 +45,6 @@ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { thread = Thread({ while (running) { try { Thread.sleep(FAN_PERIOD_MS) } catch (e: InterruptedException) { break } - // A tachometer read busy-waits 100 ms in the kernel: only while the overlay shows. if (running && OverlayShown.check(root)) writeFan(sampleFan()) } }, "hwmon-fan").apply { isDaemon = true; start() } @@ -77,11 +56,6 @@ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { thread = null } - /** - * The fan's speed in RPM. An AYN tachometer counts pulses over a short window per read and - * often catches none, reading 0 while the fan turns; a reading is held until a newer one - * comes or [FAN_HOLD_MS] passes with only zeros, when the fan is taken to have stopped. - */ private fun sampleFan(): Long { val now = SystemClock.elapsedRealtime() val rpm = fanSource?.let { readNumber(it) } ?: 0L @@ -94,7 +68,6 @@ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { return lastRpm } - /** In place and at one length, so a reader that keeps the file open sees each value whole. */ private fun writeFan(rpm: Long) { for ((file, value) in listOf(fanFile to rpm, valveFanFile to rpm * 2)) { try { @@ -112,7 +85,6 @@ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { Os.symlink(target, link.path) } - /** Only files and links are made here; deleting a link must not follow it into sysfs. */ private fun clear(root: File) { root.listFiles()?.forEach { f -> if (!java.nio.file.Files.isSymbolicLink(f.toPath()) && f.isDirectory) clear(f) @@ -122,7 +94,6 @@ class HwmonComponent(val dir: File, private val root: File) : SessionPart() { companion object { private const val TAG = "SessionService" - /** The AYN tachometer reads 0 if read again within a couple of seconds. */ private const val FAN_PERIOD_MS = 3_000L private const val FAN_HOLD_MS = 10_000L diff --git a/app/src/main/java/com/droiddeck/launcher/session/LoadingState.kt b/app/src/main/java/com/droiddeck/launcher/session/LoadingState.kt index 26799d003..a23f23beb 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/LoadingState.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/LoadingState.kt @@ -10,45 +10,32 @@ import java.io.File import java.io.RandomAccessFile import java.nio.charset.StandardCharsets -/** - * The loading overlay's state: what to say, read out of the session log, and a clock so a user - * can see that time is passing even when the log is quiet. The runtime script marks its - * milestones with "== STEP"; the client's own bootstrap does not, but it does print its download - * progress, and lifting that out is the difference between "starting the Steam client" for three - * minutes and a percentage that moves. - */ class LoadingState(context: Context, private val steam: Boolean = true) { var visible by mutableStateOf(true) var step by mutableStateOf(context.getString(com.droiddeck.launcher.R.string.loadstate_starting)) private set - /** What [step] is about, so the screen picks its checklist stage without reading the (translated) words. */ var topic by mutableStateOf(Topic.SESSION) private set - /** [step] is a download or install line worth showing as it is, not one of the script's own notes. */ var readable by mutableStateOf(false) private set var percent by mutableIntStateOf(-1) var elapsed by mutableStateOf("") var hint by mutableStateOf("") var ended by mutableStateOf(false) - /** The technical side of an ended session (exit status, log path), under the advice in [step]. */ var endedDetail by mutableStateOf<String?>(null) - // The desktop and the emulators get their own: the Steam ones talk about Steam. private val hints = context.resources.getStringArray( if (steam) com.droiddeck.launcher.R.array.loading_hints else com.droiddeck.launcher.R.array.loading_hints_desktop, ) private val res = context.resources private val startedAt = SystemClock.elapsedRealtime() - /** Once a second: the clock and the hint. */ fun tick() { val seconds = (SystemClock.elapsedRealtime() - startedAt) / 1000 elapsed = res.getString(com.droiddeck.launcher.R.string.loadstate_elapsed, seconds / 60, seconds % 60) hint = hints[((seconds / 8) % hints.size).toInt()] } - /** A line the app itself writes (an install's progress, the session starting). */ fun say(line: String, topic: Topic, readable: Boolean) { step = line this.topic = topic @@ -62,7 +49,6 @@ class LoadingState(context: Context, private val steam: Boolean = true) { endedDetail = detail } - /** Re-reads the end of the log and updates the line and the bar. */ fun update(context: Context, log: File?) { val state = read(context, log) ?: return step = state.line @@ -73,7 +59,6 @@ class LoadingState(context: Context, private val steam: Boolean = true) { private class Line(val line: String, val percent: Int, val topic: Topic, val readable: Boolean) - /** Only the tail is read: the client alone writes megabytes an hour. */ private fun read(context: Context, log: File?): Line? { if (log == null || !log.isFile) return null val text = try { @@ -132,14 +117,9 @@ class LoadingState(context: Context, private val steam: Boolean = true) { } } - /** The checklist stages a loading line can move the screen to. */ enum class Topic { RUNTIME, SESSION, DESKTOP, STEAM, STEAM_STARTING, OTHER } companion object { - /** - * A session script "== STEP" line's topic in a Steam ([steam]) or desktop session; those - * lines are English whatever the app's language. - */ internal fun topicOf(scriptStep: String, steam: Boolean): Topic { val t = scriptStep.lowercase() return when { @@ -152,7 +132,6 @@ class LoadingState(context: Context, private val steam: Boolean = true) { } } - /** The script's install lines are worth reading as they are; its other notes are not. */ internal fun readableScriptStep(scriptStep: String): Boolean { val t = scriptStep.lowercase() return t.startsWith("download") || t.startsWith("checking the linux") || t.startsWith("unpacking") || t.startsWith("installing") diff --git a/app/src/main/java/com/droiddeck/launcher/session/LogMigration.kt b/app/src/main/java/com/droiddeck/launcher/session/LogMigration.kt index c2b34f816..57964944d 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/LogMigration.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/LogMigration.kt @@ -6,13 +6,6 @@ import com.droiddeck.launcher.core.FileUtils import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File -/** - * Once, on the first start of a build with private logs: the session folders, tools/ and the loose - * tool logs that earlier builds left in `Download/DroidDeck/` move into `files/logs/`. Best effort: - * an item that cannot be moved stays where it is. Moved session folders lose their old scrub markers - * ([SessionArtifacts.unmarkMoved]), so [SessionArtifacts.scrubOlder] scrubs them under the current rules. Nothing else in that folder is touched - game - * save backups (Saves/) stay public - and after this pass the app never reads it again. - */ object LogMigration { private const val TAG = "LogMigration" private const val MARKER = ".moved-from-downloads" @@ -24,7 +17,6 @@ object LogMigration { if (marker.exists()) return val legacy = LinuxRuntime.legacyLogDir() val items = if (legacy.isDirectory) legacy.listFiles() else emptyArray() - // Unreadable (no storage access yet): try again next start rather than give up on them. if (items == null) { Log.w(TAG, "$legacy cannot be listed; logs there stay until it can"); return } var moved = 0 val movedFolders = ArrayList<File>() @@ -40,15 +32,11 @@ object LogMigration { if (SessionPaths.isSessionFolder(dest)) movedFolders.add(dest) } else Log.w(TAG, "could not move ${f.name}; left in place") } - // Earlier builds scrubbed these under older rules (or not at all) and marked them done: - // without their markers, the pass over older folders that follows at the same start puts - // them through the current redactor. SessionArtifacts.unmarkMoved(movedFolders) try { marker.writeText("moved $moved item(s)\n") } catch (e: Exception) { Log.w(TAG, "could not mark the move: ${e.message}") } if (moved > 0) Log.i(TAG, "moved $moved log item(s) from $legacy into $target") } - /** A rename where it works (same volume), else a copy then the source's removal; false = left as it was. */ private fun move(src: File, dest: File): Boolean { if (src.renameTo(dest)) return true return try { diff --git a/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt b/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt index 42a5a2866..c276da390 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt @@ -5,19 +5,6 @@ import android.util.Log import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File -/** - * Starting the Steam client signed out of Valve's servers. - * - * The client can run without a connection - installed games launch, Proton and the frame-gen are - * all local - but only if it has credentials from an earlier successful sign-in. Two keys in - * loginusers.vdf decide what it does when it cannot reach a CM: WantsOfflineMode makes it go - * offline on its own, SkipOfflineModeWarning stops it asking first. Both are read once, while the - * client starts, which is why this belongs to the main screen and not to the in-session drawer: - * changing it during a session would write the file and change nothing until the next launch. - * - * The client rewrites this file when it exits, so the request is kept as our own preference and - * written into the file again at every session start. - */ object OfflineMode { private const val TAG = "OfflineMode" private const val KEY = "steam_offline" @@ -30,7 +17,6 @@ object OfflineMode { private fun loginUsers(root: File) = File(root, "root/.local/share/Steam/config/loginusers.vdf") - /** The account the client could sign in as offline: its name, "" when it recorded none, null when there is none. */ fun account(context: Context): String? { val file = loginUsers(LinuxRuntime.rootDir(context)) val text = try { @@ -49,12 +35,6 @@ object OfflineMode { apply(context, LinuxRuntime.rootDir(context)) } - /** - * Write the request into loginusers.vdf. Every account block gets the same answer: the client - * signs in as one of them and we have no say in which. Keys the file does not carry are added - * beside the account's name; a file without an account block is left alone, since there is - * nothing to be offline as. - */ fun apply(context: Context, root: File) { val file = loginUsers(root) if (!file.isFile) return diff --git a/app/src/main/java/com/droiddeck/launcher/session/OrphanReaper.kt b/app/src/main/java/com/droiddeck/launcher/session/OrphanReaper.kt index 9a17a97ae..35ac169a7 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/OrphanReaper.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/OrphanReaper.kt @@ -3,26 +3,10 @@ package com.droiddeck.launcher.session import android.util.Log import java.io.File -/** - * Kills every process of this app's uid other than the app itself. - * - * A session is a tree of processes under proot, and proot dies with the app only when the app's - * own teardown runs. When the app is killed or crashes instead, the tree stays: proot holding the - * rootfs, gamescope holding the GPU, Steam holding its lock file - and the next session never - * comes up, or comes up beside a client that is already running. A user on a Thor Pro had to - * force-stop the app before every launch. Everything of ours that should be alive between - * sessions is inside the app process, so at session start anything else under our uid is a - * leftover, and this sweeps it. Each pid is checked by uid at kill time, never by name. - * - * The one exception is work the app started outside any session and is still waiting on - a - * Flatpak install from the store - registered with [keep]: that process and everything under it - * are left to finish. - */ object OrphanReaper { private const val TAG = "OrphanReaper" private val kept = java.util.concurrent.ConcurrentHashMap.newKeySet<Int>() - /** Spares [pid] and its descendants until [release]. */ fun keep(pid: Int) { if (pid > 0) kept.add(pid) } fun release(pid: Int) { kept.remove(pid) } @@ -49,14 +33,12 @@ object OrphanReaper { return killed } - /** [roots] and every process below them, by each process's parent in /proc/<pid>/stat. */ private fun descendants(roots: Set<Int>): Set<Int> { if (roots.isEmpty()) return emptySet() val parent = HashMap<Int, Int>() File("/proc").listFiles()?.forEach { entry -> val pid = entry.name.toIntOrNull() ?: return@forEach val stat = try { File(entry, "stat").readText() } catch (e: Exception) { return@forEach } - // "pid (comm) state ppid ...": comm may hold spaces and parentheses; the last ')' ends it. stat.substringAfterLast(") ").split(' ').getOrNull(1)?.toIntOrNull()?.let { parent[pid] = it } } val out = HashSet(roots) diff --git a/app/src/main/java/com/droiddeck/launcher/session/OverlayShown.kt b/app/src/main/java/com/droiddeck/launcher/session/OverlayShown.kt index 028091e44..22f067d54 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/OverlayShown.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/OverlayShown.kt @@ -2,12 +2,6 @@ package com.droiddeck.launcher.session import java.io.File -/** - * Whether Deck mode's performance overlay is on screen. The client sets its level by rewriting - * mangoapp's config (droiddeck-session makes it, `/tmp/mangohud.*`), and `no_display` is the - * level that hides it. The feeds whose reads cost something - the fan's tachometer busy-waits in - * the kernel, GPU memory walks every process's mappings - only run while it shows. - */ object OverlayShown { fun check(root: File): Boolean { val config = File(root, "tmp").listFiles { f -> f.name.startsWith("mangohud.") } diff --git a/app/src/main/java/com/droiddeck/launcher/session/PerfHud.kt b/app/src/main/java/com/droiddeck/launcher/session/PerfHud.kt index b3c3fd6ea..042396b9f 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/PerfHud.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/PerfHud.kt @@ -16,17 +16,7 @@ import java.io.File import java.util.Locale import java.util.concurrent.atomic.AtomicInteger -/** - * The numbers behind the top-right line: the game's own frame rate and, when frame generation - * is on, what the screen is actually being shown - "60 → 118 fps" is the proof that the engine - * is doing something, and "FG starting" or a reason is the proof that it is not. - * - * The base rate is the game window's own frames, counted here from the compositor's per-frame - * callback with or without an engine - what Steam's overlay counts too. The presented rate comes - * from the engine's telemetry while it generates. - */ class PerfHud(context: Context) { - /** Empty when the HUD is off or not yet started. */ var text by mutableStateOf("") private set @@ -36,19 +26,15 @@ class PerfHud(context: Context) { private var running = false private val handler = Handler(Looper.getMainLooper()) - /** The session's own switch (drawer), with the Downloads file as a device-side override. */ private val enabled: Boolean get() = SessionPrefs.hudEnabled(context) && !File(Environment.getExternalStorageDirectory(), "Download/droiddeck-no-hud").exists() - /** Told, on the main thread, when the window presenting frames changes. */ var onPresentingWindowChanged: (() -> Unit)? = null private var lastWindow: String? = null private val listener = object : WaylandCompositor.GameListener { override fun onGameSurface(window: String?, gpuName: String?) { - // gamescope forwards a fullscreen game's own buffers straight through and its composited - // output when the Steam menu is up; each is a different window here. Compositor thread. if (window != null && window != lastWindow) { lastWindow = window handler.post { onPresentingWindowChanged?.invoke() } @@ -68,7 +54,6 @@ class PerfHud(context: Context) { } } - /** Re-reads the switch: starts or stops to match it. */ fun refresh() { if (enabled) start() else stop() } diff --git a/app/src/main/java/com/droiddeck/launcher/session/PerfMode.kt b/app/src/main/java/com/droiddeck/launcher/session/PerfMode.kt index 4a77141c3..f197800a4 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/PerfMode.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/PerfMode.kt @@ -8,33 +8,12 @@ import android.os.Build import android.provider.Settings import android.util.Log -/** - * What a session tells Android about itself so the SoC is run for a game, not for an app. - * - * The manifest already makes the app a game (appCategory + game_mode_config); this is the - * per-window half, applied when the session activity is created: - * - * - NOT sustained performance mode: on Pixel and Qualcomm power HALs it caps the CPU and GPU at - * a level the device can hold indefinitely rather than holding a floor, which cost games and - * Big Picture their peak clocks. WinNative never asks for it; the thermal budget is left to - * the game; - * - the panel's fastest mode at its current size (Android 6+), up to the user's peak refresh rate - * ([peakRefreshCap]): a 120 Hz phone is switched to 120 Hz for the menu, not only when a game - * votes for it. With a frame cap, the fastest mode - * the cap divides evenly, so every capped frame is shown for the same number of refreshes; - * - GameManager's game state (Android 13+): "in gameplay", so an OEM framework that releases - * its boost on loading screens or menus keeps it up. - * - * Every step is version-guarded and skipped silently below its API; the returned line is what - * the session log shows, which is the first thing to read on a device that feels slow. - */ object PerfMode { private const val TAG = "PerfMode" fun apply(a: Activity, fpsLimit: Int = 0): String { val parts = ArrayList<String>(2) - // The fastest display mode at the panel's current size. parts += try { val display = if (Build.VERSION.SDK_INT >= 30) a.display else @Suppress("DEPRECATION") a.windowManager.defaultDisplay if (display == null || Build.VERSION.SDK_INT < 23) "display mode unchanged" else { @@ -54,7 +33,6 @@ object PerfMode { } } catch (t: Throwable) { Log.w(TAG, "display mode", t); "display mode failed" } - // GameManager: what mode the OS put us in, and that we are playing. parts += if (Build.VERSION.SDK_INT >= 31) { val gm = a.getSystemService(GameManager::class.java) if (gm == null) "no GameManager" else { @@ -74,13 +52,6 @@ object PerfMode { return parts.joinToString(" · ") } - /** - * The highest rate Android will run the panel at: the user's "peak refresh rate" (Smooth display, - * or the AYN/Odin quick-settings tile). Above it a mode can be requested and is even reported back - * as active, while SurfaceFlinger keeps composing at the cap. The Thor set to 60 Hz ran 120 Hz - * sessions at 60: gamescope and Steam were told 120, every game topped out at 60, and the QAM's - * Frame Limit read "60 FPS (120 Hz)" and changed nothing. Unset (most phones) is no cap. - */ fun peakRefreshCap(context: Context): Float { val peak = try { Settings.System.getFloat(context.contentResolver, "peak_refresh_rate") @@ -88,7 +59,6 @@ object PerfMode { return if (peak >= 1f) peak else Float.POSITIVE_INFINITY } - /** A panel rate that shows every capped frame for a whole number of refreshes (no cap: any). */ fun cadenceFits(hz: Float, fpsLimit: Int): Boolean { if (fpsLimit <= 0) return true val ratio = hz / fpsLimit diff --git a/app/src/main/java/com/droiddeck/launcher/session/PipPolicy.kt b/app/src/main/java/com/droiddeck/launcher/session/PipPolicy.kt index 5626e35e2..d2d4d49bd 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/PipPolicy.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/PipPolicy.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.session -/** Platform-independent eligibility; a service existing alone does not mean it has a picture. */ object PipPolicy { fun eligible(running: Boolean, frameSeen: Boolean, phase: SessionPhase, suspended: Boolean, automatic: Boolean): Boolean = running && frameSeen && phase in setOf(SessionPhase.READY, SessionPhase.SUSPENDED) && diff --git a/app/src/main/java/com/droiddeck/launcher/session/ProtonExtras.kt b/app/src/main/java/com/droiddeck/launcher/session/ProtonExtras.kt index c51cf01f4..8f51fee77 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/ProtonExtras.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/ProtonExtras.kt @@ -12,12 +12,10 @@ import java.io.File import org.json.JSONArray import org.json.JSONObject -/** Immediate download and installation of the optional ARM64 Proton builds. */ object ProtonExtras { private const val TAG = "ProtonExtras" private const val NEED_BYTES = 4L * 1024 * 1024 * 1024 private const val RELEASES = "https://api.github.com/repos/%s/releases?per_page=15" - /** How long a release check stands before the next one goes to GitHub. */ const val CHECK_EVERY_MS = 6L * 60 * 60 * 1000 @Volatile var installInProgress = false @@ -25,7 +23,6 @@ object ProtonExtras { class Tool(val id: String, val name: String, val prefix: String, val repo: String, val assetPattern: Regex) - /** [sha256] from GitHub's asset digest; [sha512] the URL of a checksum file published beside it. */ internal data class Asset(val tag: String, val name: String, val url: String, val sha512: String?, val size: Long, val sha256: String? = null) @@ -38,15 +35,11 @@ object ProtonExtras { private fun home(context: Context) = File(LinuxRuntime.rootDir(context), "root") private fun requests(context: Context) = File(home(context), ".bl-proton-extra") private fun toolsDir(context: Context) = File(home(context), ".local/share/Steam/compatibilitytools.d") - /** The release tag each build was installed from, by directory name. */ private fun releasesFile(context: Context) = File(context.filesDir, "proton-extras.json") - /** The newest release seen per source, and when it was looked up. */ private fun latestFile(context: Context) = File(context.filesDir, "proton-latest.json") - /** The installed build's directory name (e.g. GE-Proton11-7), the newest when there are several, or null. */ fun installed(context: Context, tool: Tool): String? = installedIn(toolsDir(context), tool).lastOrNull() - /** Every installed build's directory name, oldest first. */ fun installedAll(context: Context, tool: Tool): List<String> = installedIn(toolsDir(context), tool) internal fun installedIn(dir: File, tool: Tool): List<String> = @@ -56,15 +49,12 @@ object ProtonExtras { ?.sortedWith { a, b -> compareVersions(a, b) } ?: emptyList() - // Architecture names carry digits (aarch64, x86_64) that are not part of a version. private val ARCH = Regex("(?i)aarch64|arm64|x86_64|x86-64|amd64") private val NUMBER = Regex("\\d+") - /** The numbers in a release tag or build folder, in order: GE-Proton11-10 is 11, 10. */ internal fun versionKey(name: String): List<Long> = NUMBER.findAll(ARCH.replace(name, " ")).mapNotNull { it.value.toLongOrNull() }.toList() - /** Orders by those numbers, so 11-10 is after 11-9; the name itself only breaks a tie. */ internal fun compareVersions(a: String, b: String): Int { val x = versionKey(a) val y = versionKey(b) @@ -72,7 +62,6 @@ object ProtonExtras { return if (x.size != y.size) x.size.compareTo(y.size) else a.compareTo(b) } - /** [latest] when it is newer than every installed release, else null. Nothing installed is not an update. */ internal fun updateFor(installed: List<String>, latest: String?): String? { if (latest == null || installed.isEmpty()) return null val newest = installed.maxWithOrNull { a, b -> compareVersions(a, b) } ?: return null @@ -82,28 +71,20 @@ object ProtonExtras { return if (x.size > y.size) latest else null } - /** The release each installed build came from: the tag recorded at install, else its folder name. */ fun installedTags(context: Context, tool: Tool): List<String> { val recorded = readJson(releasesFile(context)) return installedAll(context, tool).map { recorded.optString(it).ifEmpty { it } } } - /** A newer release than the installed one, from the last check (no network), or null. */ fun update(context: Context, tool: Tool): String? = updateFor(installedTags(context, tool), cachedLatest(context, tool)) fun cachedLatest(context: Context, tool: Tool): String? = readJson(latestFile(context)).optJSONObject(tool.id)?.optString("tag")?.ifEmpty { null } - /** Its own lock, not the install's: a check must not wait behind a download. */ private val checkLock = Any() - /** A failed look-up (offline) is not repeated on every resume, only after this long. */ private const val RETRY_MS = 15L * 60 * 1000 private var lastAttempt = 0L - /** - * Looks up each source's newest release unless it was looked up within [CHECK_EVERY_MS] (or - * [force]). Offline or rate limited, the last answer stands. True when anything was asked. - */ fun checkLatest(context: Context, force: Boolean = false, now: Long = System.currentTimeMillis()): Boolean { synchronized(checkLock) { val file = latestFile(context) @@ -143,7 +124,6 @@ object ProtonExtras { private fun readJson(file: File): JSONObject = runCatching { JSONObject(FileUtils.readString(file) ?: "{}") }.getOrDefault(JSONObject()) - /** A request left by an earlier app version, still consumed by the session shim. */ fun queued(context: Context, tool: Tool): Boolean = requestLines(context).any { it.trim() == tool.id || it.trim().startsWith(tool.id + " ") } @@ -154,18 +134,15 @@ object ProtonExtras { else file.writeText(lines.joinToString("\n") + "\n") } - /** Deletes one installed build ([dir], else the newest); Steam discovers its absence on its next start. */ fun remove(context: Context, tool: Tool, dir: String? = null) { val name = if (dir != null) dir.takeIf { it in installedAll(context, tool) } ?: return else installed(context, tool) ?: return File(toolsDir(context), name).deleteRecursively() } - /** Deletes every build but the newest. */ fun removeOlder(context: Context, tool: Tool) { installedAll(context, tool).dropLast(1).forEach { File(toolsDir(context), it).deleteRecursively() } } - /** Downloads, verifies and registers the latest build without waiting for another session. */ @Synchronized fun install(context: Context, tool: Tool, onProgress: (String, Int) -> Unit): String? { if (installInProgress) return context.getString(R.string.pextra_busy) @@ -211,8 +188,6 @@ object ProtonExtras { return context.getString(R.string.pextra_incomplete) } - // One checksum has to be found and has to match: GitHub's sha256 digest, else the - // release's own sha512 file. A download neither vouches for is not installed. onProgress(context.getString(R.string.pextra_verifying), -1) val verified = when { asset.sha256 != null -> asset.sha256.equals(Hashes.sha256(archive), ignoreCase = true) @@ -257,7 +232,6 @@ object ProtonExtras { child.inputStream.bufferedReader().useLines { lines -> lines.forEach { line -> Log.i(TAG, line) - // "droiddeck-proton-extra: installed <folder>", the folder as the tarball names it. line.substringAfter("droiddeck-proton-extra: installed ", "").takeIf { it.isNotEmpty() }?.let { placed = it } if (line.contains("unpacking", ignoreCase = true) || line.contains("adopted", ignoreCase = true)) { onProgress(installing, -1) @@ -296,7 +270,6 @@ object ProtonExtras { } } - /** The newest full release in GitHub's list (newest first) that carries [tool]'s ARM64 archive. */ internal fun pickAsset(tool: Tool, array: JSONArray): Asset? { for (i in 0 until array.length()) { val release = array.getJSONObject(i) diff --git a/app/src/main/java/com/droiddeck/launcher/session/QuestExperimental.kt b/app/src/main/java/com/droiddeck/launcher/session/QuestExperimental.kt index 089f0631c..f12550419 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/QuestExperimental.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/QuestExperimental.kt @@ -6,7 +6,6 @@ import android.util.Log import com.droiddeck.launcher.BuildConfig import java.io.File -/** Device-detected defaults and launch evidence for experimental Quest compatibility. */ object QuestExperimental { private const val TAG = "QuestExperimental" const val DRIVER = "turnip25.1.0-quest1" @@ -30,14 +29,10 @@ object QuestExperimental { val prefs = context.getSharedPreferences("session", Context.MODE_PRIVATE) val defaultsKey = if (isQuest2()) "quest2DefaultsV1" else "questDefaultsV1" if (!prefs.getBoolean(defaultsKey, false)) { - // Auto would download an unwrapped display driver and lose the compatibility path. - // Apply once so deliberate changes made during headset testing remain possible. prefs.edit() .putString("gpuDriverMode", SessionPrefs.GPU_DRIVERS_MANUAL) .putString("androidDriver", if (isQuest2()) DRIVER_A6XX else DRIVER) .putString("linuxDriver", "") - // The existing HDR option uses SurfaceControl presentation. This is a - // corruption workaround from upstream issue #245, not a claim of HDR accuracy. .putBoolean("hdr.steam", true) .putBoolean(defaultsKey, true) .apply() diff --git a/app/src/main/java/com/droiddeck/launcher/session/RivalClients.kt b/app/src/main/java/com/droiddeck/launcher/session/RivalClients.kt index 75973e467..0d7bd22cc 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/RivalClients.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/RivalClients.kt @@ -4,25 +4,11 @@ import android.app.ActivityManager import android.content.Context import android.util.Log -/** - * Other apps on the device that sign into the user's Steam account with their own client. Steam - * allows one session per account: the moment one of them signs in, ours is signed out - which - * shows up as "Session Replaced" in the client's log 2-3 seconds after every login, and looks - * like a bug in this app. - * - * Installed is the most a normal app can know (since Android 7 `getRunningAppProcesses()` returns - * only the caller's own), so the list is checked for presence and then asked to stop. - * `killBackgroundProcesses` is a normal permission: it ends an app's background processes and - * leaves anything in the foreground alone - and background is exactly the case that keeps - * happening, because GameHub declares boot receivers and is running from the moment the phone - * starts without ever having been opened. Telling someone to close an app they never opened is - * not much help. Ported from Bannerlator's Linux session (GPL-3.0). - */ object RivalClients { private const val TAG = "RivalClients" private val PACKAGES = arrayOf( - "com.xiaoji.egggame", // GameHub + "com.xiaoji.egggame", ) fun installed(context: Context): List<String> = @@ -32,10 +18,6 @@ object RivalClients { fun name(pkg: String): String = if (pkg == "com.xiaoji.egggame") "GameHub" else pkg - /** - * Stop the installed rivals' background processes before the client starts. A rival in the - * foreground survives this. The activity service call and its log messages are unchanged. - */ fun stopBeforeSession(context: Context) { val rivals = installed(context) if (rivals.isEmpty()) return diff --git a/app/src/main/java/com/droiddeck/launcher/session/RumbleComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/RumbleComponent.kt index 4adf1c0a5..8c69891d8 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/RumbleComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/RumbleComponent.kt @@ -17,26 +17,11 @@ import com.droiddeck.launcher.input.PadBridge import java.io.DataInputStream import kotlin.math.max -/** - * Rumble for the pad, on the motors in the player's hands. - * - * When a game plays a force-feedback effect on the fake pad (or Steam Input sends the Deck's - * rumble report), the fake evdev layer inside the guest connects to the abstract socket [NAME] - * and sends one packet: strong, weak, duration in ms and the pad slot, four little-endian 16-bit - * values. This listens for those and plays the effect on the physical controller playing in that - * slot (PadBridge.controllerForSlot) when Android exposes its motors - strong and weak separately - * where it has two - and otherwise on the device's own vibrator, with the stronger motor's - * strength: the on-screen pad, or a controller without rumble. Best effort both ways: a missing - * listener costs the guest nothing, and a bad packet is dropped. - */ class RumbleComponent : SessionPart() { @Volatile private var server: LocalServerSocket? = null private var phone: Motors? = null - /** Per slot, where its last effect went, so the next one (or a stop) can end it there. */ private val playing = HashMap<Int, Motors>() - /** Per slot, the last effect handed to the motors and when, to drop the repeats games stream. */ private val sent = HashMap<Int, Sent>() - /** Each controller's motors, looked up once; null when it has none. */ private val controllers = HashMap<Int, Motors?>() private var preferences: SharedPreferences? = null private var enabled = false @@ -44,7 +29,6 @@ class RumbleComponent : SessionPart() { if (key == null || key == "rumble") refreshEnabled() } - /** The motors of the controller with this input device id, or null when it has none. Tests swap it. */ internal var controllerMotors: (Int) -> Motors? = ::lookupController @Synchronized private fun refreshEnabled() { @@ -69,9 +53,8 @@ class RumbleComponent : SessionPart() { DataInputStream(client.inputStream).readFully(bytes) buzz(u16(bytes, 0), u16(bytes, 2), u16(bytes, 4), u16(bytes, 6)) } catch (e: Exception) { - // A partial packet or a closed peer: nothing to play. } finally { - try { client.close() } catch (e: Exception) { /* already gone */ } + try { client.close() } catch (e: Exception) { } } } }, "rumble").apply { isDaemon = true; start() } @@ -87,7 +70,7 @@ class RumbleComponent : SessionPart() { controllers.clear() val s = server server = null - try { s?.close() } catch (e: Exception) { /* the accept loop ends on the next wake */ } + try { s?.close() } catch (e: Exception) { } } private fun u16(b: ByteArray, at: Int): Int = (b[at].toInt() and 0xFF) or ((b[at + 1].toInt() and 0xFF) shl 8) @@ -104,8 +87,6 @@ class RumbleComponent : SessionPart() { } if (target == null) return if ((strong == 0 && weak == 0) || ms == 0) { target.cancel(); sent.remove(slot); return } - // Games re-send their effect every frame; restarting the motor that often floods the haptics - // driver. ponytail: a change inside MIN_GAP_MS is dropped, the game's next packet brings it. val now = SystemClock.uptimeMillis() val last = sent[slot] if (last != null && (now - last.at < MIN_GAP_MS || (last.strong == strong && last.weak == weak && now < last.until))) return @@ -118,7 +99,6 @@ class RumbleComponent : SessionPart() { } } - /** The motors of the controller playing in this slot when it has any, else the phone's. */ private fun target(slot: Int): Motors? { val id = PadBridge.controllerForSlot(slot) if (id == PadBridge.NO_CONTROLLER) return phone @@ -134,7 +114,6 @@ class RumbleComponent : SessionPart() { private data class Sent(val strong: Int, val weak: Int, val at: Long, val until: Long) - /** One place an effect can play. Strengths are the evdev 0..65535 magnitudes. */ internal interface Motors { val name: String fun play(strong: Int, weak: Int, ms: Long) @@ -147,7 +126,6 @@ class RumbleComponent : SessionPart() { override fun cancel() = vibrator.cancel() } - /** Android 12+: each of the pad's motors on its own, the lower id being the strong one. */ @RequiresApi(31) private class PadMotors(override val name: String, private val manager: VibratorManager, private val ids: IntArray) : Motors { override fun play(strong: Int, weak: Int, ms: Long) { @@ -164,7 +142,6 @@ class RumbleComponent : SessionPart() { override fun cancel() = manager.cancel() } - /** Before Android 12 a pad has one vibrator. */ private class LegacyPadMotors(override val name: String, private val vibrator: Vibrator) : Motors { override fun play(strong: Int, weak: Int, ms: Long) = vibrator.vibrate(oneShot(vibrator, max(strong, weak), ms)) override fun cancel() = vibrator.cancel() @@ -172,9 +149,7 @@ class RumbleComponent : SessionPart() { companion object { private const val TAG = "SessionService" - /** Must match the fake evdev layer (fakeinput_steam.cpp). */ const val NAME = "droiddeck-rumble" - /** The shortest time between two effects started on one slot's motors. */ const val MIN_GAP_MS = 50L private fun oneShot(vibrator: Vibrator, strength: Int, ms: Long): VibrationEffect { diff --git a/app/src/main/java/com/droiddeck/launcher/session/SecondaryLibrary.kt b/app/src/main/java/com/droiddeck/launcher/session/SecondaryLibrary.kt index 10692167d..24c15df82 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SecondaryLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SecondaryLibrary.kt @@ -7,7 +7,6 @@ import java.nio.file.StandardCopyOption.COPY_ATTRIBUTES import java.security.MessageDigest import java.util.Locale -/** Android shared storage cannot create the links in Wine prefixes or native Steam depots. */ object SecondaryLibrary { private val bootstrapTools = linkedMapOf( "4185400" to "SteamLinuxRuntime_4-arm64", @@ -25,12 +24,10 @@ object SecondaryLibrary { val CARD_GUESTS = listOf("/mnt/droiddeck-sd", "/mnt/bannerlator-sd") - /** Parent binds come first; the more specific private directories override them in PRoot. */ fun binds(files: File, library: File, guests: List<String> = CARD_GUESTS): List<String> { val private = privateRoot(files, library) val apps = File(library, "steamapps") val tools = LinkedHashMap(bootstrapTools) - // Include other installed Proton/runtime versions, without routing ordinary game depots. apps.listFiles()?.filter { it.name.matches(Regex("appmanifest_\\d+\\.acf")) }?.forEach { manifest -> val text = manifest.readText() val id = Regex("\"appid\"\\s+\"(\\d+)\"").find(text)?.groupValues?.get(1) @@ -48,13 +45,8 @@ object SecondaryLibrary { for (relative in listOf("steamapps/common/$dir", "steamapps/downloading/$id")) { val target = File(private, relative) migrate(File(library, relative), target) - // FUSE discarded the executable bits. Steam will restore the depot's exact modes - // on verification; its launchers must already be executable when importing it. target.walkTopDown().filter { it.isFile }.forEach { it.setExecutable(true, true) } overrides.add(target to relative) - // Steam normalizes depot install directories to lowercase on Android's - // case-insensitive shared storage. PRoot binds are case-sensitive, so both - // spellings must reach the same private directory during the commit. val lower = relative.lowercase(Locale.ROOT) if (lower != relative) overrides.add(target to lower) } @@ -64,7 +56,6 @@ object SecondaryLibrary { } } - /** Copy once into a staging directory; retain the source and never merge over a live prefix. */ private fun migrate(source: File, target: File) { val migrated = File(target.parentFile, ".${target.name}.migrated") if (target.isDirectory) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/SelectedGameProfile.kt b/app/src/main/java/com/droiddeck/launcher/session/SelectedGameProfile.kt index aef506491..41eeac38f 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SelectedGameProfile.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SelectedGameProfile.kt @@ -16,7 +16,6 @@ data class SelectedGameProfile( val componentFiles: Map<String, String>, val environmentOverrides: Int, val windowsComponentNames: List<String>, - /** Start without Steam (GameEnvironment.NO_STEAM) is on for this game. */ val startWithoutSteam: Boolean = false, ) { val hasOverrides: Boolean @@ -53,7 +52,6 @@ data class SelectedGameProfile( inherited = override == null, ) } - // Start without Steam is a switch of its own on the Games page, listed by name. val otherEnvironment = environment.games[game.profileKey].orEmpty().keys - FexPreset.environmentKeys - GameEnvironment.NO_STEAM return SelectedGameProfile( diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionAchievementGame.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionAchievementGame.kt index 62991bd8e..d86c88df4 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionAchievementGame.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionAchievementGame.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.session -/** An active game needs a live window; a launch URL or stale Steam base layer is insufficient. */ internal object SessionAchievementGame { fun activeAppId(focused: Long?, windows: List<Long>, installed: Set<Long>): Long? { if (focused != null && focused > 0 && focused != 769L) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt index cc0f5ad82..9249871d5 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionArtifacts.kt @@ -11,44 +11,16 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * Finishes a session's folder: the compositor's log, the Steam client's logs scrubbed line by - * line, Android's crash buffer, and a marker that says the folder is complete. - * - * Three callers, because a session ends three ways. [collect] runs at an ordinary stop. The - * app's uncaught-exception handler ([CrashHandler]) runs it as the process dies, so a crash in - * our own code leaves a full folder and not a half one. And [finishAbandoned] runs at the next - * app start for a folder that has no marker: the process was killed outright (Android's phantom - * process killer, a native crash in the compositor, the battery) and nothing of ours got to run. - * Everything written *during* the session - session.log, app.log, wayland.log, audio.log, the - * device report - is already on disk at that point; only the pieces gathered at the end were - * missing, and the crash buffer keeps its entries after the process is gone, which is the whole - * reason it is worth coming back for. - */ object SessionArtifacts { private const val TAG = "SessionArtifacts" - /** Written last; a folder without it did not get its ending. */ const val COMPLETE_MARKER = ".complete" - /** - * Every file in the folder has been through the redactor under its current rules. Named after - * [LogRedactor.RULES_VERSION], so a folder scrubbed under older rules is scrubbed again. - */ private val SCRUBBED_MARKER = ".scrubbed-r${LogRedactor.RULES_VERSION}" private val OLD_SCRUBBED_MARKER = Regex("""\.scrubbed-(r?\d+)""") - /** - * Written by a session's own ending, and only when every file in the folder, subfolders - * included (whatever the session script copied into steam/ and droiddeck-esync/ as well as what - * the app wrote), went through the redactor without a failure. It lists each of those files - * with its size and modification time, under the redactor's rules version ([scrubbedFiles]). - * A share no longer relies on it: every text file goes through the share pass on the way into - * the zip regardless. - */ const val SCRUBBED_TREE_MARKER = ".scrubbed-tree" - /** Everything the end of a session gathers, into [dir]. Safe to call for a dead session. */ fun collect(context: Context, dir: File, reason: String) { try { val wayland = File(dir, "wayland.log") @@ -60,7 +32,6 @@ object SessionArtifacts { LogRedactor.learnFromRuntime(LinuxRuntime.rootDir(context)) val record = Record(dir) copySteamLogs(context, dir, record) - // A session the system killed leaves its trace here and nowhere else. SessionLogCapture.dumpCrashBuffer(File(dir, "crash.log")) markScrubbed(scrubTree(record)) SessionEvents.record("session.artifacts_collected", mapOf("reason" to reason), dir) @@ -70,17 +41,6 @@ object SessionArtifacts { } } - /** - * Every session folder not yet scrubbed under the redactor's current rules - written before - * every file was scrubbed, before the device's own addresses or the Steam account were, or - * moved in from Download/ ([LogMigration] strips their markers first, [unmarkMoved]): the - * whole folder, steam/ and every other subfolder included, through the redactor, and the - * current marker written in place of the old ones. Runs at app start with [finishAbandoned]. - * - * A folder holds some 30 MB of Steam logs, about half a minute's work, so folders go through - * a few at a time; one the process does not get to keeps its old marker and is taken up at - * the next start. - */ @Synchronized fun scrubOlder(context: Context) { val current = SessionPaths.current() @@ -104,27 +64,14 @@ object SessionArtifacts { if (failed > 0) ", $failed not fully (again at the next start)" else "") } - /** - * Folders [LogMigration] just moved in lose every scrub marker an earlier build left, so - * [scrubOlder], which runs right after it, puts them through the current rules whatever those - * markers claimed. - */ fun unmarkMoved(dirs: List<File>) { dirs.filter { SessionPaths.isSessionFolder(it) }.forEach { dir -> dir.listFiles { f -> OLD_SCRUBBED_MARKER.matches(f.name) || f.name == SCRUBBED_TREE_MARKER }?.forEach { it.delete() } } } - /** - * The session's own files (session.log, app.log, desktop.log, the Steam desktop client's - * steam-desktop.log, ...) were written as they happened, unscrubbed; this pass puts every one - * through the redactor before the folder can be shared. A file is rewritten only if a line - * changed. steam/ was scrubbed on the way in. - */ - /** A file's size and modification time as they stood right after it was scrubbed. */ private class Scrubbed(val length: Long, val modified: Long) - /** Files of a folder as they stood right after scrubbing, filled by [scrubTree] and [copySteamLogs]. */ private class Record(val dir: File) { val files = HashMap<File, Scrubbed>() var failed = false @@ -138,12 +85,6 @@ object SessionArtifacts { } } - /** - * Puts [f] through the redactor in place. Returns its size and modification time as it then - * stands, or null if it could not, and [f] may still hold the original. A writer still - * appending to [f] (the app's own log capture) changes its size afterwards, and a share then - * scrubs it again. - */ private fun scrubFile(dir: File, f: File): Scrubbed? { val tmp = File(dir, ".${f.name}.scrub") return try { @@ -152,11 +93,8 @@ object SessionArtifacts { val scrubbed = tmp.readBytes() if (scrubbed.contentEquals(f.readBytes())) { tmp.delete() - // Unchanged by the redactor, and by anything else while it read. before.takeIf { f.length() == it.length && f.lastModified() == it.modified } } else { - // Shared storage can refuse a rename; then the scrubbed bytes are written over the - // original instead, so it never stays behind unscrubbed. if (!tmp.renameTo(f)) { f.writeBytes(scrubbed); tmp.delete() } Scrubbed(scrubbed.size.toLong(), f.lastModified()).takeIf { f.length() == it.length } } @@ -167,14 +105,9 @@ object SessionArtifacts { } } - /** - * Records [record]'s folder as scrubbed - every file went through without a failure - or - * leaves it unmarked, so the next share and the pass over older folders scrub it again. - */ private fun markScrubbed(record: Record): Boolean { val dir = record.dir val tree = File(dir, SCRUBBED_TREE_MARKER) - // A marker from older rules vouches for nothing now, whether or not this pass succeeded. dir.listFiles { f -> OLD_SCRUBBED_MARKER.matches(f.name) }?.forEach { it.delete() } if (record.failed) { tree.delete() @@ -197,11 +130,6 @@ object SessionArtifacts { } } - /** - * The files of [dir] that its own ending scrubbed and that are unchanged since (same size and - * modification time), by path relative to [dir]. Empty when the folder has no such record, or - * one made under other redactor rules. - */ fun scrubbedFiles(dir: File): Set<String> { val lines = try { File(dir, SCRUBBED_TREE_MARKER).takeIf { it.isFile }?.readLines() } catch (e: Exception) { null } ?: return emptySet() @@ -214,21 +142,13 @@ object SessionArtifacts { }.toSet() } - /** The end-of-session scrub of [dir] on its own (steam/ already in place); also the pass over older and moved folders. */ internal fun scrubAndMark(dir: File): Boolean = markScrubbed(scrubTree(Record(dir))) - /** - * [record]'s folder and every folder under it (steam/, droiddeck-esync/): the session script - * copied files there verbatim, and a Steam log the app did not copy over again (gone from the - * runtime, or past its size limit) stayed as the script left it. Files [copySteamLogs] already - * redacted on the way in are in [record] and left alone. - */ private fun scrubTree(record: Record): Record { record.dir.walkTopDown().filter { it.isDirectory }.forEach { scrubFolder(it, record) } return record } - /** Steam's logs: redacted into steam/, never copied verbatim. What it wrote goes in [record]. */ private fun copySteamLogs(context: Context, dir: File, record: Record) { val logs = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/logs") if (!logs.isDirectory) return @@ -247,12 +167,6 @@ object SessionArtifacts { Log.i(TAG, "collected ${out.listFiles()?.size ?: 0} Steam log(s), scrubbed, into $out") } - /** - * Every session folder without a marker gets its ending now. Only the newest of them gets the - * Steam logs - the runtime holds one set, and it belongs to the last session that ran; an - * older folder would be handed logs that are not its own. Runs on a worker thread at app - * start; nothing here touches the session that is about to begin. - */ @Synchronized fun finishAbandoned(context: Context) { val parent = LinuxRuntime.logDir(context) @@ -289,12 +203,6 @@ object SessionArtifacts { } } - /** - * Keeps the newest [SessionPaths.KEEP_SESSIONS] session folders and deletes the rest: a few - * days of testing left hundreds, and the one a report needed was lost among them. Only folders - * that are finished; the session in progress is never touched. Runs at app start after - * [finishAbandoned], on its worker thread. - */ @Synchronized fun prune(context: Context) { val current = SessionPaths.current() @@ -304,11 +212,6 @@ object SessionArtifacts { if (old.isNotEmpty()) Log.i(TAG, "deleted ${old.size} session folder(s) past the newest ${SessionPaths.KEEP_SESSIONS}") } - /** - * Every session folder but the one in progress, and the one-off command logs (tools/, or loose - * beside the folders from before it): the Setup page's Clear logs. Returns how many session - * folders went. - */ @Synchronized fun clearAll(context: Context): Int { val current = SessionPaths.current() diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionDisplay.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionDisplay.kt index bdb557539..ae64b98cf 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionDisplay.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionDisplay.kt @@ -3,26 +3,14 @@ package com.droiddeck.launcher.session import android.content.Context import android.view.WindowManager -/** The virtual display advertised to the guest, before it is fitted onto the Android panel. */ object SessionDisplay { const val MATCH_SCREEN = "screen" - /** The window's size, kept in step while the session runs: a foldable opening or closing, a - * split screen or a freeform window resizes the guest's display instead of letterboxing it. */ const val FOLLOW_SCREEN = "follow" - /** Below this a side is a picture-in-picture or a sliver of split screen: not worth a resize. */ const val FOLLOW_MIN_SIDE = 480 - /** - * Presets are heights, not sizes: "720p" is the panel's own shape at 720 lines, never narrower - * than 16:9 and never taller than the panel (SHAPE_AUTO). The client treats the output as the - * games' native resolution, so the height is what bounds a game's default size on a 1440p - * phone; the shape is what keeps the client's interface edge to edge on a 20:9 one (1600x720) - * and keeps games at 16:9 or wider on a foldable's near-square panel. - */ const val DEFAULT_RESOLUTION = "720p" private val PRESET_HEIGHTS = listOf(720, 900, 1080) - /** The height of a preset choice ("900p"), or null for Match screen and fixed sizes. */ fun presetHeight(choice: String): Int? = if (choice.endsWith("p")) choice.dropLast(1).toIntOrNull()?.takeIf { it > 0 } else null @@ -32,42 +20,27 @@ object SessionDisplay { fun resolveChoice(panel: Pair<Int, Int>, choice: String): Pair<Int, Int> { if (choice == MATCH_SCREEN || choice == FOLLOW_SCREEN) return screenSize(panel) presetHeight(choice)?.let { return resolve(panel, it, SessionPrefs.SHAPE_AUTO) } - // Fixed sizes (custom, or a preset saved before presets were heights) keep their exact size, - // including legacy ones past the custom dialog's limit. val parts = choice.split('x').map { it.toIntOrNull() } return if (parts.size == 2 && parts.all { it != null && it > 0 }) parts[0]!! to parts[1]!! else resolve(panel, 720, SessionPrefs.SHAPE_AUTO) } - /** The height presets the panel is tall enough for, then the panel itself, with no duplicate - * sizes, then Follow screen. */ fun resolutionOptions(panel: Pair<Int, Int>): List<String> { val panelHeight = minOf(panel.first, panel.second) return PRESET_HEIGHTS.filter { it <= panelHeight }.map { "${it}p" } .filter { resolveChoice(panel, it) != screenSize(panel) } + MATCH_SCREEN + FOLLOW_SCREEN } - /** A Follow screen session's display for a window of this size: its own shape, square or - * portrait included, with even sides; null when it is too small to follow (PiP). */ fun followSize(width: Int, height: Int): Pair<Int, Int>? = if (minOf(width, height) < FOLLOW_MIN_SIDE) null else (width and 1.inv()) to (height and 1.inv()) - /** Unfolded controls: the most of the window the game may take, so a window near 16:9 itself - * still leaves the Deck controls room. */ const val UNFOLDED_MAX_GAME_SHARE = 0.8f - /** A foldable: Android 11's hinge angle sensor, which foldables report and slates do not. Unfolded - * controls are offered only there - untested on tablets, whose 16:10 landscape leaves them - * little room under a 16:9 game. */ fun foldable(context: Context): Boolean = context.packageManager.hasSystemFeature("android.hardware.sensor.hinge_angle") - /** A panel at least this wide on its shortest side (dp) is a foldable's unfolded inner screen. */ const val UNFOLDED_MIN_DP = 600 - /** Unfolded controls: the game's height in a window of this size - 16:9 across the full width, - * held either way, with even sides - and the Deck controls take the rest below it, laid out to - * whatever that leaves (DeckControlsPanel's GamepadLayout). */ fun unfoldedGameHeight(width: Int, height: Int): Int = minOf(width * 9 / 16, (height * UNFOLDED_MAX_GAME_SHARE).toInt()) and 1.inv() @@ -79,13 +52,10 @@ object SessionDisplay { return maxOf(mode.physicalWidth, mode.physicalHeight) to minOf(mode.physicalWidth, mode.physicalHeight) } - /** Interpret an older cap/aspect combination without altering the saved picture size. */ fun resolve(panel: Pair<Int, Int>, cap: Int, shape: String, custom: Pair<Int, Int>? = null): Pair<Int, Int> { custom?.let { return it } val panelW = maxOf(panel.first, panel.second).toFloat() val panelH = minOf(panel.first, panel.second).toFloat().coerceAtLeast(1f) - // Auto retains wide panels but floors squarer ones at 16:9 for game compatibility. - // Match screen removes that floor; fixed 16:9 also keeps foldable sessions stable. val aspect = when (shape) { SessionPrefs.SHAPE_WIDE -> 16f / 9f SessionPrefs.SHAPE_EXACT -> panelW / panelH @@ -95,7 +65,6 @@ object SessionDisplay { return ((height * aspect).toInt() and 1.inv()) to (height and 1.inv()) } - /** Mirrors gamescope's even-width 16:9 calculation, including near-16:9 rounding. */ fun canStretch16x9(size: Pair<Int, Int>): Boolean = (size.second * 16 + 4) / 9 / 2 * 2 > size.first } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt index 5f8f2bf96..86b027af6 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt @@ -6,15 +6,11 @@ import org.json.JSONObject import java.io.File import java.io.FileOutputStream -/** Writes the machine-readable lifecycle stream beside the existing session artifacts. */ object SessionEvents { private const val TAG = "SessionEvents" private val lock = Any() - /** [label] names the session's folder for what is being run (SessionPaths.label). */ fun begin(context: Context, mode: String, label: String? = null) { - // A runtime-install failure can leave the folder claimed before the service ever starts. - // Keep that folder, but give the next attempt its own session directory. SessionPaths.take() val dir = SessionPaths.beginOrCurrent(context, label ?: SessionPaths.label(mode, null)) synchronized(SessionState) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt index 9b3d4e650..b61ac122c 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt @@ -11,37 +11,22 @@ import com.droiddeck.launcher.runtime.LinuxRuntime import com.droiddeck.launcher.runtime.ProotFastPath import java.io.File -/** Everything the session needs written into the runtime before it starts. */ object SessionFiles { private const val TAG = "SessionFiles" private const val NO_PAD_SWITCH = "Download/droiddeck-no-pad" - /** Where the DirectAudio driver lives inside the runtime; the wrappers get it as BL_DIRECTAUDIO. */ const val DIRECTAUDIO_DIR = "usr/local/lib/directaudio" - /** The driver sets staged under it: classic Wine 11 (Valve, GE) and the system-thread interface (Proton-CachyOS). */ val DIRECTAUDIO_SETS = arrayOf("linux-wine11", "linux-wine11-systhread") - // Staged only with a complete desktop; removal matches these against the APK as well. internal val DESKTOP_FILES = arrayOf( "usr/local/bin/droiddeck-desktop" to "usr/local/bin/droiddeck-desktop", - // Games and emulators from the menu, full screen in a gamescope of their own. "usr/local/bin/droiddeck-gpu" to "usr/local/bin/droiddeck-gpu", "usr/local/bin/droiddeck-desktop-gpu" to "usr/local/bin/droiddeck-desktop-gpu", "usr/local/bin/droiddeck-desktop-window-rules" to "usr/local/bin/droiddeck-desktop-window-rules", - // KWin at the app's surface size, for Plasma's session (it starts KWin through this name). "usr/local/bin/kwin_wayland_wrapper" to "usr/local/bin/kwin_wayland_wrapper", "usr/lib/firefox/defaults/pref/droiddeck.js" to "usr/lib/firefox/defaults/pref/droiddeck.js", "etc/xdg/autostart/droiddeck-clipboard.desktop" to "etc/xdg/autostart/droiddeck-clipboard.desktop", ) - /** - * The libraries and scripts the session runs, refreshed from the apk at every launch. - * - * The runtime image carries its own copies, but a runtime installed months ago carries the - * copies of that day and a device has no way to replace them from outside the app. Staging - * them here is how a fix inside the session shim, the controller reader or one of the scripts - * reaches an already-installed runtime without a ~790 MB re-download. Each lands through a - * rename, so a session that still has one mapped keeps the file it opened. - */ fun stage(context: Context, root: File) { if (DesktopCatalog.desktopInstalled(root)) try { DesktopCatalog.cleanLegacy(context) } catch (e: Exception) { Log.w(TAG, "LXQt cleanup will retry next session", e) } @@ -81,47 +66,30 @@ object SessionFiles { "usr/local/bin/droiddeck-desktop-bookmarks" to "usr/local/bin/droiddeck-desktop-bookmarks", "usr/local/bin/droiddeck-steam-shim" to "usr/local/bin/droiddeck-steam-shim", "usr/local/bin/droiddeck-steam-shortcuts" to "usr/local/bin/droiddeck-steam-shortcuts", - // Asks the app for an Epic game's sign-in code at launch, from the compat tool (StoreLaunchRequests). "usr/local/bin/droiddeck-store-launch" to "usr/local/bin/droiddeck-store-launch", - // A game's web links to Android's browser (Wine's winebrowser starts it). "usr/local/bin/droiddeck-open-url" to "usr/local/bin/droiddeck-open-url", "usr/local/bin/droiddeck-steam-games" to "usr/local/bin/droiddeck-steam-games", "usr/local/bin/droiddeck-pad-defaults" to "usr/local/bin/droiddeck-pad-defaults", - // Cardboard mode's SteamVR: install, start and stop (docs/cardboard-vr.md). "usr/local/bin/droiddeck-steamvr" to "usr/local/bin/droiddeck-steamvr", - // Flatpak: the bwrap stand-in, the store's helper and setup, and the front end's launcher. "usr/local/bin/droiddeck-bwrap" to "usr/local/bin/droiddeck-bwrap", "usr/local/bin/droiddeck-flatpak" to "usr/local/bin/droiddeck-flatpak", "usr/local/bin/droiddeck-flatpak-setup" to "usr/local/bin/droiddeck-flatpak-setup", "usr/local/bin/droiddeck-flatpak-run" to "usr/local/bin/droiddeck-flatpak-run", - // The user's own AppImages, extracted at import (AppImageManager). "usr/local/bin/droiddeck-appimage-run" to "usr/local/bin/droiddeck-appimage-run", "usr/local/bin/droiddeck-script-run" to "usr/local/bin/droiddeck-script-run", - // The desktop's Run entries and its exec fallback: Windows programs through Steam's Proton, - // x86 Linux programs through FEX. "usr/local/bin/droiddeck-open" to "usr/local/bin/droiddeck-open", "usr/local/bin/droiddeck-proton-run" to "usr/local/bin/droiddeck-proton-run", - // The SteamOS helpers the client calls in Deck mode: the two Armada found it needs, plus - // the three under /usr/bin, all no-ops that answer "nothing to do" (see each file). "usr/bin/steamos-update" to "usr/bin/steamos-update", "usr/bin/steamos-select-branch" to "usr/bin/steamos-select-branch", - // Big Picture's "Switch to Desktop": asks the app for the desktop with Steam in it. "usr/bin/steamos-session-select" to "usr/bin/steamos-session-select", "usr/bin/jupiter-biosupdate" to "usr/bin/jupiter-biosupdate", "usr/bin/steamos-polkit-helpers/steamos-priv-write" to "usr/bin/steamos-polkit-helpers/steamos-priv-write", "usr/bin/steamos-polkit-helpers/steamos-set-timezone" to "usr/bin/steamos-polkit-helpers/steamos-set-timezone", - // On device the client called these four by their polkit-helpers path, not /usr/bin: the - // "Update Error" dialog was steamos-update missing there. "usr/bin/steamos-polkit-helpers/steamos-update" to "usr/bin/steamos-polkit-helpers/steamos-update", "usr/bin/steamos-polkit-helpers/steamos-select-branch" to "usr/bin/steamos-polkit-helpers/steamos-select-branch", "usr/bin/steamos-polkit-helpers/jupiter-biosupdate" to "usr/bin/steamos-polkit-helpers/jupiter-biosupdate", "usr/bin/steamos-polkit-helpers/jupiter-dock-updater" to "usr/bin/steamos-polkit-helpers/jupiter-dock-updater", ) - // The patched gamescope (tools/gamescope): the runtime's own version rebuilt with the ARM64 - // client fixes, over /usr/local/bin so it comes first in the session's PATH. Only when the - // apk carries it - a build without the asset leaves the runtime's copy alone. - // Valve's mangoapp (tools/mangoapp) - Deck mode's performance overlay - with the five - // libraries the runtime lacks beside it, and the wrapper on PATH that points it at them. val mangoapp = listOf( "usr/local/bin/mangoapp", "usr/local/lib/mangoapp/mangoapp", @@ -131,11 +99,9 @@ object SessionFiles { "usr/local/lib/mangoapp/libtraceevent.so.1", "usr/local/lib/mangoapp/libtracefs.so.1", ).map { it to it } - // What the Windows components installer reads .msi packages with (tools/msitools). val msitools = listOf("msiinfo", "cabextract", "7z", "7z.so", "libmsi-1.0.so.0", "libgsf-1.so.114", "libgcab-1.0.so.0") .map { "usr/local/lib/droiddeck-msitools/$it" } + listOf("NOTICE", "GPL-2", "GPL-3", "LGPL-2.1", "7zip-License", "7zip-unRarLicense").map { "usr/local/share/licenses/droiddeck-msitools/$it" } - // Cardboard mode's SteamVR driver and the Vulkan layer its compositor needs (tools/vr). val vr = listOf( "droiddeck/bin/linuxarm64/driver_droiddeck.so", "droiddeck/driver.vrdrivermanifest", @@ -143,8 +109,6 @@ object SessionFiles { "layer/libVkLayer_droiddeck_vr.so", "layer/VK_LAYER_DROIDDECK_vr.json", ).map { "usr/local/lib/droiddeck-vr/$it" to "usr/local/lib/droiddeck-vr/$it" } + - // The same layer, implicit for the whole session: it passes everything through until SteamVR - // runs, then Steam's own web helper gets the shared-texture support it needs (tools/vr). listOf("usr/local/lib/droiddeck-vr/layer/VK_LAYER_DROIDDECK_vr_implicit.json" to "usr/share/vulkan/implicit_layer.d/droiddeck_vr.json") val fexPreloads = listOf("x86_64", "i386").flatMap { arch -> @@ -180,12 +144,6 @@ object SessionFiles { LegacyNames.migrateRootfs(root) ComponentsManager.migrateLaunchDir(context) EsyncPacks.stageBundled(context, root) - // The DirectAudio driver for games under Proton: the glibc build of winedirectaudio, which - // the Proton wrappers add to WINEDLLPATH when the session asks for it (BL_DIRECTAUDIO). Two - // sets, one per Wine audio interface - the wrapper picks by what the Proton's own winepulse.so - // imports (see steam-compatibility), because both Wines call themselves 11.0 and the wrong set - // plays silence. Staged like the scripts, so a driver fix reaches an installed runtime without - // re-hosting. The sets come from the pinned DirectAudio release (tools/directaudio/release.env). val directAudio = arrayOf( "aarch64-unix/winedirectaudio.so", "aarch64-windows/winedirectaudio.drv", @@ -209,18 +167,11 @@ object SessionFiles { } if (!installed) Log.e(TAG, "DirectAudio $set/$relative NOT staged") } - // A runtime from before the sets had names kept the driver at the directory's root; it would - // never be picked again, so it goes. for (old in arrayOf("aarch64-unix", "aarch64-windows", "i386-windows")) File(root, "$DIRECTAUDIO_DIR/lib/wine/$old").deleteRecursively() - // What every process in the session preloads. LD_PRELOAD in the environment would not - // survive: the Steam client rebuilds it for each process it starts and appends its own - // overlay entry without a separator, which silently drops whatever was there. val preload = StringBuilder("/usr/local/lib/libblsession.so\n") if (!File(Environment.getExternalStorageDirectory(), NO_PAD_SWITCH).exists()) { preload.append("/usr/local/lib/libfakeinput.so\n") } - // Last, so the others' opens and stats reach it through RTLD_NEXT. It stays idle in any - // process its proot did not describe to it (ProotFastPath). if (ProotFastPath.enabled(context)) preload.append(ProotFastPath.LIBRARY + "\n") val etc = File(root, "etc").apply { mkdirs() } val staged = File(etc, "ld.so.preload.staged") @@ -234,8 +185,6 @@ object SessionFiles { } val startupMovieDir = File(root, "root/.local/share/Steam/config/uioverrides/movies") - // Steam looks up these conventional names in its user override directory. Keep both - // variants populated because the startup movie name differs across Steam clients. val bigPictureMovieInstalled = stageStartupMovie( context, startupMovieDir, @@ -387,10 +336,5 @@ object SessionFiles { indent + "}", ).joinToString(newline) - /** - * Where the session writes its log: app-private `files/logs`, bound into the session at the same - * path, so the session script's own redirection always opens. A folder leaves the device only - * through Share logs, which scrubs every file again on the way into the zip. - */ fun logDirectory(context: Context): File = LinuxRuntime.logDir(context).apply { mkdirs() } } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt index 85760c822..11f008fe6 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt @@ -17,28 +17,12 @@ import java.io.File import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream -/** - * Share logs: one zip, made when asked, of a session folder (the newest by default) with the last - * seven days of the stores' log and the one-off command logs beside it, handed to Android's share - * sheet. Logs live in app-private `files/logs`; this is how they leave the device. Every text file - * goes through [LogRedactor.redactForShare] on the way in, so even a file written before a rule - * existed is clean in the zip. The zip sits in `cache/share/` until the next share or app start. - */ object SessionLogShare { private val main = Handler(Looper.getMainLooper()) - /** - * How far the zip for a share has got, 0..1, or null when none is being made. The Share logs - * buttons show it in place of their label, so a tap is answered at once. - */ var progress by mutableStateOf<Float?>(null) private set - /** - * Zips [folder] on a worker thread with [progress] following along, then hands the zip (null - * if there was nothing to zip or it failed) to [done] on the main thread. Call on the main - * thread; a tap while a zip is being made is ignored. - */ fun prepare(context: Context, folder: () -> File?, done: (File?) -> Unit) { if (progress != null) return progress = 0f @@ -50,29 +34,17 @@ object SessionLogShare { }, "share-logs").start() } - /** The newest session folder in either place logs are written, or null if there is none. */ fun latest(context: Context): File? = SessionPaths.sessionFolders(context).lastOrNull() - /** Builds the zip (blocking). Returns null when there is no session to share. */ - /** Builds a zip for one specific session folder (blocking). */ fun zipFolder(context: Context, folder: File, onProgress: ((Float) -> Unit)? = null): File? { if (!folder.isDirectory) return null - // The client's logs past STEAM_LOG_MAX_BYTES stay out, as they do from the app's own copy: a - // CEF debug log or an old *.previous.txt reached 17 MB, every share scrubbed them line by - // line, and the share sheet took ten seconds to appear. val files = folder.walkTopDown().filter { it.isFile } .filterNot { it.parentFile?.name == "steam" && it.length() > STEAM_LOG_MAX_BYTES }.toList() if (files.isEmpty()) return null val out = shareDir(context).apply { deleteRecursively(); mkdirs() } val zip = File(out, "DroidDeck-${folder.name}.zip") - // Scrubbed on the way into the zip: a session shared while it runs has not had its end-of- - // session pass yet, and the redactor changes nothing in a line that is already clean. LogRedactor.learnFromRuntime(LinuxRuntime.rootDir(context)) - // A finished folder was scrubbed whole when it ended. A file its record lists, unchanged - // since, goes in as it is; anything else (added or changed since, hidden, or any file of a - // session still running) is scrubbed again. Scrubbing tens of MB of the client's logs on - // every share kept the share sheet from appearing for ten seconds or more. val live = liveSteamLogs(context, folder) val logs = LinuxRuntime.logDir(context) val stores = recent(File(logs, STORES_DIR)) @@ -96,7 +68,6 @@ object SessionLogShare { private fun addEntry(z: ZipOutputStream, name: String, f: File) { z.putNextEntry(ZipEntry(name)) - // Every text file, every time: the folder's own scrub may predate a rule. if (LogRedactor.isText(f)) { val w = z.bufferedWriter() LogRedactor.scrubForShare(f, w) @@ -107,38 +78,26 @@ object SessionLogShare { z.closeEntry() } - /** - * The client's own logs as they stand, for a session shared while it runs. The session script - * copies them into steam/ only as it exits, so a zip made from the drawer had none - and - * controller.txt (which pad the client opened, the touch mode it set) is what a controller or - * touch report needs most. Only for the running session: an older folder would get this - * session's logs. The same files the script copies; nothing holding credentials is in logs/. - */ private fun liveSteamLogs(context: Context, folder: File): List<File> { if (folder != SessionPaths.current() || File(folder, "steam").exists()) return emptyList() val logs = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/logs") return logs.listFiles()?.filter { it.isFile && it.length() <= STEAM_LOG_MAX_BYTES }.orEmpty() } - /** The client's content and bootstrap logs grow large over months and say nothing about a session. */ private const val STEAM_LOG_MAX_BYTES = 8L * 1024 * 1024 - /** The stores' engine log, one file a day, under files/logs. */ const val STORES_DIR = "stores" private const val RECENT_DAYS = 7 private fun shareDir(context: Context): File = File(context.cacheDir, "share") - /** The files in [dir] changed in the last [RECENT_DAYS] days. */ private fun recent(dir: File): List<File> { val since = System.currentTimeMillis() - RECENT_DAYS * 86_400_000L return dir.listFiles { f -> f.isFile && f.lastModified() >= since }?.sortedBy { it.name }.orEmpty() } - /** Deletes the last share's zip; at app start, so a zip never outlives the share it was made for. */ fun clear(context: Context) { shareDir(context).deleteRecursively() - // Where builds before this one put it. File(context.cacheDir, "shared-logs").deleteRecursively() } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPaths.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPaths.kt index 5b94c6a44..bb9940ffe 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPaths.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPaths.kt @@ -7,42 +7,14 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * One folder per session under app-private `files/logs/`, holding everything that session recorded, - * named for the day, that day's session number and what was run, so the one a report is about can - * be picked out without opening any: - * - * ``` - * files/logs/2026-09-30-03-steam/ - * device.txt what this device is, and every setting the session ran with - * session.log the guest session: proot, gamescope, the client's stdout - * wayland.log the app's compositor - * steam.log the Steam client's own log, scrubbed (Steam mode) - * steam/ the rest of the client's logs, scrubbed (Steam mode) - * desktop.log KWin, Plasma and the programs on it (desktop mode) - * files/logs/tools/ one-off commands: Flatpak installs, AppImage imports - * files/logs/stores/ the stores' engine log, one file a day - * ``` - * - * Folders from before this naming (`session-20260930-180642`) are still recognised, shared and - * pruned with the rest. - * - * The folder is claimed by whoever starts first - the activity starts the compositor before the - * service starts the session - so both write into the same one, and a recreated activity (a - * foldable opening mid-session) joins the folder in progress instead of opening another. - */ object SessionPaths { private const val TAG = "SessionPaths" - /** How many session folders are kept; older ones go at the next app start. */ const val KEEP_SESSIONS = 30 - /** Beside the session folders: the logs of one-off commands (GuestCommand's logName). */ const val TOOLS_DIR = "tools" - /** `2026-09-30-03-steam`: day, that day's session number, what was run. */ private val NAMED = Regex("""^(\d{4})-(\d{2})-(\d{2})-(\d{2,})-.+$""") - /** `session-20260930-180642`, from before. */ private val LEGACY = Regex("""^session-(\d{8})-(\d{6})(-\d+)?$""") @Volatile @@ -51,20 +23,12 @@ object SessionPaths { @Volatile private var ended: File? = null - /** - * The folder for the session now starting, or the one already in progress. With logs turned - * off on the main screen the folder lives in the app's cache instead of Downloads - the - * scripts and the compositor still need somewhere to write - and [release] deletes it. - * [label] says what is being run (see [label]); without one it comes from the session's state. - */ @Synchronized fun beginOrCurrent(context: Context, label: String? = null): File { dir?.let { if (it.isDirectory) return it } val parent = if (SessionPrefs.logsEnabled(context)) SessionFiles.logDirectory(context) else File(context.cacheDir, "session-logs") val day = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date()) - // The day's highest number plus one, not a count: pruning removes the oldest folders, and - // a count would hand out a number already in use. val last = parent.listFiles()?.mapNotNull { f -> NAMED.matchEntire(f.name)?.takeIf { f.name.startsWith("$day-") }?.groupValues?.get(4)?.toIntOrNull() }?.maxOrNull() ?: 0 @@ -80,14 +44,8 @@ object SessionPaths { fun current(): File? = dir - /** The folder of the session that ended last, while it is still there: what its Share logs sends. */ fun lastEnded(): File? = ended?.takeIf { it.isDirectory } - /** - * The ending session takes its folder with it: the next session (one that replaces this one - * in place) then claims a folder of its own instead of sharing, and this one's collector - * finishes the folder it was handed. Called on the main thread as the session stops. - */ @Synchronized fun take(): File? { val taken = dir @@ -98,22 +56,14 @@ object SessionPaths { fun file(name: String): File? = dir?.let { File(it, name) } - /** Let the next session claim a new folder. Called when a session ends. */ @Synchronized fun release(context: Context, ended: File) { - // Only the session that owns the folder lets go of it (take() usually has already): the - // next session may have claimed its own by the time the last one's collector gets here. if (dir == ended) dir = null - // A folder kept in the cache was never meant to outlive its session. if (ended != null && ended.path.startsWith(context.cacheDir.path)) { com.droiddeck.launcher.core.FileUtils.delete(ended) } } - /** - * What a session's folder is named for: Steam, the desktop, or the program started from the - * rail by its own name (`2026-10-01-02-retroarch`). - */ fun label(mode: String, program: String?): String = when (mode) { SessionService.MODE_RUN -> com.droiddeck.launcher.frontend.Library.nameForProgram(program) ?: program?.substringAfterLast('/') @@ -122,21 +72,15 @@ object SessionPaths { else -> mode } - /** Whether [f] is a session's folder, in either naming. */ fun isSessionFolder(f: File): Boolean = f.isDirectory && (NAMED.matches(f.name) || LEGACY.matches(f.name)) - /** - * Oldest first: by day, then by when in the day. A folder from before the new naming sorts - * ahead of a new one from the same day, which it always predates. - */ val chronological: Comparator<File> = compareBy { f -> NAMED.matchEntire(f.name)?.groupValues?.let { g -> "${g[1]}${g[2]}${g[3]}1${g[4].padStart(6, '0')}" } ?: LEGACY.matchEntire(f.name)?.groupValues?.let { g -> "${g[1]}0${g[2]}${g[3].removePrefix("-").padStart(2, '0')}" } ?: f.name } - /** Every session folder in the places logs are written, oldest first. */ fun sessionFolders(context: Context): List<File> = (com.droiddeck.launcher.runtime.LinuxRuntime.logDir(context).listFiles { f -> isSessionFolder(f) }?.toList() ?: emptyList()) .sortedWith(chronological) diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPipController.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPipController.kt index 92070761d..1b26e7de2 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPipController.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPipController.kt @@ -12,7 +12,6 @@ import android.widget.Toast import com.droiddeck.launcher.R import com.droiddeck.launcher.SessionActivity -/** Owns Android's PiP parameters, while the service continues to own the session. */ class SessionPipController( private val activity: SessionActivity, private val bounds: () -> Rect, diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index b941508d4..0d4b827fe 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -12,7 +12,6 @@ import com.droiddeck.launcher.gpu.Cardboard import com.droiddeck.launcher.gpu.ScreenEffects import org.json.JSONObject -/** The in-session switches: the HUD and how the on-screen controls decide to appear. */ object SessionPrefs { data class GameTextureFiltering(val anisotropy: Int? = null, val lodBias: String? = null) @@ -32,10 +31,8 @@ object SessionPrefs { const val OSC_ALWAYS = "always" const val OSC_STEAM_QAM = "steam-qam" const val OSC_NEVER = "never" - /** Steam's touch controller (SteamTouchControls) in place of the app's pad; Steam sessions only. */ const val OSC_STEAM_TOUCH = "steam-touch" - /** What Back does in a Steam session, first press then second: the labels of the two orders. */ val BACK_MENU_THEN_QAM = R.string.back_menu_then_qam val BACK_QAM_THEN_MENU = R.string.back_qam_then_menu @@ -61,7 +58,6 @@ object SessionPrefs { prefs(context).edit().putInt("orientation", orientation).apply() } - /** Whether the launcher hides Android's status and navigation bars. */ fun launcherFullscreen(context: Context): Boolean = prefs(context).getBoolean("launcherFullscreen", true) fun setLauncherFullscreen(context: Context, on: Boolean) { @@ -80,14 +76,12 @@ object SessionPrefs { prefs(context).edit().putBoolean("gameStoresEnabled", on).apply() } - /** The store download engine's speed tier (stores/download/StoreDownloadTier ids); Fast unless chosen otherwise. */ fun gameStoresSpeedTier(context: Context): String = prefs(context).getString("gameStoresSpeedTier", "fast") ?: "fast" fun setGameStoresSpeedTier(context: Context, tier: String) { prefs(context).edit().putString("gameStoresSpeedTier", tier).apply() } - /** Which tab a signed-in store opens on when its chip is picked: [STORES_OPEN_LIBRARY] or [STORES_OPEN_STORE]. */ fun storesOpenTab(context: Context): String = prefs(context).getString("storesOpenTab", STORES_OPEN_LIBRARY)?.takeIf { it == STORES_OPEN_STORE } ?: STORES_OPEN_LIBRARY @@ -95,7 +89,6 @@ object SessionPrefs { prefs(context).edit().putString("storesOpenTab", if (tab == STORES_OPEN_STORE) STORES_OPEN_STORE else STORES_OPEN_LIBRARY).apply() } - /** Mature titles (the stores' own 17+/18 ratings or adult tags) in the storefront's shelves and search; off by default. Owned games always show. */ fun storesShowMature(context: Context): Boolean = prefs(context).getBoolean("storesShowMature", false) fun setStoresShowMature(context: Context, show: Boolean) { @@ -105,25 +98,18 @@ object SessionPrefs { const val STORES_OPEN_LIBRARY = "library" const val STORES_OPEN_STORE = "store" - /** The install root picked last time the Stores asked where (a StoreInstallRoot target's path); only the dialog's default, never a silent choice. */ fun storesInstallTarget(context: Context): String = prefs(context).getString("storesInstallTarget", "") ?: "" fun setStoresInstallTarget(context: Context, path: String) { prefs(context).edit().putString("storesInstallTarget", path).apply() } - /** How many store downloads run at once, 1..3; one by default (a single download already fills the link). */ fun gameStoresParallel(context: Context): Int = prefs(context).getInt("gameStoresParallel", 1).coerceIn(1, 3) fun setGameStoresParallel(context: Context, count: Int) { prefs(context).edit().putInt("gameStoresParallel", count.coerceIn(1, 3)).apply() } - /** - * The session's performance HUD (the fps box). A Deck-mode Steam session with the performance - * overlay has Steam's own (mangoapp, from the QAM), so there the HUD is off unless turned on - * in one - a choice kept apart from every other session's, where it stays on by default. - */ fun hudEnabled(context: Context): Boolean = prefs(context).getBoolean(hudKey(context), !mangoappSession(context)) fun setHudEnabled(context: Context, on: Boolean) { @@ -132,11 +118,9 @@ object SessionPrefs { private fun hudKey(context: Context) = if (mangoappSession(context)) "hudDeck" else "hud" - /** A Steam session with Deck mode's performance overlay (mangoapp). */ private fun mangoappSession(context: Context) = SessionState.mode == SessionService.MODE_STEAM && mangoapp(context) - /** When enabled, a single Back opens Steam QAM and a double Back opens the session menu. */ fun backActionsInverted(context: Context): Boolean = prefs(context).getBoolean("backActionsInverted", false) fun setBackActionsInverted(context: Context, inverted: Boolean) { @@ -148,9 +132,6 @@ object SessionPrefs { const val TOUCH_DIRECT = "direct" const val TOUCH_OFF = "off" - /** How touch drives the pointer: a touchpad (drag moves it from where it is) or direct - * (it jumps under the finger). Auto = touchpad on the desktop, direct in Steam. - * Off ignores touches on the guest picture; Android controls remain usable. */ fun touchMode(context: Context): String = prefs(context).getString("touch", TOUCH_AUTO) ?: TOUCH_AUTO fun setTouchMode(context: Context, mode: String) { @@ -161,12 +142,6 @@ object SessionPrefs { const val SHAPE_WIDE = "16:9" const val SHAPE_EXACT = "exact" - /** - * The shape of the display the session presents: the panel's own (never narrower than 16:9), - * exactly the panel's (a 4:3 or 3:2 handheld, drawn edge to edge), or a fixed 16:9. A foldable defaults to 16:9, which sits with modest bars on either of its - * panels; the panel's own shape would fit one and leave a strip on the other, and gamescope's - * display cannot change size once the session is up. - */ private fun shapeMode(context: Context): String = prefs(context).getString("shape", null) ?: if (context.packageManager.hasSystemFeature("android.hardware.sensor.hinge_angle")) SHAPE_WIDE else SHAPE_AUTO @@ -177,12 +152,6 @@ object SessionPrefs { prefs(context).edit().putString("osc", mode).apply() } - /** - * The imported glibc Turnip every session draws with inside the runtime - Steam, its games and - * the desktop alike, as they run on the same GPU; "" = the driver built into the runtime. - * Resolved by LinuxVulkanDriver at session start. It was once chosen per mode: the Steam - * session's choice, the one nearly everyone set, carries over. - */ fun linuxDriver(context: Context): String = prefs(context).getString("linuxDriver", null) ?: prefs(context).getString("linuxDriver.steam", null) @@ -195,11 +164,6 @@ object SessionPrefs { const val GPU_DRIVERS_AUTO = "auto" const val GPU_DRIVERS_MANUAL = "manual" - /** - * Who picks the GPU drivers: [GPU_DRIVERS_AUTO] (the app, the matched pair recommended for - * this GPU, kept current - DriverPairs) or [GPU_DRIVERS_MANUAL] (the user). Auto for new - * installs; see settleGpuDriverMode. - */ fun gpuDriverMode(context: Context): String = prefs(context).getString("gpuDriverMode", GPU_DRIVERS_AUTO) ?: GPU_DRIVERS_AUTO @@ -207,10 +171,6 @@ object SessionPrefs { prefs(context).edit().putString("gpuDriverMode", mode).apply() } - /** - * Auto arrived after people had picked drivers by hand: an install that chose either driver - * keeps its choice (Manual), everyone else is Auto. Decided once, at process start. - */ fun settleGpuDriverMode(context: Context) { val p = prefs(context) if (p.contains("gpuDriverMode")) return @@ -218,7 +178,6 @@ object SessionPrefs { p.edit().putString("gpuDriverMode", if (chosen) GPU_DRIVERS_MANUAL else GPU_DRIVERS_AUTO).apply() } - /** Drivers Auto downloaded: the only ones it removes when a newer pair replaces them. */ fun gpuAutoInstalled(context: Context): Set<String> = prefs(context).getStringSet("gpuAutoInstalled", emptySet()).orEmpty() @@ -226,9 +185,6 @@ object SessionPrefs { prefs(context).edit().putStringSet("gpuAutoInstalled", ids.toSet()).apply() } - // The Steam client's own sound has one route now: DirectAudio's engine inside the PulseAudio - // daemon (PulseAudioComponent), one step from Android. The old "clientDirectAudio" boolean - // (which meant the relay route, one hop more) is no longer read. fun stretch16x9(context: Context): Boolean = prefs(context).getBoolean("stretch16x9", false) @@ -237,20 +193,14 @@ object SessionPrefs { } - /** - * DirectAudio for games: their Wine audio driver talks to the relay helper on this side. On - * unless the user turned it off. - */ fun directAudio(context: Context): Boolean = prefs(context).getBoolean("directAudio", true) fun setDirectAudio(context: Context, on: Boolean) { prefs(context).edit().putBoolean("directAudio", on).apply() } - /** The microphone for voice chat, off until the user turns it on (which asks for RECORD_AUDIO). */ fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", false) - /** Whether the app has already asked for the microphone once at start-up. */ fun micAsked(context: Context): Boolean = prefs(context).getBoolean("micAsked", false) fun setMicAsked(context: Context) { @@ -261,7 +211,6 @@ object SessionPrefs { prefs(context).edit().putBoolean("mic", on).apply() } - /** Optional SSID reporting and scans. A Location grant alone never opts the user in. */ fun wifiDiscoveryEnabled(context: Context): Boolean = prefs(context).getBoolean("wifiDiscovery", false) fun setWifiDiscoveryEnabled(context: Context, on: Boolean) { @@ -276,7 +225,6 @@ object SessionPrefs { internal data class CoreAllocation(val clientOverride: Boolean, val clientCpus: String, val gameCpus: String) - /** Defaults are read only for absent keys; even a saved empty/all-core choice wins. */ internal fun coreAllocation(context: Context, defaults: CpuAllocationDefaults.Allocation? = CpuAllocationDefaults.device): CoreAllocation { val saved = prefs(context) return CoreAllocation( @@ -286,37 +234,24 @@ object SessionPrefs { ) } - /** Use the device default until the user saves an override or a client mask. */ fun clientCpusOverride(context: Context): Boolean = coreAllocation(context).clientOverride fun setClientCpusOverride(context: Context, on: Boolean) { prefs(context).edit().putBoolean("clientCpusOverride", on).apply() } - /** Comma-separated core list for the client (taskset -c syntax); "" = every core. */ fun clientCpus(context: Context): String = coreAllocation(context).clientCpus fun setClientCpus(context: Context, list: String) { prefs(context).edit().putString("clientCpus", list).apply() } - /** Comma-separated core list for games (taskset -c syntax); "" = every core (explicitly restored when the client is pinned). */ fun gameCpus(context: Context): String = coreAllocation(context).gameCpus fun setGameCpus(context: Context, list: String) { prefs(context).edit().putString("gameCpus", list).apply() } - /** - * Whether Steam's xalia helper is kept out of the session (PROTON_USE_XALIA=0). - * - * xalia is an x86 Windows program Proton launches to give Windows programs gamepad navigation. - * Under FEX it cannot load the session's aarch64 preload shim, so its socket() and memfd calls - * reach the vendor's seccomp filter raw; where that answers ENOSYS - a Galaxy Fold, measured - - * it storms, and the session dies seconds after Big Picture appears. On by default: where it - * does run, it sits beside every game under FEX for nothing a controller-first session needs - * (about 10% of a core beside Once Upon a KATAMARI on an SD 8 Gen 2). - */ fun noXalia(context: Context): Boolean = prefs(context).getBoolean("noXalia", true) fun setNoXalia(context: Context, on: Boolean) { @@ -340,10 +275,6 @@ object SessionPrefs { const val SYNC_FSYNC = "fsync" const val SYNC_WINESERVER = "wineserver" - /** - * The sync Proton games use, as the three switches above decide it: droiddeck-ntsync wins - * while it is on, then droiddeck-fsync first, then droiddeck-esync, and wineserver alone while all three are off. - */ fun syncBackend(context: Context): String = syncBackendOf(fastSync(context), fsyncFirst(context), syncFallback(context)) fun syncBackendOf(fastSync: Boolean, fsyncFirst: Boolean, syncFallback: Boolean): String = when { @@ -353,7 +284,6 @@ object SessionPrefs { else -> SYNC_WINESERVER } - /** Picks one sync for Proton games; the switches change together, in one write. */ fun setSyncBackend(context: Context, backend: String) { require(backend == SYNC_ESYNC || backend == SYNC_NTSYNC || backend == SYNC_FSYNC || backend == SYNC_WINESERVER) { "unknown sync $backend" } prefs(context).edit() @@ -369,28 +299,12 @@ object SessionPrefs { prefs(context).edit().putBoolean("syncFallback", on).apply() } - /** - * Whether gamescope asks for realtime-priority Vulkan queues (GAMESCOPE_FORCE_VULKAN_REALTIME=1, - * which the app's gamescope build honours without CAP_SYS_NICE). Off by default, as in - * Bannerlator's session: the compositor's queue preempting the game's buys nothing on a device - * whose GPU is waiting on the CPU. - */ fun gamescopeRealtime(context: Context): Boolean = prefs(context).getBoolean("gamescopeRealtime", false) fun setGamescopeRealtime(context: Context, on: Boolean) { prefs(context).edit().putBoolean("gamescopeRealtime", on).apply() } - /** - * Whether proot runs without its seccomp acceleration (PROOT_NO_SECCOMP=1). - * - * proot normally installs a seccomp filter so only the syscalls it must rewrite stop in the - * tracer; everything else runs untraced, which is most of proot's speed. Where a vendor kernel - * handles that filter badly the wrong calls are trapped or refused - ENOSYS from calls that - * plainly exist is the signature - and the fallback is to trace everything instead: slower, - * but correct. Max's advice for devices whose kernels "don't work well with it". - */ - /** Hold the GPU at its top clock during a session (GpuClockPin). Off by default: power and heat. */ fun gpuClockPin(context: Context): Boolean = prefs(context).getBoolean("gpuClockPin", false) fun setGpuClockPin(context: Context, on: Boolean) { @@ -403,11 +317,6 @@ object SessionPrefs { prefs(context).edit().putBoolean("prootNoSeccomp", on).apply() } - /** - * Whether the session's path lookups take proot's fast path (ProotFastPath): answered inside - * each process instead of a round trip through the tracer. On by default; it needs proot's - * seccomp filter, so it is off whenever proot runs without one. - */ fun prootFastPath(context: Context): Boolean = prefs(context).getBoolean("prootFastPath", true) fun setProotFastPath(context: Context, on: Boolean) { @@ -430,43 +339,21 @@ object SessionPrefs { return trimmed.takeIf { Regex("[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?").matches(it) } } - /** - * Turnip's sysmem rendering (TU_DEBUG=sysmem) for the runtime's driver: bypasses GMEM tiling. - * On by default, as WinNative runs every Linux session: Chromium -> ANGLE -> Zink draws the - * client's interface as many small render passes, each paying GMEM's load/store and binning, - * and WinNative's A/B on an Adreno 840 put a game ahead with it too (Palworld 42.8 against 41.3). - */ fun tuSysmem(context: Context): Boolean = prefs(context).getBoolean("tuSysmem", true) fun setTuSysmem(context: Context, on: Boolean) { prefs(context).edit().putBoolean("tuSysmem", on).apply() } - /* - * The client's interface is drawn Chromium -> ANGLE -> Zink -> Turnip, and that chain is what - * limits its menus (~14 fps on an Adreno 840 while a game ran 89). These make the chain - * cheaper rather than asking for more; the three environment switches are on by default, - * Deck mode off. Bannerlator's LinuxTuning, carried over; none device-proven here yet. - */ - /** mesa_glthread=true: GL marshalled off the calling thread. */ fun glThread(context: Context): Boolean = prefs(context).getBoolean("glThread", true) fun setGlThread(context: Context, on: Boolean) { prefs(context).edit().putBoolean("glThread", on).apply() } - /** MESA_NO_ERROR=1: no GL error checking. */ fun noGlError(context: Context): Boolean = prefs(context).getBoolean("noGlError", true) fun setNoGlError(context: Context, on: Boolean) { prefs(context).edit().putBoolean("noGlError", on).apply() } - /** - * What the pad is to the Steam client: [CONTROLLER_DECK], a Steam Deck controller (Quick Access - * button, gyro, Steam Input's full treatment - SteamDeckPad), [CONTROLLER_STEAM], a 2025 Steam - * Controller (the same, plus grip sense and its own haptics), [CONTROLLER_DS4] or - * [CONTROLLER_DUALSENSE], a PlayStation pad with its touchpad fed by the trackpads, or - * [CONTROLLER_XBOX360], the plain Xbox 360 pad of earlier versions (QAM by the Guide+A chord). - */ fun steamController(context: Context): String = prefs(context).getString("steamController", CONTROLLER_DECK) ?: CONTROLLER_DECK fun setSteamController(context: Context, id: String) { prefs(context).edit().putString("steamController", id).apply() } - /** [steamController]'s choice as libfakeinput's pad type (FakeInputWriter.PAD_TYPE_*). */ fun padType(id: String): Int = when (id) { CONTROLLER_STEAM -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_STEAM CONTROLLER_XBOX360 -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_XBOX @@ -475,24 +362,15 @@ object SessionPrefs { else -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DECK } - /** - * Deck mode's performance overlay (mangoapp, beside gamescope): the QAM's Overlay Level draws - * through it. Off is the way out where Valve's mangoapp crashes (one Turnip build did). - */ fun mangoapp(context: Context): Boolean = prefs(context).getBoolean("mangoapp", true) fun setMangoapp(context: Context, on: Boolean) { prefs(context).edit().putBoolean("mangoapp", on).apply() } - /** - * Zink's lazy descriptor mode (ZINK_DESCRIPTORS=lazy) with its compact set layout - * (ZINK_DEBUG=compact) for the client's GL-on-Vulkan UI, as WinNative runs it. On by default. - */ fun zinkLazy(context: Context): Boolean = prefs(context).getBoolean("zinkLazy", true) fun setZinkLazy(context: Context, on: Boolean) { prefs(context).edit().putBoolean("zinkLazy", on).apply() } - /** The Android driver the compositor loads: "" = pick by GPU, else a bundled or imported id. */ @JvmStatic fun androidDriver(context: Context): String = prefs(context).getString("androidDriver", "") ?: "" @@ -501,12 +379,7 @@ object SessionPrefs { prefs(context).edit().putString("androidDriver", id).apply() } - // ── Storage the session can see ───────────────────────────────────────────────────────────── - /** - * The folder on this device that every session shows at `/root/ROMs`, for the emulators on the - * desktop. "" = none chosen. All of internal storage is at `/root/Storage` regardless. - */ fun romsDir(context: Context): String = prefs(context).getString("romsDir", "") ?: "" @@ -514,11 +387,6 @@ object SessionPrefs { prefs(context).edit().putString("romsDir", path).apply() } - /** - * Whether a session keeps its folder under files/logs. Off, the same logs are kept in - * the app's cache for the session's lifetime (the scripts need somewhere to write) and thrown - * away at the end, so nothing accumulates in Downloads. - */ fun logsEnabled(context: Context): Boolean = prefs(context).getBoolean("logs", true) @@ -526,7 +394,6 @@ object SessionPrefs { prefs(context).edit().putBoolean("logs", on).apply() } - /** Steam storage-call diagnostics are opt-in because they add timing work to file operations. */ fun storageDiagnosticsEnabled(context: Context): Boolean = prefs(context).getBoolean("storageDiagnostics", false) @@ -534,24 +401,13 @@ object SessionPrefs { prefs(context).edit().putBoolean("storageDiagnostics", on).apply() } - // ── Per-mode display ──────────────────────────────────────────────────────────────────── - /** - * The tallest the session's display may be, in pixels, for MODE_STEAM / MODE_DESKTOP: - * 0 = the panel's own height, otherwise a cap. Both modes default to 720p: the client's CEF - * is the heaviest thing in a session, Big Picture is drawn for a TV at arm's length, and on - * a handheld panel 720p is where its menus stay responsive on a regular flagship; the desktop - * and the emulators under it get the same GPU headroom. Read once, when the session's display - * is sized; a cap the user chose wins over the default. - */ private fun resolutionCap(context: Context, mode: String): Int = prefs(context).getInt("resolutionCap.$mode", 720) - /** Whether the user chose the mode's resolution (a cap or a custom size) rather than the default. */ fun resolutionChosen(context: Context, mode: String): Boolean = prefs(context).contains("displayResolution.$mode") || prefs(context).contains("resolutionCap.$mode") || customResolution(context, mode) != null - /** One per-mode choice now owns both dimensions; old caps/shapes are read only for migration. */ fun resolutionChoice(context: Context, mode: String, panel: Pair<Int, Int>): String { val saved = prefs(context) saved.getString("displayResolution.$mode", null)?.let { value -> @@ -574,24 +430,10 @@ object SessionPrefs { prefs(context).edit().putString("displayResolution.$mode", value).apply() } - /** - * The FEXCore preset for the games the client launches (core/FexPreset ids); "" = FEX's defaults. - * Performance + TSO unless chosen, WinNative's default: FEX's own defaults keep half-barrier TSO - * and full-precision x87, which cost every x86 game time; a game that needs them can still be - * given another preset. - */ fun fexPreset(context: Context): String = prefs(context).getString("fexPreset", DEFAULT_FEX_PRESET) ?: DEFAULT_FEX_PRESET private const val DEFAULT_FEX_PRESET = "PERFORMANCE_TSO" - /** - * Force SSBS for Proton games: Wine resumes threads from a Windows CONTEXT that never carries - * PSTATE.SSBS, so they run with speculative store bypass disabled; libssbs.so keeps it set - * (on DiRT 3 / GE-Proton: from ~99% of a game's threads running without it to none). The - * speed-up is reported on Oryon cores (Snapdragon 8 Elite) and was not measurable on an - * 8 Gen 3, so it is off unless turned on. A game's own environment can still say - * DROIDDECK_FORCE_SSBS=0. - */ fun forceSsbs(context: Context): Boolean = prefs(context).getBoolean("forceSsbs", false) fun setForceSsbs(context: Context, on: Boolean) { @@ -606,18 +448,12 @@ object SessionPrefs { .onFailure { android.util.Log.e("GameEnvironment", "Could not update game environment", it) } } - /** Whether opening DroidDeck starts a Steam session instead of showing the front end. */ fun runSteamAtStartup(context: Context): Boolean = prefs(context).getBoolean("runSteamAtStartup", false) fun setRunSteamAtStartup(context: Context, on: Boolean) { prefs(context).edit().putBoolean("runSteamAtStartup", on).apply() } - /** - * The Games folders earlier builds let the user add (one game per subfolder); read only by - * GamesFolderMigration, which turns them into entries and clears them. The single folder an - * even earlier build kept is carried in. - */ fun addedGamesDirs(context: Context): List<String> { val p = prefs(context) val list = p.getString("addedGamesDirs", null) @@ -630,10 +466,6 @@ object SessionPrefs { prefs(context).edit().putString("addedGamesDirs", dirs.distinct().joinToString("\n")).remove("addedGamesDir").apply() } - /** - * The Games folders earlier builds had (host path, guest path), still bound into the session so - * their games keep their paths; written once by GamesFolderMigration. - */ fun gameFolderBinds(context: Context): List<Pair<String, String>> = (prefs(context).getString("gameFolderBinds", "") ?: "").split('\n').mapNotNull { line -> line.split('\t').takeIf { it.size == 2 && it[0].isNotEmpty() && it[1].isNotEmpty() }?.let { it[0] to it[1] } @@ -643,17 +475,12 @@ object SessionPrefs { prefs(context).edit().putString("gameFolderBinds", binds.distinctBy { it.first }.joinToString("\n") { "${it.first}\t${it.second}" }).apply() } - /** The Games folders were turned into entries (GamesFolderMigration); a record, not a gate. */ fun gamesFoldersMigrated(context: Context): Boolean = prefs(context).getBoolean("gamesFoldersMigrated", false) fun setGamesFoldersMigrated(context: Context) { prefs(context).edit().putBoolean("gamesFoldersMigrated", true).apply() } - /** - * Game folders (by path) the user removed from the added games: a scan skips them, so they - * stay off the Games tab and out of Steam until restored. Nothing on disk is touched. - */ fun removedAddedGames(context: Context): List<String> = (prefs(context).getString("removedAddedGames", "") ?: "").split('\n').filter { it.isNotEmpty() } @@ -664,31 +491,24 @@ object SessionPrefs { } } - /** An added game's name as the user set it (in the app, or in Steam's Properties); "" = automatic. */ fun addedGameName(context: Context, folderPath: String): String = prefs(context).getString("addedName:$folderPath", "") ?: "" fun setAddedGameName(context: Context, folderPath: String, name: String) { prefs(context).edit().putString("addedName:$folderPath", name.trim()).apply() } - /** An added game's Start in, as the session sees it; "" = the exe's own folder. */ fun addedGameStartIn(context: Context, folderPath: String): String = prefs(context).getString("addedStartIn:$folderPath", "") ?: "" fun setAddedGameStartIn(context: Context, folderPath: String, guestDir: String) { prefs(context).edit().putString("addedStartIn:$folderPath", guestDir.trim()).apply() } - /** An added game's launch options, as Steam takes them ("-dx11", "VAR=1 %command%"). */ fun addedGameLaunch(context: Context, folderPath: String): String = prefs(context).getString("addedLaunch:$folderPath", "") ?: "" fun setAddedGameLaunch(context: Context, folderPath: String, options: String) { prefs(context).edit().putString("addedLaunch:$folderPath", options.trim()).apply() } - /** - * The last value of [field] (AppName, StartDir, LaunchOptions) taken over from Steam's own - * Properties for this game, so the same Steam edit is not taken again over a later app edit. - */ fun addedGameAdopted(context: Context, folderPath: String, field: String): String? = prefs(context).getString("addedAdopted:$field:$folderPath", null) @@ -696,7 +516,6 @@ object SessionPrefs { prefs(context).edit().putString("addedAdopted:$field:$folderPath", value).apply() } - /** Where an added game's art for [slot] comes from when the user chose it ("folder", "steam", "sgdb", "file"); "" = automatic. */ fun addedGameArtSource(context: Context, folderPath: String, slot: String): String = prefs(context).getString("addedArt:$slot:$folderPath", "") ?: "" @@ -704,14 +523,12 @@ object SessionPrefs { prefs(context).edit().putString("addedArt:$slot:$folderPath", source).apply() } - /** Whether added games without art of their own get Steam's store art fetched for them. */ fun addedGamesArt(context: Context): Boolean = prefs(context).getBoolean("addedGamesArt", true) fun setAddedGamesArt(context: Context, on: Boolean) { prefs(context).edit().putBoolean("addedGamesArt", on).apply() } - /** The .exe the user chose for one game folder (by its path), "" = the scanner's pick. */ fun addedGameExe(context: Context, folderPath: String): String = prefs(context).getString("addedExe:$folderPath", "") ?: "" private val addedGameLock = Any() @@ -729,7 +546,6 @@ object SessionPrefs { unchanged } - /** The shortcut appid stored for the folder, or null before its first scan. */ fun addedGameStoredAppId(context: Context, folderPath: String): Long? = prefs(context).getLong("addedAppId:$folderPath", 0L).takeIf { it != 0L } @@ -739,21 +555,15 @@ object SessionPrefs { ?: first.also { p.edit().putLong("addedAppId:$folderPath", it).apply() } } - /** The app's colour theme (ui/Themes ids); Graphite unless chosen otherwise. */ fun theme(context: Context): String = prefs(context).getString("theme", "graphite") ?: "graphite" fun setTheme(context: Context, id: String) { prefs(context).edit().putString("theme", id).apply() } - /** - * A fixed size for the session's display, per mode, or null. When set it replaces both the - * cap and the shape: the compositor fits it to the panel with bars where the shapes differ. - */ private fun customResolution(context: Context, mode: String): Pair<Int, Int>? = parseResolution(prefs(context).getString("customRes.$mode", null)) - /** "1024x768" (or ×, or *) to an even size inside 320x240..3840x2160; anything else is null. */ fun parseResolution(text: String?): Pair<Int, Int>? { val parts = text?.trim()?.split('x', 'X', '×', '*')?.map { it.trim() } ?: return null if (parts.size != 2) return null @@ -763,25 +573,12 @@ object SessionPrefs { return Pair(w and 1.inv(), h and 1.inv()) } - /** - * HDR10 output for MODE_STEAM / MODE_DESKTOP. Off by default. Honoured only when the panel - * lists HDR10 (HdrSupport), and decided when the compositor starts, which is once per app - * process: a change applies after the app is fully closed and opened again. - */ fun hdr(context: Context, mode: String): Boolean = prefs(context).getBoolean("hdr.$mode", false) fun setHdr(context: Context, mode: String, on: Boolean) { prefs(context).edit().putBoolean("hdr.$mode", on).apply() } - /** - * The session's frame cap, 0 for none. One number used everywhere a frame is paced: gamescope's - * -r (what the client and its games see as the display's rate), the compositor's buffer release - * pacer, the rate the display layer votes for, and the panel mode picked, which is the fastest - * one the cap divides evenly (40 on a 120 Hz panel, not on a 144 Hz one). A 60 fps cap on a - * 144 Hz panel with nothing else changed judders; this is what WinNative's per-shortcut limit - * does. Applies next session. - */ fun fpsLimit(context: Context, mode: String): Int = prefs(context).getInt("fpsLimit.$mode", 0) fun setFpsLimit(context: Context, mode: String, fps: Int) { @@ -790,28 +587,19 @@ object SessionPrefs { val fpsLimitChoices = listOf(0 to "Off", 30 to "30", 40 to "40", 45 to "45", 60 to "60", 90 to "90", 120 to "120") - /** [fpsLimitChoices] labelled in the app's language. */ fun fpsLimitChoices(context: Context): List<Pair<Int, String>> = fpsLimitChoices.map { (fps, label) -> fps to if (fps == 0) context.getString(R.string.frame_gen_off) else label } - /** - * How the compositor resizes the session onto the panel (WaylandCompositor.nativeSetUpscaler's - * modes). Linear is the default; Nearest preserves hard pixel edges. Spatial filters work - * when enlarged; Sharpen only works at any size. The old Off/Linear and FSR/FSR Fit pairs - * are equivalent on Wayland, so saved aliases resolve to one choice. - */ val upscalerChoices = listOf( 0 to "Linear", 2 to "Nearest", 4 to "AMD FSR 1", 3 to "Snapdragon GSR", 8 to "Snapdragon GSR (quality)", 7 to "NVIDIA NIS", 6 to "Sharpen only", ) - /** The [upscalerChoices] labels that are words rather than product names. */ private val upscalerLabels = mapOf( 0 to R.string.sprefs_upscaler_linear, 2 to R.string.sprefs_upscaler_nearest, 8 to R.string.sprefs_upscaler_gsr_quality, 6 to R.string.sprefs_upscaler_sharpen, ) - /** [upscalerChoices] labelled in the app's language; the English list stays for the device report. */ fun upscalerChoices(context: Context): List<Pair<Int, String>> = upscalerChoices.map { (mode, label) -> mode to (upscalerLabels[mode]?.let(context::getString) ?: label) } @@ -835,9 +623,7 @@ object SessionPrefs { prefs(context).edit().putInt("upscaleSharpness", pct.coerceIn(0, 100)).apply() } - // ── Screen effects and texture filtering (the Display page) ───────────────────────────── - /** The compositor's post chain as last set; off until the user picks a Look or moves a row. */ fun screenEffects(context: Context): ScreenEffects { val text = prefs(context).getString("screenEffects", null) ?: return ScreenEffects.OFF return runCatching { ScreenEffects.decode(JSONObject(text)) }.getOrDefault(ScreenEffects.OFF) @@ -847,10 +633,8 @@ object SessionPrefs { prefs(context).edit().putString("screenEffects", effects.encode().toString()).apply() } - /** The key [cardboard] lives under; SessionActivity watches it so a change applies live. */ const val CARDBOARD_KEY = "cardboard" - /** Cardboard mode (gpu/Cardboard) as last set; off by default. */ fun cardboard(context: Context): Cardboard { val text = prefs(context).getString(CARDBOARD_KEY, null) ?: return Cardboard.OFF return runCatching { Cardboard.decode(JSONObject(text)) }.getOrDefault(Cardboard.OFF) @@ -860,21 +644,16 @@ object SessionPrefs { prefs(context).edit().putString(CARDBOARD_KEY, cardboard.encode().toString()).apply() } - /** The Cardboard picture (Cardboard.SOURCE_*) to go back to once SteamVR stops; -1 = none (it was - * not switched for SteamVR). Kept across activity restarts, so a SteamVR session that outlives the - * activity still hands the picture back. */ fun cardboardSourceBeforeSteamVr(context: Context): Int = prefs(context).getInt("cardboardSourceBeforeSteamVr", -1) fun setCardboardSourceBeforeSteamVr(context: Context, source: Int) { prefs(context).edit().putInt("cardboardSourceBeforeSteamVr", source).apply() } - /** The prefs file [cardboard] lives in, for a change listener. */ fun sessionPrefs(context: Context): android.content.SharedPreferences = prefs(context) val textureAnisotropyChoices = TextureFiltering.ANISOTROPY.map { it to if (it == 0) "Off" else "${it}x" } - /** [textureAnisotropyChoices] labelled in the app's language. */ fun textureAnisotropyChoices(context: Context): List<Pair<Int, String>> = textureAnisotropyChoices.map { (value, label) -> value to if (value == 0) context.getString(R.string.frame_gen_off) else label } @@ -886,7 +665,6 @@ object SessionPrefs { } } - /** [textureLodBiasChoices] labelled in the app's language. */ fun textureLodBiasChoices(context: Context): List<Pair<String, String>> = textureLodBiasChoices.map { (value, label) -> value to when (value) { TextureFiltering.LOD_BIAS_OFF -> context.getString(R.string.frame_gen_off) @@ -895,7 +673,6 @@ object SessionPrefs { } } - /** Anisotropic filtering forced on DirectX 9-11 games (core/TextureFiltering); 0 = the game's own. */ fun textureAnisotropy(context: Context): Int = prefs(context).getInt("textureAnisotropy", 0).takeIf { it in TextureFiltering.ANISOTROPY } ?: 0 @@ -904,7 +681,6 @@ object SessionPrefs { publishGameEnvironment(context) } - /** Texture sharpness: the mip LOD bias choice (TextureFiltering.LOD_BIAS), "0" = the game's own. */ fun textureLodBias(context: Context): String = prefs(context).getString("textureLodBias", null)?.takeIf { it in TextureFiltering.LOD_BIAS } ?: TextureFiltering.LOD_BIAS_OFF @@ -981,16 +757,11 @@ object SessionPrefs { prefs(context).edit().putString("gameTextureFiltering", json.toString()).apply() } - /** Hands the change to the next game launch (GameEnvironmentStore); the running game keeps its own. */ private fun publishGameEnvironment(context: Context) { runCatching { GameEnvironmentStore.publish(context) } .onFailure { android.util.Log.e("GameEnvironment", "Could not update game environment", it) } } - /** - * The mode whose per-mode settings apply: a program run under gamescope (MODE_RUN) is a - * fullscreen session like Steam's, so it takes Steam's display, driver and HDR choices. - */ fun prefMode(mode: String): String = if (mode == SessionService.MODE_RUN) SessionService.MODE_STEAM else mode fun pipAutoEnter(context: Context): Boolean = prefs(context).getBoolean("pipAutoEnter", false) @@ -999,7 +770,6 @@ object SessionPrefs { prefs(context).edit().putBoolean("pipAutoEnter", enabled).apply() } - /** A foldable's unfolded panel shows the game above the Deck controls (SessionDisplay.unfoldedGameHeight). */ fun unfoldedControls(context: Context): Boolean = prefs(context).getBoolean("unfoldedControls", false) fun setUnfoldedControls(context: Context, enabled: Boolean) { @@ -1009,8 +779,6 @@ object SessionPrefs { const val SURFACE_STICK = "stick" const val SURFACE_TRACKPAD = "trackpad" - /** The Deck controls' gamepad tab: what each side is - an analog stick or a Steam trackpad. - * Left a stick and right a trackpad until changed, as a Deck is mostly played. */ fun gamepadSurface(context: Context, right: Boolean): String = prefs(context).getString(if (right) "gamepadRight" else "gamepadLeft", null) ?: if (right) SURFACE_TRACKPAD else SURFACE_STICK @@ -1023,7 +791,6 @@ object SessionPrefs { prefs(context).getString("suspendPolicy.${prefMode(mode)}", SUSPEND_MANUAL) ?.let { if (it == LEGACY_SUSPEND_DOWNLOADS) SUSPEND_AUTO else it } ?.takeIf { it in setOf(SUSPEND_AUTO, SUSPEND_NATIVE, SUSPEND_MANUAL, SUSPEND_NEVER) } - // Direct games share Steam's settings but have no Steam client to prepare. ?.let { if (mode != SessionService.MODE_STEAM && it == SUSPEND_NATIVE) SUSPEND_AUTO else it } ?: SUSPEND_MANUAL @@ -1058,7 +825,6 @@ object SessionPrefs { }.apply() } - /** Steam only: the client's memory goes to the device's zram while a game runs (ZramSupport). */ fun zramCompression(context: Context): Boolean { val prefs = prefs(context) return if (prefs.contains("zramCompression")) prefs.getBoolean("zramCompression", false) else ZramSupport.recommended(context) @@ -1068,15 +834,7 @@ object SessionPrefs { prefs(context).edit().putBoolean("zramCompression", on).apply() } - // ── Game storage ──────────────────────────────────────────────────────────────────────── - /** - * A second Steam library on this device: the folder bound at /mnt/droiddeck-sd and - * registered with the client, which then asks where to install every game and shows both - * on its Storage page. "" = automatic: the SD card when one is in the phone (the default, - * so the choice is made inside the client like anywhere else); GAME_STORAGE_OFF = internal - * only; otherwise the folder chosen. - */ fun gameStorage(context: Context): String = prefs(context).getString("gameStorage", "") ?: "" const val GAME_STORAGE_OFF = "off" diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt index f068b8f26..1de3c745c 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -50,32 +50,16 @@ import java.util.Date import java.util.Locale import java.util.UUID -/** - * Owns the running session: the proot tree, the audio daemon, the network link and the locks that - * keep all three alive while the app is not on screen. - * - * The session deliberately does **not** belong to the activity. Android demotes a process the - * moment it loses its last visible activity, and the low-memory killer then reaps the guest - so - * leaving Big Picture to answer a message would come back to a dead Steam. A foreground service - * holds the process at perceptible priority, a partial wake lock keeps the CPU from dropping the - * guest's threads, and a high-performance WiFi lock keeps the radio out of power-save so a - * backgrounded download does not throttle to nothing. All three are Bannerlator's recipe, where - * each was added to fix a failure seen on a device. - * - * The activity comes and goes on top of this; see [com.droiddeck.launcher.wayland.CompositorHost]. - */ class SessionService : Service() { override fun attachBaseContext(newBase: android.content.Context) = super.attachBaseContext(com.droiddeck.launcher.core.AppLanguage.wrap(newBase)) private val components = java.util.concurrent.CopyOnWriteArrayList<SessionPart>() - /** The Steam Deck controller's sysfs binds (SteamDeckPad), when this session has one. */ private var deckBinds: List<String> = emptyList() private val stopLock = Any() private var wakeLock: PowerManager.WakeLock? = null private var wifiLock: WifiManager.WifiLock? = null private var sessionPid = -1 - /** Auxiliary PTY-backed proot shells launched from a secondary-display terminal. */ private val auxiliaryProcesses = HashMap<Int, Long>() private val auxiliaryProcessesLock = Any() private val mainHandler = Handler(Looper.getMainLooper()) @@ -89,7 +73,6 @@ class SessionService : Service() { private var activityVisible = true private var screenOn = true private var manualPauseRequested = false - /** An explicit Steam sleep request is independent of the background policy. */ private var steamSleepToken: String? = null private var nativeSleepToken: String? = null private var nativeSleepReady = false @@ -98,7 +81,6 @@ class SessionService : Service() { private var suspendOperationPending = false private var pipTask = false private var suspendAttemptFailed = false - /** One validated game request waiting for this Steam session to reach a usable state. */ private var pendingSteamGameId: String? = null private var pendingSteamGameGeneration = -1 private val flushSteamGame = object : Runnable { @@ -117,7 +99,6 @@ class SessionService : Service() { updateSuspendPolicy() } } - /** Counts sessions this service has started; a process exit from an earlier one is ignored. */ @Volatile private var sessionGen = 0 override fun onCreate() { @@ -246,47 +227,26 @@ class SessionService : Service() { stopSession(0) return START_NOT_STICKY } - // Another Steam client on the device signs ours out seconds after every login; the one that - // does it here runs from boot without being opened. Only the Steam session signs in. - // The desktop too: its Steam launchers run the client right there (droiddeck-steam-launch). if (SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP) RivalClients.stopBeforeSession(this) SessionState.firstFrameSeen = false SessionState.guestPid = -1 SessionEvents.transition(SessionPhase.STARTING_GUEST, "service.started", mapOf("mode" to SessionState.mode)) - // Re-arm after clearing readiness: a retained picture can present between the activity's - // start request and this callback, and that notice must not be erased by the reset above. com.droiddeck.launcher.wayland.CompositorHost.newSession() - // This session's number, claimed here and not when its process starts: the session it - // replaces can report its own exit in the gap between the two, and that exit must not - // be taken as this one's. val gen = ++sessionGen cancelQueuedSteamGame(clearMailbox = false) clearSteamGameMailbox() acquireLocks() Thread({ - // A tree the last session left behind (the app was killed or crashed, so its teardown - // never ran) would hold the rootfs, the GPU and Steam's lock: nothing of ours should - // be alive between sessions outside this process. OrphanReaper.reap("session starting") runSession(gen) }, "session-start").start() - // The activity or the notification stops us; the system must not resurrect a session whose - // guest processes are long gone. return START_NOT_STICKY } - /** - * Finish the session's folder after the session has stopped, then let go of it. The guest - * script copies Steam's logs too, at a clean exit; this runs whatever killed the session, - * which is when they matter. The collecting itself is [SessionArtifacts], shared with the - * crash handler and the next-start sweep so a folder is finished whichever way it ends. - */ private fun collectSessionArtifacts(dir: File) { try { SessionArtifacts.collect(this, dir, "session stopped") } finally { - // Last, so everything above is in the file it is about - and only this session's: - // a session that replaced this one may already own the capture and the folder. SessionLogCapture.stopFor(dir) SessionPaths.release(this, dir) } @@ -315,7 +275,6 @@ class SessionService : Service() { return if (name.contains("710-720") || name.contains("710_720")) "sysmem" else null } - // ── The session ───────────────────────────────────────────────────────────────────────── private fun runSession(gen: Int) { SessionTerminal.clear() @@ -333,7 +292,6 @@ class SessionService : Service() { killStragglers() SessionFiles.stage(this, root) com.droiddeck.launcher.agent.AgentGuest.reset(this) - // The compat tool asks for an Epic game's sign-in code at every launch, the client's Play button included. com.droiddeck.launcher.stores.gog.GogComet.begin() com.droiddeck.launcher.stores.StoreLaunchRequests.start(this) @@ -344,31 +302,21 @@ class SessionService : Service() { val size = SessionState.outputSize val guest = ArrayList<String>() - // The desktop's Steam launchers start the client there (droiddeck-steam-launch), through the - // same set-up as a Steam session: it gets what the client and its games are started with. val steamHere = SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP - // Some GPUs need a component Proton does not ship. Apply those before the guest, so the - // next game launch copies them into the prefix. A download failure is logged and the - // session still starts; the next session tries again. if (steamHere) { runCatching { ComponentsManager.ensureForcedPackages(this) } .onSuccess { if (it != null) Log.i(TAG, it) } .onFailure { Log.w(TAG, "forced components", it) } } addClientEnvironment(guest, steamHere) - // Where the fast path's description of proot's view goes, once the binds are known. val fastPathAt = guest.size val pulse = startAudio(guest, sessionDir) guest.add("BL_WIDTH=" + size.first) guest.add("BL_HEIGHT=" + size.second) - // Follow screen: the output is resized under gamescope when a foldable opens or closes, - // and the session script asks gamescope to resize Steam's X screen along with it. if (SessionState.followScreen) guest.add("BL_FOLLOW_OUTPUT=1") if (SessionState.hdr) { - // The activity opened the compositor's HDR gate: gamescope offers HDR to its clients - // and DXVK takes the HDR10 swapchain when a game asks for one. guest.add("BL_HDR=1") guest.add("DXVK_HDR=1") Log.i(TAG, "hdr: gamescope --hdr-enabled, DXVK_HDR=1") @@ -384,12 +332,8 @@ class SessionService : Service() { deckBinds = emptyList() if (controllersOn) { addControllerEnvironment(guest, fakeInputDir, sessionDir) - // Steam enumerates /dev/input while it starts. Make every attached controller's - // discovery node exist after the rings and guest environment are ready. SessionState.padBridge?.syncConnectedControllers() } - // The user's own games, for the runtime's shortcuts writer to put in the client's library - // before the client starts (see frontend/AddedGames and droiddeck-steam-shortcuts). if (steamHere) { val added = com.droiddeck.launcher.frontend.AddedGames.scan(this) val listing = com.droiddeck.launcher.frontend.AddedGames.writeListing(this, added) @@ -398,9 +342,7 @@ class SessionService : Service() { if (com.droiddeck.launcher.gpu.Lossless.owned(this)) guest.add("BL_LOSSLESS_OWNED=1") if (added.isNotEmpty()) Log.i(TAG, "added games: " + added.joinToString { "${it.name} (${it.exe.name})" }) } - // Where the guest leaves a request for another session (the desktop's Steam launchers). guest.add("BL_LAUNCH_DIR=" + sessionRoot.path) - // The second library's name, for droiddeck-steam-library; the bind itself is made below. GameStorage.effective(this)?.let { guest.add("BL_LIBRARY_LABEL=" + it.label.replace('"', ' ')) } if (SessionState.mode == MODE_STEAM && SessionState.steamUi == "desktop") guest.add("BL_STEAM_UI=desktop") val shellGuest = ArrayList(guest).apply { @@ -416,13 +358,7 @@ class SessionService : Service() { guest.add(it) Log.i(TAG, "steam: handing the client $it") } - // A game whose download finishes in this session gets its Windows components readied in it. if (SessionState.mode == MODE_STEAM) com.droiddeck.launcher.frontend.SessionInstalls.watch(this) - // A program under gamescope: the script's run mode takes the path (an AppImage, a script - // or a binary inside the runtime). This is how an emulator gets the GPU - the desktop's - // KWin composites in software and offers no dma-buf, so a Vulkan swapchain cannot exist - // there (RPCS3 died with VK_ERROR_SURFACE_LOST); gamescope's Xwayland is the path the - // Steam games already render through. if (SessionState.mode == MODE_RUN) { val program = SessionState.program if (program.isNullOrEmpty()) { @@ -435,12 +371,9 @@ class SessionService : Service() { Log.i(TAG, "run: $program (${SessionState.programArgs.size} arguments) under gamescope") } - // Android has no /dev/shm; the cache stands in for it and, unlike the real thing, keeps - // whatever a session leaves behind. The client abandons tens of megabytes of streams a run. FileUtils.clear(File(cacheDir, "shm")) val binds = sessionBinds(controllersOn, fakeInputDir, sessionDir, guest, sessionRoot, runtimeDir) - // The fast path is told exactly the rootfs and binds proot is given (ProotFastPath). val fastPathKey = if (ProotFastPath.enabled(this)) { val prootBinds = LinuxRuntime.binds( this, sessionRoot, runtimeDir, Environment.getExternalStorageDirectory(), binds, @@ -464,10 +397,6 @@ class SessionService : Service() { val hostEnv = HostEnvironment() hostEnv["PROOT_LOADER"] = LinuxRuntime.prootLoader(this).path hostEnv["PROOT_TMP_DIR"] = cacheDir.path - // proot links against a libtalloc beside it, and Android's linker does not search an - // executable's own directory: unnamed, the process dies before it starts and says so only - // in `logcat -b crash`. - // proot reads this itself, so it belongs in proot's own environment rather than the guest's. if (SessionPrefs.prootNoSeccomp(this)) { hostEnv["PROOT_NO_SECCOMP"] = "1" Log.i(TAG, "proot: seccomp acceleration off by request") @@ -488,8 +417,6 @@ class SessionService : Service() { SessionTerminal.prepare(terminalCommand, terminalHostEnvironment.map { (key, value) -> "$key=$value" }.toTypedArray(), root, gen) } - // Whether the client signs in to Valve or starts offline: read once, while it starts, and - // rewritten by the client when it exits, so it is set again here at every session start. if (SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP) OfflineMode.apply(this, root) val networkLink = LinuxNetworkLinkComponent(this, root) @@ -509,10 +436,6 @@ class SessionService : Service() { watchLaunchRequests(sessionRoot) watchSyncWanted(root) EsyncPacks.fetchInBackground(this, root) - // One session replacing another (the desktop's Steam launchers): the old proot is killed - // by the teardown a second after the new one has started, and its exit used to arrive - // here as "session ended: 137" and end the NEW session. An exit belongs to the session - // that started it. val pid = HostProcess.start(line, hostEnv.asArray(), root, { status -> if (gen != sessionGen) { Log.i(TAG, "an earlier session's process ended ($status); the current one carries on") @@ -561,10 +484,7 @@ class SessionService : Service() { } } - /** The session's own log folder, opened and filled with what is known before anything starts. */ private fun openSessionFolder(): File { - // One folder per session, claimed by whoever started first - the activity starts the - // compositor before this service runs - so the compositor's log lands in the same place. val sessionDir = SessionPaths.beginOrCurrent(this) val sessionLog = File(sessionDir, "session.log") SessionState.logFile = sessionLog @@ -572,155 +492,91 @@ class SessionService : Service() { SessionState.sessionId = sessionDir.name SessionState.eventsFile = File(sessionDir, "events.jsonl") SessionEvents.record("session.logs_ready", mapOf("logDir" to sessionDir.path)) - // Written first, so a session that dies in its first second still says what it ran on. DeviceReport.write(this, File(sessionDir, "device.txt"), SessionState.mode) NetworkReport.write(this, File(sessionDir, "network.txt")) - // Everything the app decides from here on - the driver it chose, the audio line, a rival - // client stopped, the exit status - reaches logcat and nowhere a user can get at. Mirror it. SessionLogCapture.start(File(sessionDir, "app.log")) return sessionDir } - /** The guest's base environment: paths, the display, the GL/Vulkan stack and the client's switches. */ private fun addClientEnvironment(guest: MutableList<String>, steamHere: Boolean) { guest.add("/usr/bin/env") guest.add("-i") - // PRoot executes guest[0] directly; assignments belong after env -i. guest.add("DROIDDECK_BWRAP_SOCKET=" + com.droiddeck.launcher.runtime.BwrapSpawner.socket(this)) guest.add("HOME=/root") guest.add("USER=root") guest.add("PATH=/usr/local/bin:/usr/bin:/bin") guest.add("TERM=xterm-256color") guest.add("LANG=C.UTF-8") - // Steam's interface language: the app's (Setup's choice, or the system's). The system's - // region decides between Spain's and Latin American Spanish, which the app has one of. guest.add("BL_STEAM_LANGUAGE=" + SteamLanguage.forLocale( com.droiddeck.launcher.core.AppLanguage.effective(this), com.droiddeck.launcher.core.AppLanguage.system(this).country, )) - // Without this the session is UTC: the client's clock, its logs and every timestamp in a - // session bundle sit hours off the device's. Bannerlator carries the same line. guest.add("TZ=" + java.util.TimeZone.getDefault().id) guest.add("XDG_RUNTIME_DIR=" + LinuxRuntime.GUEST_RUNTIME_DIR) guest.add("XDG_SESSION_TYPE=wayland") guest.add("WAYLAND_DISPLAY=wayland-0") guest.add("BL_ANDROID_CLIPBOARD=" + File(filesDir, "session/android-clipboard").path) guest.add("GAMESCOPE_FORCE_GENERAL_QUEUE=1") - // Steam's CEF needs GL and the rootfs ships no native GL driver: route it through Zink. guest.add("MESA_LOADER_DRIVER_OVERRIDE=zink") guest.add("GALLIUM_DRIVER=zink") guest.add("LIBGL_KOPPER_DRI2=true") LinuxRuntime.vulkanIcd(this)?.let { guest.add("VK_ICD_FILENAMES=" + it.path) } - // An imported glibc Turnip, when one is set (by the user, or by Auto): the session script - // checks the manifest and its library from inside and points the loader at it with - // VK_DRIVER_FILES, so the runtime's own driver above stays untouched and is what a bad - // import falls back to. One driver for every session: the driver's shader cache is keyed on - // its build, and with one per mode every emulator compiled its shaders twice. val linuxDriverId = SessionPrefs.linuxDriver(this) LinuxVulkanDriver.resolveIcdPath(this, linuxDriverId) ?.let { guest.add(LinuxVulkanDriver.ENV + "=" + it) } - // Turnip's own debug switches, for the runtime's driver and everything on it. The file in - // Downloads holds the value verbatim ("sysmem", "sysmem,deck_emu"); with nothing there, an - // imported driver from the A710/A720/A722 legs gets "sysmem" on its own, which is what both - // its authors advise for those GPUs and what nothing else in the list needs. tuDebug(linuxDriverId)?.let { guest.add("TU_DEBUG=$it") } - // Zink renders the client's UI (Chromium -> ANGLE -> Zink -> Turnip). Lazy descriptors is - // the mode Zink recommends where the driver has no descriptor buffer, and what Ludashi ships - // by default for its Zink path; a switch here because on one Fold the menus run at 14 fps. - // compact packs Zink's descriptor sets into fewer, so a draw binds less (WinNative's default). if (SessionPrefs.zinkLazy(this)) { guest.add("ZINK_DESCRIPTORS=lazy") guest.add("ZINK_DEBUG=compact") } - // The rest of the client-interface switches (SessionPrefs): GL marshalled off the calling - // thread and no GL error checks. if (SessionPrefs.glThread(this)) guest.add("mesa_glthread=true") if (SessionPrefs.noGlError(this)) guest.add("MESA_NO_ERROR=1") - // Mesa's shader cache as one database instead of a file per entry. Every lookup in the - // file cache is an open and every store an open and a rename, each a proot stop (a rename - // two), for every pipeline DXVK, vkd3d-proton and Zink compile or find; the database is - // opened once and read and written with pread/pwrite, which proot never sees. Mesa removes - // the old folder itself once it has gone a week untouched. guest.add("MESA_DISK_CACHE_DATABASE=1") - // glibc's per-thread rseq registration is refused by Android's app seccomp policy, so - // every thread start paid a SIGSYS that proot answers; the malloc top pad grows the heap - // 16 MB at a time instead of 128 KB, and every brk(2) is a proot stop too. guest.add("GLIBC_TUNABLES=glibc.pthread.rseq=0:glibc.malloc.top_pad=16777216") if (SessionState.mode == MODE_STEAM) guest.add("BL_MANGOAPP=" + (if (SessionPrefs.mangoapp(this)) "1" else "0")) if (SessionState.mode == MODE_STEAM) { guest.add("BL_GAMESCOPE_STRETCH_16X9=" + (if (SessionPrefs.stretch16x9(this)) "1" else "0")) } - // Proton's own gate for its xalia helper (its `proton` script reads this, and sets - // XALIA_SUPPORTED_ONLY itself otherwise). Skipped by default: under FEX it costs every game - // a slice of a core for gamepad navigation the session already has. if (SessionPrefs.noXalia(this)) guest.add("PROTON_USE_XALIA=0") guest.add("BL_SYNC=" + (if (SessionPrefs.fastSync(this)) "1" else "0")) guest.add("BL_SYNC_FALLBACK=" + (if (SessionPrefs.syncFallback(this)) "1" else "0")) guest.add("BL_FSYNC_FIRST=" + (if (SessionPrefs.fsyncFirst(this)) "1" else "0")) - // gamescope's realtime Vulkan queues (the session script turns this into - // GAMESCOPE_FORCE_VULKAN_REALTIME); off unless the user turns it on. guest.add("BL_GAMESCOPE_REALTIME=" + (if (SessionPrefs.gamescopeRealtime(this)) "1" else "0")) if (SessionState.mode == MODE_STEAM && SessionPrefs.zramCompression(this) && com.droiddeck.launcher.core.ZramSupport.supported()) guest.add("BL_ZRAM=1") guest.add("BANNER_AUDIO_DIRECT_DECAY=0") - // Anything else, for a device that cannot be reached with a debugger: droiddeck-env in the - // app's own external files (envSwitchFile) holds KEY=VALUE lines that go into the session's - // environment as written, after ours, so a line here wins. Zink and Turnip tunables - // (ZINK_DESCRIPTORS=lazy, MESA_*), gamescope's, the client's - whatever the experiment - // needs, without a build per attempt. extraEnv().forEach { guest.add(it) } - // The agent bridge's experiment lines (AgentEnv), after the user's so an agent's win. com.droiddeck.launcher.agent.AgentEnv.beginSession(this).forEach { guest.add(it) } - // A game launched by pinned Steam inherits its mask. Explicitly restore all game cores - // when requested, as well as applying restricted game choices in the Proton wrapper. val clientPinned = steamHere && SessionPrefs.clientCpusOverride(this) if (clientPinned) { guest.add("BL_CLIENT_CPUS=" + CpuCores.listOrAll(SessionPrefs.clientCpus(this))) } - // The game cores also pin the desktop and a program from the rail; without a choice the - // session script picks every core but the slowest cluster for those (program_cores). CpuCores.gameLaunchList(SessionPrefs.gameCpus(this), clientPinned) .takeIf { it.isNotEmpty() }?.let { guest.add("BL_GAME_CPUS=$it") } } - /** PulseAudio, and the DirectAudio relay when a path needs it; returns the daemon for suspend and resume. */ private fun startAudio(guest: MutableList<String>, sessionDir: File): PulseAudioComponent { - // PulseAudio always: the client is a native Linux program and has no other way to make a - // sound - its menus, its music and its voice chat all go through here. DirectAudio is not - // an alternative to it on this path: it replaces the audio driver INSIDE Wine, so it - // changes what games do and leaves the client alone. The microphone is its own opt-in on - // top, and the helper only opens an input stream when asked - so a user who wants game - // sound but no recording gets exactly that, and Android's recording indicator stays off. - // The desktop starts Windows programs through the same Proton wrappers (droiddeck-proton-run). val wantsDirectAudio = (SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP) && SessionPrefs.directAudio(this) val wantsMic = SessionPrefs.micEnabled(this) && checkSelfPermission(android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED - // Both paths sit under the app's files directory, which the session binds at its own path, - // so the same string is valid on both sides and nothing has to be translated. val audioDir = File(filesDir, "directaudio").apply { mkdirs() } val relaySocket = File(audioDir, "relay.sock") val micFifo = if (wantsMic) File(audioDir, "mic.fifo") else null val audioLog = File(sessionDir, "audio.log") val pulse = PulseAudioComponent(this, micFifo?.absolutePath) - // The client's own sound: DirectAudio's engine inside the daemon, one step from Android (the - // daemon runs on the Android side). The relay below is for games and the microphone only. pulse.setLogFile(audioLog) pulse.attach(this) guest.add("PULSE_SERVER=unix:" + pulse.socket().absolutePath) components.add(pulse) if (wantsDirectAudio || wantsMic) { - // After the daemon in the list, so it can wait for the pipe the daemon makes. val relay = DirectAudioRelayComponent(relaySocket, micFifo) relay.setLogFile(audioLog) relay.attach(this) components.add(relay) } if (wantsDirectAudio) { - // Read by the Proton wrappers, which point Wine at the driver and name it in the - // prefix. Absent, they take an early return and the game uses Proton's own audio - so - // this variable is the whole of the selection. guest.add("BL_DIRECTAUDIO=/" + SessionFiles.DIRECTAUDIO_DIR) guest.add("BANNER_AUDIO_DIRECT_RELAY=" + relaySocket.absolutePath) } @@ -730,11 +586,8 @@ class SessionService : Service() { return pulse } - /** The fake evdev pads: the ring files the app writes and the identity SDL and Steam see. */ private fun addControllerEnvironment(guest: MutableList<String>, fakeInputDir: File, sessionDir: File) { FakeInputWriter.prepareRingSlots(fakeInputDir, 4) - // Virtual pads a client made last session (event16 and up, and their hidden rings) are - // not there any more; a client that crashed never took its own down. fakeInputDir.listFiles()?.forEach { file -> val node = Regex("event(\\d+)").matchEntire(file.name)?.groupValues?.get(1)?.toIntOrNull() if (file.name.startsWith(".uinput-") || (node != null && node >= FIRST_VIRTUAL_PAD)) file.delete() @@ -742,24 +595,13 @@ class SessionService : Service() { guest.add("FAKE_EVDEV_DIR=" + fakeInputDir.path) val rings = FakeInputWriter.getRingEnv(fakeInputDir) if (!rings.isNullOrEmpty()) guest.add("FAKE_EVDEV_MEMFD_PATHS=$rings") - // SDL and Steam key their mapping database on bus+vendor+product: only a known - // identity gets the standard layout without the user configuring the pad by hand. guest.add("FAKE_EVDEV_IDENTITY=xbox360") guest.add("FAKE_EVDEV_VIBRATION=1") val uinput = !File(Environment.getExternalStorageDirectory(), NO_UINPUT_SWITCH).exists() - // In Steam, the pad is what a Deck's is: a Steam Deck controller the client reads over - // hidraw (SteamDeckPad), which only works with Steam Input's virtual pad for games to read. - // Decided only once its sysfs is in place: without it the client would find no Deck, and - // with the Deck asked for the pad's own nodes are withdrawn - no controller at all. - // Set up whatever the pad is to be, so the drawer can switch it between a Deck, a Steam - // Controller and an Xbox 360 pad while the session runs (FakeInputWriter.setPadType). val wantsDeck = uinput && SessionState.mode == MODE_STEAM && SessionState.steamUi != "desktop" && !File(Environment.getExternalStorageDirectory(), NO_DECK_PAD_SWITCH).exists() - // Steam's touch controller rides on the Deck's sysfs and is offered only beside it. val wantsTouch = wantsDeck && !File(Environment.getExternalStorageDirectory(), NO_STEAM_TOUCH_SWITCH).exists() - // A paired Triton is exposed as a second native Steam Controller HID, with its original - // report IDs and output path, beside whatever the handheld's own pad is. val wantsTriton = wantsDeck && SteamControllerBle.hasPairedController(this) deckBinds = if (wantsDeck) SteamDeckPad.prepare(this, fakeInputDir.parentFile!!.parentFile!!, wantsTouch, wantsTriton, SessionPrefs.padType(SessionPrefs.steamController(this))) else emptyList() @@ -767,43 +609,29 @@ class SessionService : Service() { if (wantsDeck && !SessionState.deckPad) Log.w(TAG, "deck pad: not available this session; the pad stays an Xbox 360 controller") logControllersAtStart() if (uinput) { - // /dev/uinput, stood in for by libfakeinput: the virtual pad Steam Input makes for a - // game becomes a node the game reads, carrying the player's layout, as on a Deck. guest.add("FAKE_EVDEV_UINPUT=1") if (SessionState.deckPad) { guest.add("FAKE_EVDEV_DECK=1") if (wantsTriton) guest.add("FAKE_EVDEV_TRITON=1") - // What the pad starts as; the drawer's changes reach libfakeinput through the ring. val padType = SessionPrefs.steamController(this) guest.add("FAKE_EVDEV_PAD_TYPE=$padType") com.droiddeck.launcher.input.FakeInputWriter.setPadType(SessionPrefs.padType(padType)) - // Steam's touch controller (SteamTouchDevice): the file the app and libfakeinput share. if (wantsTouch) com.droiddeck.launcher.input.SteamTouchDevice.prepare(fakeInputDir)?.let { guest.add("FAKE_TOUCHCTL_RING=" + it.path) } guest.add("FAKE_DECK_SYSFS_LISTING=" + SteamDeckPad.listingDir(fakeInputDir.parentFile!!.parentFile!!).path) } } else if (SessionState.mode == MODE_STEAM) { - // Without it, games see the pad itself wearing Steam Input's virtual-gamepad identity - - // for games the client starts only. Everywhere else (the desktop, a program from the - // rail) that identity hides the pad: SDL ignores a Steam virtual gamepad unless it - // runs under Steam, so every SDL emulator came up with no controller. guest.add("FAKE_EVDEV_STEAM_VIRTUAL=1") } guest.add("SDL_JOYSTICK_DISABLE_UDEV=1") guest.add("SDL_HIDAPI_JOYSTICK_DISABLE_UDEV=1") - // The client reads a Deck controller through SDL's HIDAPI; a hint in the environment - // outranks the client's own. Nothing else is shown the Deck (libfakeinput). if (!SessionState.deckPad) guest.add("SDL_JOYSTICK_HIDAPI=0") - // The guest side of the pads (libfakeinput: which pads were opened, the Deck's hidraw and why - // it was refused) in pad.log beside the session's other logs, so it is in every shared zip. - // Setup-time lines only, nothing per input event. guest.add("FAKE_EVDEV_LOG=1") guest.add("FAKE_EVDEV_LOG_FILE=" + File(sessionDir, "pad.log").path) SessionState.fakeInputDir = fakeInputDir } - /** What proot binds into the guest: the pads, the battery, storage, the game library, added games and ROMs. */ private fun sessionBinds( controllersOn: Boolean, fakeInputDir: File, @@ -815,11 +643,6 @@ class SessionService : Service() { val binds = ArrayList<String>() if (controllersOn) binds.add(fakeInputDir.path + ":/dev/input") if (controllersOn) binds.addAll(deckBinds) - // The client's battery readout (the Quick Access Menu, the top bar) reads - // /sys/class/power_supply/BAT<n>/..., a laptop's or a Deck's naming; Android's supply is - // called "battery" and its files differ, so the client sees no battery at all. A directory - // of our own, written from Android's battery API every few seconds, is bound over it. - // Steam's time estimates come from /run/vpower instead, written straight into the rootfs. val battery = BatteryComponent( File(filesDir, "session/sys/power_supply"), File(LinuxRuntime.rootDir(this), "run/vpower"), @@ -827,55 +650,39 @@ class SessionService : Service() { battery.attach(this) components.add(battery) binds.add(battery.dir.path + ":/sys/class/power_supply") - // The overlay's CPU and GPU temperatures and the fan, as hwmon sensors it knows by name. val hwmon = HwmonComponent(File(filesDir, "session/sys/hwmon"), LinuxRuntime.rootDir(this)) if (hwmon.prepare()) { hwmon.attach(this) components.add(hwmon) binds.add(hwmon.dir.path + ":/sys/class/hwmon") } - // CPU load for everything in the session that reads /proc/stat, the overlay among them. val cpuStat = CpuStatComponent(File(filesDir, "session/proc-stat")) if (cpuStat.prepare()) { cpuStat.attach(this) components.add(cpuStat) binds.add(cpuStat.file.path + ":/proc/stat") } - // The GPU memory in use for the overlay's VRAM lines, which it would read from tracefs. val gpuMem = GpuMemComponent(File(LinuxRuntime.rootDir(this), "run/droiddeck-hud/gpu-mem"), LinuxRuntime.rootDir(this)) if (gpuMem.prepare()) { gpuMem.attach(this) components.add(gpuMem) } - // The GPU's load and temperature for the performance overlay, where KGSL's sysfs is refused. val gpuStats = GpuStatsComponent(File(filesDir, "session/sys/kgsl-3d0")) if (gpuStats.prepare()) { gpuStats.attach(this) components.add(gpuStats) binds.addAll(gpuStats.binds()) } - // Rumble for the on-screen pad: the fake evdev layer sends force-feedback effects to this - // listener, which drives the phone's vibrator (see RumbleComponent). if (controllersOn) components.add(RumbleComponent().also { it.attach(this) }) - // Where the device's files appear inside the session. Internal storage is bound at its own - // path already, and every program's file dialog opens at home and lists "Computer" from - // /proc/mounts, where a proot bind never shows - so a user saw only the runtime's own - // tree and could not find the phone at all. The same storage is placed under home as - // well, and the ROMs folder chosen on the main screen beside it; a bind rather than a - // link, so a folder on an SD card works the same. val home = File(LinuxRuntime.rootDir(this), "root") File(home, "Storage").mkdirs() binds.add(Environment.getExternalStorageDirectory().path + ":/root/Storage") - // A second Steam library: the storage chosen in the Steam cog, at the path the runtime's - // droiddeck-steam-library registers with the client. Nothing bound = the script removes - // the entry, so the client never offers a place that is not there. val library = GameStorage.effective(this) var storageDiagnosticLibrary: File? = null if (library != null) { val problem = GameStorage.prepare(this, library.path) if (problem == null) { File(LinuxRuntime.rootDir(this), "mnt/droiddeck-sd").mkdirs() - // Links, prefixes and Steam entries made before the rename still name the old path. File(LinuxRuntime.rootDir(this), "mnt/bannerlator-sd").mkdirs() try { binds.addAll(SecondaryLibrary.binds(filesDir, File(library.path))) @@ -900,8 +707,6 @@ class SessionService : Service() { } ?: false, ) if (device != null) { - // guest already has the session command at this point. Put these with the `env -i` - // assignments so they reach the Steam process instead of becoming script args. val envAt = guest.indexOf(LinuxRuntime.SESSION_SCRIPT).takeIf { it >= 0 } ?: guest.size guest.add(envAt, "BL_STORAGE_DIAGNOSTICS=1") guest.add(envAt + 1, "BL_STORAGE_DEVICE=$device") @@ -910,8 +715,6 @@ class SessionService : Service() { Log.w(TAG, "storage diagnostics: selected library could not be identified; metadata snapshot only") } } - // The user's own games folder (the Steam cog's "Added games"), bound at a fixed place so - // the shortcuts the app writes point somewhere whatever storage the folder is on. for (root in com.droiddeck.launcher.frontend.AddedGames.roots(this)) { if (root.host.isDirectory && root.host.canRead()) { File(LinuxRuntime.rootDir(this), root.guest.removePrefix("/")).mkdirs() @@ -921,8 +724,6 @@ class SessionService : Service() { Log.w(TAG, "added games: ${root.host} is not a readable folder this session") } } - // Store games installed on a card: each card's Games root at its own fixed place, so the - // shortcuts point somewhere whether or not the card is also the Steam library. for ((host, guest) in com.droiddeck.launcher.stores.StoreInstallRoot.externalRoots(this)) { if (host.isDirectory && host.canRead()) { File(LinuxRuntime.rootDir(this), guest.removePrefix("/")).mkdirs() @@ -930,7 +731,6 @@ class SessionService : Service() { Log.i(TAG, "store games: $host -> $guest") } } - // Folders of added scripts outside internal storage, where their links point. binds.addAll(com.droiddeck.launcher.runtime.UserApps.binds(this)) val roms = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() }?.let { File(it) } if (roms != null && roms.isDirectory && roms.canRead()) { @@ -987,7 +787,7 @@ class SessionService : Service() { if (!stillSameProcess()) return val tree = descendants(prootPid) if (!stillSameProcess()) return - android.os.Process.sendSignal(prootPid, 15) // SIGTERM + android.os.Process.sendSignal(prootPid, 15) if (!waitForExit(prootPid, GRACE_MS)) { if (!stillSameProcess()) return Log.w(TAG, "proot $prootPid did not exit on SIGTERM; killing it") @@ -995,15 +795,14 @@ class SessionService : Service() { } var killed = 0 for ((pid, started) in tree) { - val stat = readStat(pid) ?: continue // already gone - if (stat.second != started) continue // same number, different process + val stat = readStat(pid) ?: continue + if (stat.second != started) continue android.os.Process.killProcess(pid) killed++ } if (killed > 0) Log.i(TAG, "swept $killed process(es) proot left behind") } - /** Every process under [root], as (pid, start time), from a single walk of /proc. */ private fun descendants(root: Int): List<Pair<Int, Long>> { val started = HashMap<Int, Long>() val children = HashMap<Int, MutableList<Int>>() @@ -1027,7 +826,6 @@ class SessionService : Service() { return out } - /** (parent pid, start time) from /proc/pid/stat, or null when the process is gone. */ private fun readStat(pid: Int): Pair<Int, Long>? { val stat = try { File("/proc/$pid/stat").readText() } catch (e: Exception) { return null } val close = stat.lastIndexOf(')') @@ -1140,19 +938,6 @@ class SessionService : Service() { File(directory, STEAM_GAME_REQUEST + ".tmp").delete() } - /** - * proot's --kill-on-exit takes its tracees down, but a session that died from the inside - * (the client asserting, Xwayland going) leaves gamescopereaper and the session script - * behind, still holding the Wayland socket and the audio server the next session needs. They - * are our uid, so they are ours to kill. - */ - /** - * Every process of ours still running a program out of the Linux runtime once proot is gone. - * proot's --kill-on-exit and the tree sweep only reach what proot still traces; a tracee it - * lost - an Xwayland that aborted from one thread and then looped on a syscall proot's own - * seccomp filter, with no tracer left, answers ENOSYS - survives both, reparented to init, - * and wrote 9 GB of one line into the session log before anything killed it. - */ private fun killGuestLeftovers() { val me = android.os.Process.myPid() val rootfs = try { File(filesDir, "linuxfs").canonicalPath } catch (e: Exception) { return } + "/" @@ -1161,7 +946,6 @@ class SessionService : Service() { for (proc in procs) { val pid = proc.name.toIntOrNull() ?: continue if (pid == me) continue - // Another uid's exe is not ours to read; a process already gone has none. val exe = try { File(proc, "exe").canonicalPath } catch (e: Exception) { continue } if (!exe.startsWith(rootfs)) continue android.os.Process.killProcess(pid) @@ -1189,13 +973,6 @@ class SessionService : Service() { if (killed > 0) Log.w(TAG, "killed $killed leftover process(es) of a previous session") } - /** - * The desktop's Steam launchers cannot start the client where they are (no dma-buf on the - * desktop), so they leave `steam-launch` in the session directory instead: which UI, and a - * steam:// URL or nothing. This ends the session and hands the activity the one to start. - * `session=desktop` (Big Picture's "Switch to Desktop", steamos-session-select) asks for the - * desktop instead, with Steam's desktop client started in it. - */ private var launchWatcher: android.os.FileObserver? = null private fun watchLaunchRequests(dir: File) { @@ -1275,12 +1052,6 @@ class SessionService : Service() { private val desktopImports = java.util.concurrent.ConcurrentHashMap<String, Any>() - /** - * What the desktop cannot run where it lies (droiddeck-open), added as Add an app adds it, menu entry - * included: an AppImage of sharun's, whose hard links Android denies apps and only the app's own proot - * (link2symlink) makes, or a script or program whose folder of Linux programs is on noexec shared - * storage, copied in. The answer goes beside the request, "ok <folder>" or "error <why>". - */ private fun importForDesktop(dir: File, text: String) { val fields = text.lineSequence().mapNotNull { line -> line.split('=', limit = 2).takeIf { it.size == 2 }?.let { it[0] to it[1].trim() } @@ -1291,7 +1062,6 @@ class SessionService : Service() { val binds = LinuxRuntime.lastBinds(this).map { BwrapSpawner.Bind.parse(it) } val file = File(BwrapSpawner.hostPath(binds, LinuxRuntime.rootDir(this).path, guestPath)) val context = applicationContext - // A request for what is already being added waits for that, then finds it. val lock = desktopImports.computeIfAbsent(key) { Any() } Thread({ fun imported() = UserApps.desktopImport(context, key, guestPath)?.let { "ok $it" } @@ -1413,8 +1183,6 @@ class SessionService : Service() { private fun completeSteamSleep() { val token = steamSleepToken ?: return - // Write before SIGCONT: the guest's login1 sees it immediately on thaw - // and delivers PrepareForSleep(false), restoring Steam's main surface. writeSteamSleepState(token, "awake") steamSleepToken = null } @@ -1427,7 +1195,6 @@ class SessionService : Service() { } } - /** Let Steam prepare before freezing; retain the wake lock until that freeze completes. */ private fun prepareNativeSleep() { val token = UUID.randomUUID().toString().replace("-", "") nativeSleepToken = token @@ -1451,7 +1218,6 @@ class SessionService : Service() { updateSuspendPolicy() } nativeSleepTimeout = timeout - // A missing or stalled guest helper must not keep the device awake indefinitely. mainHandler.postDelayed(timeout, 4000L) } @@ -1468,7 +1234,6 @@ class SessionService : Service() { nativeSleepFallback = false } - /** Resume must also recover a Steam sleep whose request never reached Android. */ private fun requestSteamWake() { if (!SessionState.running || SessionState.mode != MODE_STEAM) return val dir = LinuxRuntime.sessionRoot(this) @@ -1491,8 +1256,6 @@ class SessionService : Service() { SessionState.guestPid = -1 com.droiddeck.launcher.agent.AgentGuest.stop() com.droiddeck.launcher.stores.StoreLaunchRequests.stop() - // Cloud saves of a store game played this session, if the compat tool's exit request did - // not upload them (a session closed from the drawer never reaches it). Thread({ com.droiddeck.launcher.stores.CloudSaves.uploadAllDirty(applicationContext, "session-end", running = false) }, "cloud-session-end").start() runCatching { com.droiddeck.launcher.agent.AgentEnv.endSession(this) } .onFailure { Log.w(TAG, "clearing the agent's session environment", it) } @@ -1511,13 +1274,6 @@ class SessionService : Service() { } } - /** - * proot is one thread, and every syscall it traps anywhere in the session - a path lookup in - * Proton's python, Wine's file opens, the Steam client's /proc scans - waits for that thread to - * be scheduled, at nice 0 beside a game that keeps the big cores busy. It is raised to nice -6, - * under the compositor's -8. After a moment, not at once: the first guest process is forked by - * proot itself and would inherit the value, and with it everything the session starts. - */ private fun raiseTracer(pid: Int, gen: Int) { Thread({ try { @@ -1555,13 +1311,6 @@ class SessionService : Service() { launchWatcher = null syncWatcher?.stopWatching() syncWatcher = null - // proot's --kill-on-exit takes the guest tree down only if proot gets to run it, and - // SIGKILL never lets it. A SIGKILLed proot left its tracees alive with no tracer: every - // seccomp-trapped syscall then failed with ENOSYS, they spun on retries at a full core - // for over an hour, and the reaper could not even open /proc to kill them. So: SIGTERM, - // a grace for proot's own cleanup, SIGKILL only if it will not go, then a sweep of the - // tree it had - snapshotted first, each pid checked against its start time so a number - // reused by a new process is never touched. val prootPid = sessionPid sessionPid = -1 val auxiliary = synchronized(auxiliaryProcessesLock) { @@ -1570,11 +1319,6 @@ class SessionService : Service() { SessionTerminal.clear(sessionGen) val controller = suspendController suspendController = null - // On its own thread, never here: stopSession runs on the main thread (the notification's - // Stop action arrives there), and collecting means copying the compositor's log, scrubbing - // every Steam log line by line - 42 files on one measured run - and waiting for logcat to - // dump the crash buffer. That was about four and a half seconds of blocked main thread, - // and Android ANR'd the app for it: the desktop session that would not let go. val ended = SessionPaths.take() if (ended != null) Thread({ collectSessionArtifacts(ended) }, "session-collect").start() components.reversed().forEach { @@ -1606,10 +1350,6 @@ class SessionService : Service() { if (controller != null) controller.closeAndResume(finishAfterTeardown) else finishAfterTeardown() } - /** - * Swiped out of recents. The activity is destroyed without any of our teardown running, so the - * guest would survive as an orphan holding the rootfs and the GPU. Treat the swipe as "quit". - */ override fun onTaskRemoved(rootIntent: Intent?) { if (pipTask) { Log.i(TAG, "PiP task dismissed - retaining the session under its background policy") @@ -1633,7 +1373,6 @@ class SessionService : Service() { override fun onBind(intent: Intent?): IBinder? = null - // ── Keeping the process alive ─────────────────────────────────────────────────────────── private fun acquireLocks() { try { @@ -1652,7 +1391,7 @@ class SessionService : Service() { val wifi = applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager val allowed = DeviceSupport.lowLatencyWifiSafe() if (!allowed) Log.i(TAG, "wifi lock skipped: its low-latency mode resets this device") - @Suppress("DEPRECATION") // deprecated from API 29, still honoured; targetSdk is 28 + @Suppress("DEPRECATION") if (allowed && wifiLock?.isHeld != true) { wifiLock = wifi?.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "DroidDeck:session-wifi") ?.apply { @@ -1662,8 +1401,6 @@ class SessionService : Service() { } Log.i(TAG, "wifi lock held=${wifiLock?.isHeld}") } catch (t: Throwable) { - // A partial wake lock keeps the process alive but does not stop WiFi power-save from - // throttling a backgrounded download to nothing, which is what this lock is for. Log.w(TAG, "no wifi lock (${t.message}) - a backgrounded download may stall") } } @@ -1683,11 +1420,9 @@ class SessionService : Service() { wifiLock = null } - // ── Notification ──────────────────────────────────────────────────────────────────────── private fun buildNotification(): Notification { val manager = getSystemService(NotificationManager::class.java) - // IMPORTANCE_LOW: it must never make a sound or push a heads-up over a game. val channel = NotificationChannel(CHANNEL_ID, getString(R.string.session_channel), NotificationManager.IMPORTANCE_LOW).apply { description = getString(R.string.session_channel_description) @@ -1784,7 +1519,6 @@ class SessionService : Service() { mainHandler.post(poll) } - /** The pads attached as the session starts; the activity logs the ones that come and go after. */ private fun logControllersAtStart() { val pads = ArrayList<String>() for (id in android.view.InputDevice.getDeviceIds()) { @@ -1798,25 +1532,11 @@ class SessionService : Service() { companion object { private const val TAG = "SessionService" - /** proot's tracer: above everything in the guest, under the compositor thread's -8. */ private const val TRACER_NICE = -6 - /** Downloads file whose contents become TU_DEBUG inside the session, e.g. "sysmem". */ private const val TU_DEBUG_SWITCH = "Download/droiddeck-tu-debug" - /** - * Where droiddeck-env (KEY=VALUE lines added to the session environment verbatim) used to - * be. Ignored now: a line there reaches the Steam client's environment as written - * (LD_PRELOAD, VK_ICD_FILENAMES, PATH...), and any app with storage access can write to - * Download - code of its choosing inside our sandbox, next to the client's saved login. - */ private const val LEGACY_ENV_SWITCH = "Download/droiddeck-env" private const val ENV_SWITCH_NAME = "droiddeck-env" - /** - * The session's extra-environment file: Android/data/<package>/files/droiddeck-env. Only - * this app, adb and the user (through the app's own file manager) can write there - from - * Android 11 on. Before that other apps with storage access can reach Android/data too, so - * there is no such file at all. - */ fun envSwitchFile(context: Context): File? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) context.getExternalFilesDir(null)?.let { File(it, ENV_SWITCH_NAME) } @@ -1837,10 +1557,8 @@ class SessionService : Service() { private const val ACTION_STOP_AUXILIARY = "com.droiddeck.launcher.STOP_AUXILIARY" private const val ACTION_AUXILIARY_EXITED = "com.droiddeck.launcher.AUXILIARY_EXITED" private const val EXTRA_AUXILIARY_PID = "auxiliaryPid" - /** Command lines that can only belong to a session of ours. */ private val STRAGGLERS = listOf("droiddeck-session", "gamescope", "Xwayland", "steamrtarm64", "steamwebhelper", "linuxfs/opt/android-host/proot", "/libproot.so", "pulseaudio/libpulseaudio.so") - /** How long proot gets to run its own cleanup before it is killed outright. */ private const val GRACE_MS = 1200L private const val STEAM_PICKUP_MS = 1500L private const val STEAM_EXIT_MS = 10_000L @@ -1851,19 +1569,14 @@ class SessionService : Service() { private const val NO_UINPUT_SWITCH = "Download/droiddeck-no-uinput" private const val NO_DECK_PAD_SWITCH = "Download/droiddeck-no-deck-pad" private const val NO_STEAM_TOUCH_SWITCH = "Download/droiddeck-no-steam-touch" - /** libfakeinput numbers the pads made through its /dev/uinput stand-in from here. */ private const val FIRST_VIRTUAL_PAD = 16 const val EXTRA_MODE = "mode" const val MODE_STEAM = "steam" const val MODE_DESKTOP = "lxqt" - /** A program inside the runtime, fullscreen under gamescope (EXTRA_PROGRAM = its path). */ const val MODE_RUN = "run" const val EXTRA_PROGRAM = "program" - /** MODE_RUN: arguments after the program (a game to boot). */ const val EXTRA_PROGRAM_ARGS = "programArgs" - /** MODE_STEAM: "desktop" for the client's desktop UI (default Big Picture); a steam:// URL to hand it. - * MODE_DESKTOP: "desktop" = open Steam's desktop client in the desktop once it is up. */ const val EXTRA_STEAM_UI = "steamUi" const val EXTRA_STEAM_URL = "steamUrl" const val EXTRA_GAME_ID = "gameId" @@ -1904,7 +1617,6 @@ class SessionService : Service() { context.startService(Intent(context, SessionService::class.java).setAction(ACTION_RESUME)) } - /** Route a validated decimal Steam game id through the current guest session. */ fun launchGame(context: Context, gameId: String): Boolean { if (!GameLaunchLink.validId(gameId) || !SessionState.running || SessionState.mode != MODE_STEAM || SessionState.stopRequested) return false @@ -1923,7 +1635,6 @@ class SessionService : Service() { context.startService(Intent(context, SessionService::class.java).setAction(ACTION_SUSPEND_POLICY_CHANGED)) } - /** Let the session service clean up the PTY's proot tree if the control screen closes. */ fun stopTerminalProcess(context: Context, pid: Int) { if (pid > 1) context.startService( Intent(context, SessionService::class.java) diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt index 57c377bcb..2f9f305ba 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionState.kt @@ -5,11 +5,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -/** - * The little that the activity and the service have to agree on. Process-wide rather than passed - * through intents, because both halves live in one process and the point of the split is that - * either can outlive the other. - */ object SessionState { @Volatile var phase = SessionPhase.IDLE @@ -48,26 +43,17 @@ object SessionState { var running = false var suspended by mutableStateOf(false) @Volatile var pipActive = false - /** MODE_RUN: the program inside the runtime the session was started for. */ @Volatile var program: String? = null @Volatile var programArgs: List<String> = emptyList() - /** MODE_STEAM: "desktop" for the client's desktop UI, else Big Picture; and a steam:// URL to hand it. */ @Volatile var steamUi: String? = null @Volatile var steamUrl: String? = null - /** A session the guest asked for (the desktop's Steam launchers): started by the activity once this one has ended. */ var relaunch: android.content.Intent? = null - /** HDR10 was asked for and the panel can show it: the compositor was told, and the session - * gets DXVK_HDR=1 and gamescope --hdr-enabled. Decided by the activity before the compositor starts. */ @JvmStatic var hdr = false - /** The pad is presented to the Steam client as a Steam Deck controller this session - * (SteamDeckPad): its Quick Access button is a real button, not the Guide+A chord. Decided by - * the service as the session starts, which can be after the activity has resumed - hence - * [deckPadListener]. */ @Volatile private var deckPadValue = false @JvmStatic var deckPad: Boolean get() = deckPadValue @@ -77,57 +63,40 @@ object SessionState { deckPadListener?.invoke() } - /** Told (on the thread that changed it) whenever [deckPad] changes. */ @Volatile var deckPadListener: (() -> Unit)? = null - /** The session screen's virtual pad, for the agent bridge's pad input (AgentInput). */ @Volatile var padBridge: com.droiddeck.launcher.input.PadBridge? = null - /** What the second screen shows, and on which display: kept for the session, so a screen - * turned off (sleep, a closed lid) or an activity recreated brings it back as it was. */ @Volatile var secondScreenMode = com.droiddeck.launcher.input.SecondScreenMode.NONE @Volatile var secondScreenDisplay = -1 - /** Which session this is: SessionService.MODE_STEAM or MODE_DESKTOP. */ @Volatile var mode = "steam" - /** The size gamescope was told to render at; set by the activity before the service starts. */ @Volatile var outputSize: Pair<Int, Int> = Pair(1920, 1080) - /** The session's resolution is "Follow screen": outputSize tracks the window while it runs. */ @Volatile var followScreen = false @Volatile var refreshHz: Float = 60f - /** - * How far the compositor enlarges the session onto the panel (panel over outputSize, long - * side to long side); set with outputSize. 0 before a session has been sized. Texture - * sharpness "Auto" (core/TextureFiltering) is derived from it when a game launches. - */ @Volatile var upscaleRatio: Float = 0f - /** The session's frame cap (SessionPrefs.fpsLimit), fixed when it starts; 0 = none. */ @Volatile var fpsLimit: Int = 0 - /** The compositor has presented a frame, so the loading panel is behind us for this session. */ @Volatile var firstFrameSeen = false - /** Where this session is writing its log, for the "it ended" message. */ @Volatile var logFile: File? = null - /** The fake evdev directory, once the service has prepared the rings. */ @Volatile var fakeInputDir: File? = null - /** Told when the session ends, so a visible activity can close itself. */ @Volatile var endListener: ((Int) -> Unit)? = null @@ -136,7 +105,6 @@ object SessionState { } } -/** Stable lifecycle values returned by the debug agent bridge. */ enum class SessionPhase { IDLE, PREPARING, diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionTerminal.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionTerminal.kt index 4db8f8671..c7ecba3bb 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionTerminal.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionTerminal.kt @@ -4,7 +4,6 @@ import com.termux.terminal.TerminalSession import com.termux.terminal.TerminalSessionClient import java.io.File -/** Immutable launch context for the optional PTY shell attached to the active Linux runtime. */ object SessionTerminal { private data class Launch( val command: Array<String>, diff --git a/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt b/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt index 160c0b3a3..1d4e6aec1 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SettingsReset.kt @@ -6,15 +6,6 @@ import com.droiddeck.launcher.core.GameEnvironment import com.droiddeck.launcher.input.ControllerPrefs import com.droiddeck.launcher.runtime.LinuxRuntime -/** - * Setup's "Reset all settings": every choice goes back to the default its getter falls back to. - * - * Only settings are removed. The "session" file also holds the library (added games' names, exes, - * appids and art picks), migration records, the drivers Auto installed, the storage and ROMs - * folders and the one-time permission prompts; those stay, as do store sign-ins, imported drivers - * and Lossless Scaling. Game profiles (each game's and the Default profile's environment - * overrides) are cleared only when asked for. - */ object SettingsReset { private const val TAG = "SettingsReset" @@ -30,11 +21,9 @@ object SettingsReset { "guestHostname", "fexPreset", "forceSsbs", "logs", "storageDiagnostics", "upscaler", "upscaleSharpness", "screenEffects", "textureAnisotropy", "textureLodBias", "steamDownloadsInBackground", "steam_offline", - // Cleared so the Quest's one-time compatibility defaults are written again. "questDefaultsV1", ) - /** Per-mode settings, stored as "<prefix><mode>". */ internal val SESSION_PREFIXES = listOf( "resolutionCap.", "displayResolution.", "customRes.", "hdr.", "fpsLimit.", "suspendPolicy.", ) @@ -43,13 +32,6 @@ object SettingsReset { internal fun isSessionSetting(key: String) = key in SESSION_KEYS || SESSION_PREFIXES.any { key.startsWith(it) } - /** - * Resets the settings, then per-game profiles when [gameProfiles] is set ([gameScopes] are the - * library's profile keys; scopes with saved overrides are found on their own). Blocking: call - * off the main thread. Returns false if a part could not be reset; the rest is still done. - * The language is not reset here: AppLanguage.setChosen changes the process locale, so the - * caller does that on the main thread. - */ fun resetAll(context: Context, gameProfiles: Boolean, gameScopes: Collection<String> = emptyList()): Boolean { var ok = true val session = context.getSharedPreferences("session", Context.MODE_PRIVATE) diff --git a/app/src/main/java/com/droiddeck/launcher/session/SteamDeckPad.kt b/app/src/main/java/com/droiddeck/launcher/session/SteamDeckPad.kt index ca30bf469..4b8daf517 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SteamDeckPad.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SteamDeckPad.kt @@ -7,33 +7,8 @@ import com.droiddeck.launcher.core.FileUtils import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File -/** - * The session's pad, described to the Steam client as a Steam Deck controller. - * - * On a Deck - and on the handhelds InputPlumber serves as one - the client finds the controller - * through libudev as a `hidraw` device under a `hid` device under a USB interface and reads it - * itself. libfakeinput serves the device node (`/dev/hidraw16`, built from the pad's input ring); - * what the client also needs is the sysfs it walks to find and identify that node, which a sandbox - * shows nothing of. This writes that sysfs as the kernel lays out a Deck's controller interface and - * returns the binds that put it in place: - * - * - `/sys/devices/droiddeck/usb1` - the USB device (Valve, 28de:1205), its interface 2, the HID - * device on it (`HID_ID`, `report_descriptor`) and that device's `hidraw/hidraw16`; - * - `/sys/class/hidraw/hidraw16` - what an enumeration of the `hidraw` subsystem lists; - * - `/sys/dev/char/240:16` - how the node's device number is looked up (in the directory the - * runtime already binds there for the GPU); - * - `/run/udev/data/c240:16` - the udev database entry that marks the device initialised, without - * which libudev's enumeration leaves a device node out; - * - [listingDir] - stand-ins for the listings of `/sys`, `/sys/class` and `/sys/bus`. An app under - * an enforcing SELinux policy (every retail phone) may not list those directories, and libudev - * abandons its whole scan when it cannot, so the client never reached `hidraw`; libfakeinput - * lists these to the client instead when the real listing is refused (FAKE_DECK_SYSFS_LISTING). - * - * Symlinks are absolute guest paths; proot resolves them inside the session. - */ object SteamDeckPad { private const val TAG = "SessionService" - /** Must match libfakeinput (fakeinput_steam.cpp: DECK_HIDRAW_*). */ private const val MAJOR = 240 private const val MINOR = 16 private const val NODE = "hidraw$MINOR" @@ -44,20 +19,14 @@ object SteamDeckPad { private const val HIDRAW = "$HID/hidraw/$NODE" private const val SERIAL = "DROIDDECK0001" - // The paired Bluetooth Steam Controller is a separate Triton HID device. It must not inherit - // the Deck's identity or report format. Must match fakeinput_steam.cpp (TRITON_HIDRAW_*). private const val TRITON_MINOR = 18 private const val TRITON_HID = "$GUEST_DEVICES/bluetooth/0005:28DE:1303.0003" private const val TRITON_SERIAL = "DROIDDECKSC01" - // The handheld's own pad as the Steam Controller (SessionPrefs.CONTROLLER_STEAM): the wired - // controller (28de:1302) on a USB device of its own, since it is built in. private const val TRITON_USB = "$GUEST_DEVICES/usb3" private const val TRITON_INTERFACE = "$TRITON_USB/3-1:1.0" private const val TRITON_WIRED_HID = "$TRITON_INTERFACE/0003:28DE:1302.0004" - /** Must match libfakeinput (DS4_HIDRAW_MINOR, DUALSENSE_HIDRAW_MINOR). */ private const val SONY_DS4_MINOR = 20 private const val SONY_DUALSENSE_MINOR = 21 - /** Must match libfakeinput's kDs4ReportDescriptor and kDualSenseReportDescriptor. */ private val DS4_REPORT_DESCRIPTOR = intArrayOf( 0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x06, 0x00, 0xff, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x85, 0x01, 0x09, 0x20, 0x95, 0x3f, 0x81, 0x02, 0x85, 0x05, 0x09, 0x21, 0x95, 0x1f, 0x91, 0x02, @@ -71,11 +40,8 @@ object SteamDeckPad { 0x85, 0x05, 0x09, 0x22, 0x95, 0x28, 0xb1, 0x02, 0x85, 0x09, 0x09, 0x23, 0x95, 0x13, 0xb1, 0x02, 0x85, 0x20, 0x09, 0x24, 0x95, 0x3f, 0xb1, 0x02, 0xc0, ).map { it.toByte() }.toByteArray() - /** Must match libfakeinput (TRITON_PAD_HIDRAW_*): its own node, so a paired controller keeps 18. */ private const val TRITON_PAD_MINOR = 19 - // Steam's touch controller (SteamTouchDevice), a second device beside the Deck's: 0000:11fb, - // "Mobile Touch Control", on a USB device of its own. Must match libfakeinput (TOUCH_*). private const val TOUCH_MINOR = 17 private const val TOUCH_NODE = "hidraw$TOUCH_MINOR" private const val TOUCH_USB = "$GUEST_DEVICES/usb2" @@ -84,7 +50,6 @@ object SteamDeckPad { private const val TOUCH_HIDRAW = "$TOUCH_HID/hidraw/$TOUCH_NODE" private const val TOUCH_SERIAL = "DROIDDECK0001" - /** libfakeinput's kTouchReportDescriptor: Generic Desktop / Game Pad, 40-byte input report. */ private val TOUCH_REPORT_DESCRIPTOR = intArrayOf( 0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x09, 0x02, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x28, 0x81, 0x02, @@ -92,14 +57,12 @@ object SteamDeckPad { 0x09, 0x04, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0xb1, 0x02, 0xc0, ).map { it.toByte() }.toByteArray() - /** InputPlumber's CONTROLLER_DESCRIPTOR, as libfakeinput answers HIDIOCGRDESC. */ private val REPORT_DESCRIPTOR = intArrayOf( 0x06, 0xff, 0xff, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x02, 0x09, 0x03, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x06, 0x09, 0x07, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0xb1, 0x02, 0xc0, ).map { it.toByte() }.toByteArray() - /** Triton's native 0x45/0x47 input and 0x80-0x85 haptic output report IDs. */ private val TRITON_REPORT_DESCRIPTOR = intArrayOf( 0x06, 0x00, 0xff, 0x09, 0x01, 0xa1, 0x01, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, @@ -115,23 +78,13 @@ object SteamDeckPad { 0xc0, ).map { it.toByte() }.toByteArray() - /** The wired controller's: its 0x42 state report in place of the Bluetooth 0x45. */ private val TRITON_WIRED_REPORT_DESCRIPTOR = TRITON_REPORT_DESCRIPTOR.copyOf().also { it[15] = 0x42 } - /** Where [prepare] puts the stand-in listings; bound into the guest at the same path. */ fun listingDir(sessionRoot: File) = File(sessionRoot, "sys/deck/listing") - /** Writes the tree and returns the `host:guest` binds for it, or none if it could not be made. */ - /** The pad's own devices by minor (the Deck, its Steam Controller, the PlayStation pads): guest paths. */ private val padNodes = mutableMapOf<Int, String>() @Volatile private var hidrawClassDir: File? = null - /** - * Lists only the device the pad is now ([padType], FakeInputWriter.PAD_TYPE_*) in - * /sys/class/hidraw, none for an Xbox 360 pad. Device scans then find what is plugged in: SDL's - * (the PlayStation pads' driver) keeps a device it found and could not open, and never tries it - * again, while one that disappears from the listing is let go. Before the shim is told. - */ @Synchronized fun showPad(padType: Int) { val dir = hidrawClassDir ?: return @@ -200,11 +153,8 @@ object SteamDeckPad { padNodes.clear() padNodes[MINOR] = HIDRAW - // Only the names matter: libudev reads a listing for the subsystems to descend into and - // then opens /sys/class/hidraw itself, which is the bind above. val listing = listingDir(sessionRoot) for (name in listOf("sys/bus", "sys/class", "sys/devices", "class/hidraw", "bus")) File(listing, name).mkdirs() - // The runtime binds this directory over /sys/dev/char for the GPU (LinuxRuntime.bindGpuNode). link(File(context.cacheDir, "drm/sys/$MAJOR:$MINOR"), HIDRAW) val udevData = File(LinuxRuntime.rootDir(context), "run/udev/data").apply { mkdirs() } @@ -238,8 +188,6 @@ object SteamDeckPad { Log.i(TAG, "deck pad: /dev/$TOUCH_NODE described as Steam's touch controller (0000:11fb)") } - // A Steam Controller's HID device: the handheld's own pad as the wired one, or the paired - // one on Bluetooth. Both are always described; libfakeinput offers whichever is in use. fun describeTriton(wired: Boolean) { val hid = if (wired) TRITON_WIRED_HID else TRITON_HID val product = if (wired) "1302" else "1303" @@ -276,8 +224,6 @@ object SteamDeckPad { File(udevData, "c$MAJOR:$minor").writeText("I:1\nE:ID_INPUT=1\nE:ID_INPUT_JOYSTICK=1\n") Log.i(TAG, "steam controller: /dev/$node described as Triton (28de:$product)") } - // The handheld's own pad as a PlayStation controller, each a USB device of its own with - // the report descriptor libfakeinput serves (kDs4/kDualSenseReportDescriptor). fun describeSony(dualSense: Boolean) { val bus = if (dualSense) 5 else 4 val product = if (dualSense) "0ce6" else "09cc" diff --git a/app/src/main/java/com/droiddeck/launcher/session/SteamDownloadMonitor.kt b/app/src/main/java/com/droiddeck/launcher/session/SteamDownloadMonitor.kt index af8515fbb..1744dc28e 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SteamDownloadMonitor.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SteamDownloadMonitor.kt @@ -17,11 +17,6 @@ internal class SteamDownloadMonitor { private var lastReceivedBytes = -1L private var lastReceivedAt = 0L - /** - * [receivedBytes] is the app uid's received-byte counter (the guest runs as that uid), or a - * negative value when Android does not report one. Steam writes its rate line about once a - * minute and flushes manifest counters even less often, so live traffic is what bridges them. - */ fun poll(runtimeRoot: File, now: Long = System.currentTimeMillis(), receivedBytes: Long = -1L): Boolean { val current = manifests(runtimeRoot).associate { it.path to read(it) } val progressed = current.any { (path, state) -> @@ -128,7 +123,6 @@ internal class SteamDownloadMonitor { private companion object { const val MANIFEST_PROGRESS_GRACE_MS = 8_000L - // Longer than Steam's ~60 s between rate lines, for when no traffic counter is available. const val CONTENT_ACTIVITY_GRACE_MS = 90_000L const val MIN_RECEIVE_BYTES_PER_SEC = 64L * 1024L val PATH = Regex("\"path\"\\s+\"([^\"]+)\"") diff --git a/app/src/main/java/com/droiddeck/launcher/session/SteamLanguage.kt b/app/src/main/java/com/droiddeck/launcher/session/SteamLanguage.kt index 11bb6d303..166dc8e23 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SteamLanguage.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SteamLanguage.kt @@ -2,14 +2,9 @@ package com.droiddeck.launcher.session import java.util.Locale -/** - * Steam's name for a language: the API language codes the client and the Steamworks API use - * ("english", "schinese", "koreana"...), for every language Steam's interface comes in. - */ object SteamLanguage { const val DEFAULT = "english" - /** Every language the Steam client's interface is offered in. */ val all: Set<String> = setOf( "arabic", "bulgarian", "schinese", "tchinese", "czech", "danish", "dutch", "english", "finnish", "french", "german", "greek", "hungarian", "indonesian", "italian", "japanese", @@ -17,16 +12,11 @@ object SteamLanguage { "spanish", "latam", "swedish", "thai", "turkish", "ukrainian", "vietnamese", "malay", ) - /** Spanish-speaking regions Steam serves with Latin American Spanish rather than Spain's. */ private val latinAmerica = setOf( "419", "AR", "BO", "CL", "CO", "CR", "CU", "DO", "EC", "GT", "HN", "MX", "NI", "PA", "PE", "PR", "PY", "SV", "US", "UY", "VE", ) - /** - * Steam's language for [locale]. [region] stands in for the locale's own when it has none: the - * app's Spanish carries no region, and a player in Mexico should get Latin American Spanish. - */ fun forLocale(locale: Locale, region: String? = null): String { val country = locale.country.ifEmpty { region.orEmpty() } return when (locale.language) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/StorageDiagnostics.kt b/app/src/main/java/com/droiddeck/launcher/session/StorageDiagnostics.kt index 73b64fb44..62e00be14 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/StorageDiagnostics.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/StorageDiagnostics.kt @@ -5,14 +5,9 @@ import android.system.Os import android.util.Log import java.io.File -/** The opt-in snapshot written beside a session's other logs. */ object StorageDiagnostics { private const val TAG = "StorageDiagnostics" - /** - * Records facts about the library actually selected for this session and returns its device - * number for the preload shim. The path and volume ID stay out of the shared log. - */ fun writeSnapshot( sessionDir: File, library: File, @@ -46,7 +41,6 @@ object StorageDiagnostics { } } - /** The longest matching mount entry determines the filesystem for nested bindable folders. */ private fun filesystemType(path: File): String? = runCatching { val canonical = path.canonicalPath File("/proc/mounts").useLines { lines -> diff --git a/app/src/main/java/com/droiddeck/launcher/session/WinComponentNames.kt b/app/src/main/java/com/droiddeck/launcher/session/WinComponentNames.kt index 5a8d38f5e..e9720e0f9 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/WinComponentNames.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/WinComponentNames.kt @@ -1,11 +1,5 @@ package com.droiddeck.launcher.session -/** - * What to call a catalog component on screen. Bannerlator's catalog keys (vcredist2010_dll, - * oalinst_dll, d3dcompiler_47...) have no display name, and some descriptions do not fit the entry, - * so names come from here: families by pattern, the rest from a table, the key itself otherwise. - * The "_dll" twin of an installer (the direct DLL copy) shares the installer's name. - */ object WinComponentNames { private val TABLE = mapOf( "oalinst" to "OpenAL", "XLiveRedist" to "Games for Windows Live", "gfw" to "Games for Windows Live", diff --git a/app/src/main/java/com/droiddeck/launcher/session/WinComponents.kt b/app/src/main/java/com/droiddeck/launcher/session/WinComponents.kt index 20e3f374c..c79171df2 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/WinComponents.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/WinComponents.kt @@ -19,23 +19,6 @@ import java.io.FileOutputStream import java.security.MessageDigest import java.util.zip.ZipInputStream -/** - * Windows components (OpenAL, d3dx9, XAudio, VC++ DLLs...) for a game's Proton prefix, picked per - * game, from Bannerlator's catalog (components.json on winlator-contents, Bottles' manifest format). - * - * Installing one runs its steps once, into the runtime: the archives it names are downloaded and - * its DLLs copied to /opt/droiddeck/wincomponents/<name>/{system32,syswow64}, with component.json - * holding its DLL overrides and the components it bundles. The picks go to the runtime as - * ~/.config/droiddeck/wincomponents.json, and droiddeck-wincomponents copies the picked components - * into the game's prefix at every launch (see that script for why every launch). - * - * A component whose installer is a Windows Installer package (.msi: XNA, PhysX, Games for Windows - * Live, MSXML...) installs here too, without running it: droiddeck-msi-install reads the package's - * tables in the runtime and lays out its files (Program Files, the .NET GAC, winsxs) and registry - * values the way Wine's msiexec would, under the same store folder. One that runs any other - * installer (.exe), unpacks a cabinet or edits the prefix's registry by itself is listed with the - * reason it cannot be installed yet. - */ object WinComponents { private const val TAG = "WinComponents" const val CATALOG_URL = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/main/components.json" @@ -43,46 +26,23 @@ object WinComponents { private const val GUEST_SELECTION = "root/.config/droiddeck/wincomponents.json" private const val STORE = "opt/droiddeck/wincomponents" private val ID = Regex("[A-Za-z0-9][A-Za-z0-9._-]{0,63}") - // register_dll is left out on purpose: the DLLs it names are ones Wine ships and has registered - // at the same path, so the native file at that path is the one the registration loads. private val FILE_STEPS = setOf("download_archive", "archive_extract", "copy_dll", "copy_file", "override_dll", "register_dll") private val INSTALLER_STEPS = setOf("install_exe", "install_msi") - /** - * Steps done here without any installer running: a Windows version or registry value for the - * prefix (written by droiddeck-wincomponents with the rest), files picked out of a cabinet, and - * "uninstall Wine Mono", which the mscoree override beside it already does for the game. - */ private val OFFLINE_STEPS = setOf("set_windows", "set_register_key", "get_from_cab", "uninstall") - /** - * Installers that are their own setup program (NSIS, InnoSetup, IExpress scripts): nothing - * inside them is a package to lay out, so they wait for a way to run them. - */ private val RUNS_OWN_SETUP = setOf( "K-Lite", "ffdshow", "lavfilters702", "lavfilters741", "quicktime72", "dirac", "webview2", "aairruntime", "gfw", "ie8_kb2936068", "art2k7min", "vcredist6sp6", "VulkanRT", "jet40", "mdac28", "oalinst", "dotnet20", "dotnet20sp1", "dotnet35", "dotnet35sp1", ) private const val MSI_INSTALL = "/usr/local/bin/droiddeck-msi-install" - /** Where a downloaded package or installer waits for the installer, inside the runtime so it can read it. */ private const val MSI_CACHE = "$STORE/.packages" - /** - * Catalog entries every Proton here already ships, which an install would only shadow: Wine - * Gecko and Mono, the Chinese/Japanese/Korean fonts (Malgun, MS Gothic, YaHei, SimSun are in - * share/fonts of Proton Experimental, GE-Proton and proton-cachyos alike), Wine's own - * PowerShell wrapper and the Vulkan loader (vulkan-1.dll, 32- and 64-bit, in all three). - */ private val PROTON_PROVIDES = setOf("gecko", "mono", "cjkfonts", "powershell", "VulkanRT") - /** The keys the catalog nests under "environment" on a few .NET entries. */ private val NESTED_KEYS = setOf("url", "file_name", "file_checksum", "file_size") fun validId(id: String): Boolean = ID.matches(id) && ".." !in id private val BACKGROUND = ThreadLocal<Boolean>() - /** - * Runs [block] as background work: the runtime helpers an install starts on this thread run - * at the lowest CPU and I/O priority (nice 19, ionice idle where the runtime has it). - */ fun <T> background(block: () -> T): T { BACKGROUND.set(true) try { return block() } finally { BACKGROUND.remove() } @@ -101,58 +61,35 @@ object WinComponents { class Component( val name: String, val description: String, val provider: String, val status: String, val dependencies: List<String>, val steps: List<Step>, - /** - * A recording of what the component's own installer leaves in a prefix (winlator-contents' - * component-snapshots release, made by tools/snapshot/record.py): the files it places and - * the registry values it writes. With one, the installer never runs: the files come from - * the archive beside the recording or out of the vendor's installer, the registry from the - * recording. Empty for a component without one. - */ val snapshot: String = "", ) enum class Support { READY, NEEDS_INSTALLER, UNSUPPORTED } - /** The two halves of an install the page shows one after the other, each 0..100. */ enum class Phase { DOWNLOAD, INSTALL } - /** Progress: the component being worked on, what it is doing, the phase, 0..100 or -1 when unknown. */ class Progress(val component: String, val stage: String, val phase: Phase, val percent: Int) - /** The percent a droiddeck-msi-install progress line carries ("progress 37% placing files"), or -1. */ private val PERCENT_LINE = Regex("""^(\d{1,3})% (.*)$""") private fun engineLine(text: String): Pair<String, Int> = PERCENT_LINE.find(text)?.let { it.groupValues[2] to it.groupValues[1].toInt().coerceIn(0, 100) } ?: (text to -1) - /** - * A step that installs a Windows Installer package, or an installer .exe that is a wrapper - * around packages (a WiX bundle, a self-extracting cabinet or 7-Zip archive), which - * droiddeck-msi-install lays out here without running it. - */ private fun isPackage(step: Step): Boolean = step.action in INSTALLER_STEPS && step.str("url").startsWith("https://") && (step.action == "install_msi" || listOf(step.str("file_name"), step.str("url").substringBefore('?')) .any { it.endsWith(".msi", ignoreCase = true) || it.endsWith(".exe", ignoreCase = true) }) - /** - * Catalog entries nothing here can use: the IE8 security update patches a browser no prefix - * has; the OpenAL setup program is a twin of oalinst_dll, which copies the same DLLs; and - * WebView2 can only be installed by running Microsoft's installer in the prefix (its files - * sit in a container nothing opens and may not be hosted), which no runtime here does. - */ private val POINTLESS = setOf("ie8_kb2936068", "oalinst", "webview2") private fun protonProvides(name: String): Boolean = name in PROTON_PROVIDES || name in POINTLESS || name.startsWith("mono-") fun hasSnapshot(c: Component): Boolean = c.snapshot.startsWith("https://github.com/") && c.snapshot.endsWith(".snapshot.json") - /** The steps a recording makes redundant: everything that places files or edits the registry. */ private val SNAPSHOT_COVERS = INSTALLER_STEPS + setOf("download_archive", "archive_extract", "copy_dll", "copy_file") + OFFLINE_STEPS fun fetch(): List<Component>? = Downloader.downloadString(CATALOG_URL)?.let { parse(it) } private const val CATALOG_CACHE = "wincomponents-catalog.json" - /** The catalog from the network, kept for later; the copy kept last time when offline. */ fun fetch(context: Context): List<Component>? { val cache = File(context.filesDir, CATALOG_CACHE) Downloader.downloadString(CATALOG_URL)?.let { body -> @@ -161,7 +98,6 @@ object WinComponents { return runCatching { parse(cache.readText()) }.getOrNull() } - /** The copy of the catalog kept by [fetch], without asking the network; null before the first fetch. */ fun cached(context: Context): List<Component>? = runCatching { parse(File(context.filesDir, CATALOG_CACHE).readText()) }.getOrNull() @@ -186,21 +122,14 @@ object WinComponents { } } - /** - * The catalog entry that installs [name] here: the detector names installers - * (vcredist2022, oalinst), and their DLL-copy twin (vcredist2022_dll, oalinst_dll) is the one - * that installs without running anything; [name] itself when there is no such twin. - */ fun installable(name: String, all: Map<String, Component>): String { val twin = all[if (name == "oalinst") "oalinst_dll" else "${name}_dll"] return if (twin != null && support(twin, all) == Support.READY) twin.name else name } - /** Whether [c] can be installed here, its bundled components included. */ fun support(c: Component, all: Map<String, Component>, depth: Int = 0): Support { if (c.status != "ready" || depth > 8 || protonProvides(c.name)) return Support.UNSUPPORTED val installers = c.steps.filter { it.action in INSTALLER_STEPS } - // A recording of the installer's result stands in for running it, whatever the steps say. if (hasSnapshot(c)) { return c.dependencies.map { all[it]?.let { d -> support(d, all, depth + 1) } ?: Support.UNSUPPORTED } .maxOrNull() ?: Support.READY @@ -219,7 +148,6 @@ object WinComponents { } private fun fileStepOk(step: Step): Boolean = when (step.action) { - // An .exe or .cab archive (DirectX's redistributable) is opened by the runtime's tools. "download_archive", "archive_extract" -> step.str("url").let { url -> !url.startsWith("http") || url.startsWith("https://github.com/") && url.substringBefore('?').lowercase().let { it.endsWith(".tar.xz") || it.endsWith(".zip") || it.endsWith(".exe") || it.endsWith(".cab") } @@ -228,20 +156,14 @@ object WinComponents { else -> true } - /** The installed version of a component, or null. */ fun installed(context: Context, id: String): String? = runCatching { JSONObject(File(LinuxRuntime.rootDir(context), "$STORE/$id/component.json").readText()).optString("version", "?") }.getOrNull() - /** Every component in the runtime, by id. */ fun installedIds(context: Context): List<String> = File(LinuxRuntime.rootDir(context), STORE).listFiles { f -> f.isDirectory && validId(f.name) } .orEmpty().map { it.name }.filter { installed(context, it) != null }.sorted() - /** - * Installs [c] and the components it bundles that are not installed yet. [onProgress] gets a - * line for the step and 0..100 (or -1). Returns null on success, else a message. - */ fun install(context: Context, c: Component, all: Map<String, Component>, onProgress: (Progress) -> Unit): String? { if (support(c, all) != Support.READY) return "${c.name} cannot be installed here yet" val order = ArrayList<Component>() @@ -251,8 +173,6 @@ object WinComponents { if (x.name == c.name || installed(context, x.name) == null) order.add(x) } visit(c, 0) - // One download per archive however many components copy out of it (DirectMusic's eleven - // parts all come from dxnt, 100 MB). val work = File(context.cacheDir, "wincomponents").apply { FileUtils.delete(this); mkdirs() } val archives = HashMap<String, File>() return try { @@ -276,10 +196,7 @@ object WinComponents { val overrides = ArrayList<String>() var source: File? = null var msi: JSONObject? = null - // The registry values the steps themselves set (a Windows version, a key), beside the packages'. val registry = ArrayList<JSONObject>() - // Archives the runtime's tools opened (installer .exe, cabinets), by the name the catalog - // calls them, and the folders they were opened into; cleaned up at the end. val runtimeArchives = HashMap<String, File>() val runtimeDirs = ArrayList<File>() val cabs = HashMap<String, File>() @@ -293,8 +210,6 @@ object WinComponents { for (step in c.steps) when { hasSnapshot(c) && step.action in SNAPSHOT_COVERS -> Unit else -> when (step.action) { - // A component may have several packages (PowerShell's 32- and 64-bit): each adds to the - // same folder, the first one names the component. "install_msi", "install_exe" -> { val (result, problem) = installPackage(context, c, step, staging, onProgress) if (result == null) return problem @@ -303,15 +218,12 @@ object WinComponents { result.optJSONArray("notes")?.let { for (i in 0 until it.length()) notes.put(it.get(i)) } } ?: result } - // A catalog step that makes room for the installer's own copy; ours is copied anyway. "delete_dlls" -> Unit "download_archive", "archive_extract" -> { val url = step.str("url") if (!url.startsWith("http")) continue source = archives[url] ?: run { val name = url.substringBefore('?').substringAfterLast('/') - // A zip or tar.xz is opened here; an .exe or .cab by the runtime's tools, so it - // is downloaded where the runtime can read it and kept for get_from_cab steps. val byRuntime = name.lowercase().let { it.endsWith(".exe") || it.endsWith(".cab") } val file = if (byRuntime) File(root, "$MSI_CACHE/${archives.size}-$name").apply { parentFile?.mkdirs() } else File(work, "${archives.size}-$name") @@ -333,8 +245,6 @@ object WinComponents { dir.also { archives[url] = it } } } - // Files picked out of a cabinet: one the catalog downloaded (DirectX's redistributable, - // a self-extracting cabinet), or cabinets an earlier pick put under temp/. "get_from_cab" -> { val pattern = step.str("source") val containers = runtimeArchives[pattern]?.let { listOf(it) } ?: run { @@ -362,7 +272,6 @@ object WinComponents { } } } - // The Windows version a game sees: HKCU\Software\Wine "Version", the last word wins. "set_windows" -> step.str("version").takeIf { it.isNotEmpty() }?.let { version -> registry.removeAll { it.optString("key") == "Software\\Wine" && it.optString("name") == "Version" } registry += JSONObject().put("hive", "HKCU").put("key", "Software\\Wine").put("name", "Version").put("type", "sz").put("data", version) @@ -385,8 +294,6 @@ object WinComponents { } registry += value } - // "Uninstall Wine Mono" before a real .NET Framework: the mscoree override the catalog - // sets beside it is what takes Mono out of the game's way; nothing to remove here. "uninstall" -> Unit "copy_dll", "copy_file" -> { val from = source ?: return "a copy step before any archive" @@ -424,7 +331,6 @@ object WinComponents { FileUtils.delete(temp) } if (registry.isNotEmpty()) { - // The packages' values are already in registry.json; the steps' own go after them. val file = File(staging, "registry.json") val values = runCatching { JSONObject(file.readText()).optJSONArray("values") }.getOrNull() ?: JSONArray() registry.forEach { values.put(it) } @@ -444,13 +350,6 @@ object WinComponents { return null } - /** - * Lays the component out from its recording: downloads the snapshot, then its files (the - * archive beside it when the recording carries them, else out of the vendor's installer by - * the paths the recording maps, small generated ones inline) into [staging]/drive_c, and the - * recorded registry values into registry.json. Returns a result like the MSI engine's, or - * null and why. - */ private fun installSnapshot(context: Context, c: Component, staging: File, work: File, onProgress: (Progress) -> Unit): Pair<JSONObject?, String> { val root = LinuxRuntime.rootDir(context) val name = c.snapshot.substringAfterLast('/') @@ -481,7 +380,6 @@ object WinComponents { file.delete() placed = drive.walkTopDown().count { it.isFile } } else { - // The files come out of the installer the catalog names, by the paths the recording maps. val step = c.steps.firstOrNull { it.action in INSTALLER_STEPS && it.str("url").startsWith("https://") } ?: return null to "the recording needs the installer, which the catalog does not name" val url = step.str("url") @@ -532,14 +430,9 @@ object WinComponents { .put("notes", JSONArray(notes + listOfNotNull(installerVersion.takeIf { it.isNotEmpty() }?.let { "recorded from $it on ${snapshot.optString("wine")}" }))) to "" } - /** The regex for a catalog file pattern: * matches anything, case does not matter. */ private fun globRegex(pattern: String) = Regex("^" + pattern.split("*").joinToString(".*") { Regex.escape(it) } + "$", RegexOption.IGNORE_CASE) - /** - * Has the runtime's tools open [archive] (an installer .exe, a cabinet, a self-extracting - * one) into a folder of the package cache called [tag]; that folder, or null and a log line. - */ private fun unpackInRuntime(context: Context, c: Component, archive: File, tag: String, onProgress: (Progress) -> Unit): File? { val root = LinuxRuntime.rootDir(context) val cache = File(root, MSI_CACHE) @@ -563,12 +456,6 @@ object WinComponents { return dir } - /** - * Downloads the component's package (.msi) or installer (.exe) into the runtime and has - * droiddeck-msi-install lay it out in [staging]. Returns the installer's result (product, - * version, counts, notes), or null and why. Everything the installer prints also goes to - * files/logs/tools. - */ private fun installPackage(context: Context, c: Component, step: Step, staging: File, onProgress: (Progress) -> Unit): Pair<JSONObject?, String> { val root = LinuxRuntime.rootDir(context) val url = step.str("url") @@ -641,10 +528,6 @@ object WinComponents { Log.w(TAG, "extract ${archive.name}", e); false } - /** - * Bannerlator's rule: the manifest's own temp paths are Bottles', so the files are found by name - * anywhere in the unpacked tree, preferring the ones under the matching win32/win64 folder. - */ private fun copyMatching(srcRoot: File, pattern: String, dest: File, arch: String?) { if (pattern.isEmpty()) return val rx = Regex("^" + pattern.split("*").joinToString(".*") { Regex.escape(it) } + "$", RegexOption.IGNORE_CASE) @@ -658,7 +541,6 @@ object WinComponents { matches.forEach { f -> f.copyTo(File(dest, f.name.lowercase()), overwrite = true) } } - /** Removes a component from the runtime and from every game's picks. */ @Synchronized fun uninstall(context: Context, id: String) { require(validId(id)) @@ -666,17 +548,8 @@ object WinComponents { save(context, selections(context).mapValues { (_, sel) -> Selection(sel.auto - id, sel.user - id) }) } - /** - * Why a component was turned on for a game by itself ([setAuto]): [kind] says whose list it came - * from (see AutoComponents), [args] what the reason line names. Kept beside the picks. - */ class AutoReason(val kind: String, val args: List<String>) - /** - * Everything kept per game, by the appid its prefix is named after: [auto] - turned on by - * itself, with its reason; [user] - a switch the user pressed (true on, false off), which wins - * over [auto] either way. What the launch reads is [effective]. - */ class Selection(val auto: Map<String, AutoReason>, val user: Map<String, Boolean>) { val effective: List<String> get() = (auto.keys.filter { user[it] != false } + user.filterValues { it }.keys).distinct() @@ -697,7 +570,6 @@ object WinComponents { AutoReason(r.optString("kind"), (0 until a.length()).map { a.optString(it) }) } }.orEmpty() - // Before there were overrides, every pick was the user's own. val user = if (users == null) games.optJSONArray(app)?.let { ids -> (0 until ids.length()).map { ids.getString(it) }.filter { validId(it) }.associateWith { true } }.orEmpty() else users.optJSONObject(app)?.let { o -> o.keys().asSequence().filter { validId(it) }.associateWith { o.optBoolean(it) } }.orEmpty() Selection(auto, user) @@ -706,7 +578,6 @@ object WinComponents { emptyMap() } - /** Each game's picks as the launch takes them, by the appid its prefix is named after. */ fun read(context: Context): Map<String, List<String>> = selections(context).mapValues { it.value.effective }.filterValues { it.isNotEmpty() } @@ -714,7 +585,6 @@ object WinComponents { fun selection(context: Context, appKey: String): Selection = selections(context)[appKey] ?: Selection(emptyMap(), emptyMap()) - /** The user's own switch for [id] in [appKey]: on or off, over whatever was picked by itself. */ @Synchronized fun setUser(context: Context, appKey: String, id: String, on: Boolean) { require(validId(id)) @@ -724,7 +594,6 @@ object WinComponents { save(context, all) } - /** The components picked by themselves for [appKey], replacing the earlier ones; the user's switches stay. */ @Synchronized fun setAuto(context: Context, appKey: String, auto: Map<String, AutoReason>) { require(auto.keys.all { validId(it) }) @@ -734,7 +603,6 @@ object WinComponents { save(context, all) } - /** The user's picks for [appKey] set outright (each listed on, every other user switch dropped). */ @Synchronized fun setPicks(context: Context, appKey: String, ids: List<String>) { require(ids.all { validId(it) }) @@ -746,7 +614,6 @@ object WinComponents { private fun save(context: Context, games: Map<String, Selection>) { val kept = games.filterValues { it.auto.isNotEmpty() || it.user.isNotEmpty() } - // Version 1 still: droiddeck-wincomponents reads "games" and ignores the rest. val text = JSONObject().put("version", 1) .put("games", JSONObject().apply { kept.forEach { (app, sel) -> sel.effective.takeIf { it.isNotEmpty() }?.let { put(app, JSONArray(it)) } } }) .put("auto", JSONObject().apply { @@ -762,11 +629,6 @@ object WinComponents { publish(context, text) } - /** - * Writes the picks where the launch script reads them; SessionFiles calls this at every session - * start too, so a runtime reinstalled since keeps them. An empty selection still writes a file, - * which is how a component turned off for every game is taken out of their prefixes. - */ @Synchronized fun publish(context: Context, text: String? = null) { val body = text ?: runCatching { AtomicFile(File(context.filesDir, SELECTION)).readFully().toString(Charsets.UTF_8) }.getOrNull() ?: return diff --git a/app/src/main/java/com/droiddeck/launcher/store/FlathubApi.kt b/app/src/main/java/com/droiddeck/launcher/store/FlathubApi.kt index a26c4d4a4..b95a4c10e 100644 --- a/app/src/main/java/com/droiddeck/launcher/store/FlathubApi.kt +++ b/app/src/main/java/com/droiddeck/launcher/store/FlathubApi.kt @@ -9,10 +9,6 @@ import java.net.HttpURLConnection import java.net.URL import java.net.URLEncoder -/** - * Flathub's public API (flathub.org/api/v2): the collections its front page shows, search, and an - * app's AppStream data. Only what this device can install is kept - apps built for aarch64. - */ object FlathubApi { private const val TAG = "FlathubApi" private const val BASE = "https://flathub.org/api/v2" @@ -33,7 +29,6 @@ object FlathubApi { val runtime: String?, val downloadSize: Long, val installedSize: Long, val arches: List<String>, ) - /** A category as Flathub's search filters it, with the name shown for it. */ class Category(val id: String, @StringRes val label: Int) val categories = listOf( @@ -43,14 +38,11 @@ object FlathubApi { Category("Utility", R.string.store_cat_utilities), ) - /** Front-page collections: popular, trending, recently-updated, recently-added. */ fun collection(name: String, count: Int = 30): List<AppSummary>? { - // The collections are not filtered by architecture; ask for more and keep what fits. val body = get("$BASE/collection/$name?page=1&per_page=${count * 2}") ?: return null return hits(body)?.take(count) } - /** Search, optionally within one category (Flathub's main_categories facet). */ fun search(query: String, category: String? = null, count: Int = 48): List<AppSummary>? { val filters = JSONArray().put(JSONObject().put("filterType", "arches").put("value", ARCH)) if (category != null) filters.put(JSONObject().put("filterType", "main_categories").put("value", category)) @@ -60,7 +52,6 @@ object FlathubApi { return hits(body) } - /** Flathub's own copy of an app's icon: for apps that export only an SVG, which is not drawn here. */ fun iconUrl(id: String) = "https://dl.flathub.org/media/icons/128x128/$id.png" fun details(id: String): AppDetails? { @@ -120,7 +111,6 @@ object FlathubApi { Log.w(TAG, "hits: $e"); null } - /** AppStream descriptions are a small HTML subset: paragraphs and lists become lines. */ internal fun plainText(html: String): String = html .replace(Regex("(?i)<li>\\s*"), "• ") .replace(Regex("(?i)</(p|li|ul|ol)>"), "\n") diff --git a/app/src/main/java/com/droiddeck/launcher/store/StoreState.kt b/app/src/main/java/com/droiddeck/launcher/store/StoreState.kt index cd47019d8..c03a98246 100644 --- a/app/src/main/java/com/droiddeck/launcher/store/StoreState.kt +++ b/app/src/main/java/com/droiddeck/launcher/store/StoreState.kt @@ -11,10 +11,6 @@ import androidx.compose.runtime.setValue import com.droiddeck.launcher.R import com.droiddeck.launcher.runtime.FlatpakManager -/** - * The store's state for the life of the process, so an install carries on - and its progress - * shows - while the user moves around the launcher or plays something meanwhile. - */ object StoreState { private val main = Handler(Looper.getMainLooper()) @@ -26,10 +22,8 @@ object StoreState { private set var checkingUpdates by mutableStateOf(false) private set - /** Flathub has been asked this session, so an empty [updates] means up to date. */ var updatesChecked by mutableStateOf(false) private set - /** What is running: "setup", an app id, or "update-all"; null when idle. */ var busy by mutableStateOf<String?>(null) private set var stage by mutableStateOf<String?>(null) @@ -37,7 +31,6 @@ object StoreState { var percent by mutableStateOf(-1) private set - /** The front page's sections, by collection name; absent until loaded, empty list on failure. */ val sections = mutableStateMapOf<String, List<FlathubApi.AppSummary>>() var sectionsFailed by mutableStateOf(false) private set @@ -49,7 +42,6 @@ object StoreState { var searching by mutableStateOf(false) private set - /** An app whose Flathub page was asked for from outside the store (its card on Desktop), once. */ var openRequest by mutableStateOf<String?>(null) private var searchSeq = 0 diff --git a/app/src/main/java/com/droiddeck/launcher/store/UserAppsState.kt b/app/src/main/java/com/droiddeck/launcher/store/UserAppsState.kt index 340eabfbf..9979b0e5d 100644 --- a/app/src/main/java/com/droiddeck/launcher/store/UserAppsState.kt +++ b/app/src/main/java/com/droiddeck/launcher/store/UserAppsState.kt @@ -10,31 +10,23 @@ import androidx.compose.runtime.setValue import com.droiddeck.launcher.R import com.droiddeck.launcher.runtime.UserApps -/** - * The Desktop page's added apps and the one add or remove in progress. State changes only on the - * main thread; the work runs on a thread of its own and posts back. - */ object UserAppsState { private val main = Handler(Looper.getMainLooper()) var items by mutableStateOf<List<UserApps.App>>(emptyList()) private set - /** What is being added or removed; null when idle. */ var working by mutableStateOf<String?>(null) private set var stage by mutableStateOf<String?>(null) private set var percent by mutableStateOf(-1) private set - /** Why the last add or remove failed, shown on the page until the next one. */ var lastError by mutableStateOf<String?>(null) private set - /** The app whose updates are being looked up, and what each lookup found, by app key. */ var checking by mutableStateOf<String?>(null) private set var updates by mutableStateOf<Map<String, UserApps.UpdateCheck>>(emptyMap()) private set - /** Bumped per listing; only the newest one is shown. */ private var listing = 0 fun refresh(context: Context) { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/AchievementNotifications.kt b/app/src/main/java/com/droiddeck/launcher/stores/AchievementNotifications.kt index 818bd0f37..e6bfc35f6 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/AchievementNotifications.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/AchievementNotifications.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import org.json.JSONObject -/** Session-owned unlocks; history reads and upload acknowledgements never create notifications. */ object AchievementNotifications { data class Unlock(val client: String, val id: String, val name: String, val description: String, val icon: String?, val game: String) @@ -38,7 +37,6 @@ object AchievementNotifications { if (pending.firstOrNull() === unlock) pending = pending.drop(1) } - /** Only presentation fields from the owned daemon, bounded before entering Compose. */ fun parse(event: JSONObject, game: String): Unlock? { val client = event.optString("client_id") val id = event.optString("id") diff --git a/app/src/main/java/com/droiddeck/launcher/stores/CloudPlan.kt b/app/src/main/java/com/droiddeck/launcher/stores/CloudPlan.kt index 720b462d6..6368f558a 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CloudPlan.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CloudPlan.kt @@ -2,21 +2,7 @@ package com.droiddeck.launcher.stores import org.json.JSONObject -/** - * What a cloud sync may do, decided from three views of each save file: the local copy, the cloud - * copy, and the baseline - both as they were when this device last synced the game. Pure, so the - * rules are tested on their own: - * - * - No upload before a download has completed once on this device (no baseline): a first launch - * writes new profile files that must never replace the real saves in the cloud. - * - The first download (no baseline) lets the cloud win where the two differ - the local folder is - * backed up first. - * - A file changed on both sides since the baseline is a conflict: neither side is overwritten; the - * user picks Keep cloud or Keep local in Manage saves. - * - Otherwise the side that changed since the baseline wins; one that changed on neither is left. - */ object CloudPlan { - /** One file as this device last saw it in sync: the local MD5, and the cloud's MD5 (when known) and time. */ data class Entry(val localMd5: String, val cloudMd5: String?, val cloudModified: Long) class Local(val md5: String) @@ -24,11 +10,9 @@ object CloudPlan { class Plan(val transfer: List<String>, val conflicts: List<String>, val same: List<String>, val refused: String? = null) - /** The cloud copy differs from what the baseline recorded of it. */ private fun cloudChanged(r: Remote, b: Entry?): Boolean { if (b == null) return true if (r.md5 != null && b.cloudMd5 != null) return !r.md5.equals(b.cloudMd5, ignoreCase = true) - // Times only (Epic's listing): the service's own clock, so a couple of seconds of slack. return r.modified > 0 && b.cloudModified > 0 && kotlin.math.abs(r.modified - b.cloudModified) > 2000 } @@ -36,7 +20,6 @@ object CloudPlan { private fun same(l: Local, r: Remote): Boolean = r.md5 != null && r.md5.equals(l.md5, ignoreCase = true) - /** Cloud → device. [baseline] null = this device never synced the game. */ fun down(local: Map<String, Local>, remote: Map<String, Remote>, baseline: Map<String, Entry>?): Plan { val transfer = ArrayList<String>(); val conflicts = ArrayList<String>(); val same = ArrayList<String>() for ((name, r) in remote) { @@ -53,7 +36,6 @@ object CloudPlan { return Plan(transfer, conflicts, same) } - /** Device → cloud. Refused outright without a baseline. */ fun up(local: Map<String, Local>, remote: Map<String, Remote>, baseline: Map<String, Entry>?): Plan { if (baseline == null) return Plan(emptyList(), emptyList(), emptyList(), refused = "no-baseline") val transfer = ArrayList<String>(); val conflicts = ArrayList<String>(); val same = ArrayList<String>() diff --git a/app/src/main/java/com/droiddeck/launcher/stores/CloudSavePaths.kt b/app/src/main/java/com/droiddeck/launcher/stores/CloudSavePaths.kt index c54297794..62133fbbd 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CloudSavePaths.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CloudSavePaths.kt @@ -1,28 +1,10 @@ -/* - * Cloud-save folder resolution for GOG and Epic games, after Bannerlator's GogCloudSavePaths and - * EpicCloudSavePaths (device-proven there, August 2026): a store's save-location template expanded - * against the game's Proton prefix and its install folder. - */ package com.droiddeck.launcher.stores import java.io.File -/** - * Where a store keeps a game's saves on Windows, made concrete inside this game's Proton prefix. - * - * GOG gives a template per game in its public remote-config (`cloudStorage.locations[].location`): - * a leading `<?TOKEN?>` (or `%TOKEN%`) and a path, e.g. ELDERBORN's - * `<?APPLICATION_DATA_LOCAL_LOW?>/Hyperstrange/ELDERBORN`. Epic gives `CloudSaveFolder` in the - * game's catalog attributes: a leading `{Token}`, with `{EpicId}` / `{AppName}` inside, e.g. - * `{AppData}/Game/Saved/SaveGames`. Profile tokens map under the prefix's Wine user - * (`drive_c/users/steamuser`), the install tokens to the game's folder. The path is walked against - * what is on disk case-insensitively; a result outside its boundary is refused, never guessed. - */ object CloudSavePaths { - /** The Wine user every prefix has (Proton's). */ fun profile(prefix: File): File = File(prefix, "drive_c/users/steamuser") - /** Expands a GOG location template; null for an unknown token or an escape. */ fun gog(template: String, prefix: File, install: File): File? { val norm = template.replace('\\', '/').trim().trimStart('/') val match = Regex("^(<\\?[^?>]+\\?>|%[^%]+%)").find(norm) ?: return null @@ -42,7 +24,6 @@ object CloudSavePaths { return walk(base, boundary, norm.substring(match.value.length), fuzzy = false) } - /** Expands an Epic CloudSaveFolder; null for an unknown token or an escape. */ fun epic(template: String, prefix: File, install: File, accountId: String, appName: String): File? { val norm = template.replace('\\', '/').trim().trimStart('/') val match = Regex("^\\{([^}]+)\\}").find(norm) ?: return null @@ -62,10 +43,6 @@ object CloudSavePaths { return walk(base, boundary, rest, fuzzy = true) } - /** - * Each segment matched against the folder's entries case-insensitively (and, for Epic, by letters - * and digits alone when that names exactly one), else taken as written - a download creates it. - */ private fun walk(base: File, boundary: File, rest: String, fuzzy: Boolean): File? { var dir = base for (seg in rest.trimStart('/').split('/').filter { it.isNotEmpty() && it != "." }) { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/CloudSaves.kt b/app/src/main/java/com/droiddeck/launcher/stores/CloudSaves.kt index e0f4ab6c1..3c3d38d26 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CloudSaves.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CloudSaves.kt @@ -17,34 +17,16 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import org.json.JSONObject -/** - * GOG and Epic cloud saves for an installed store game: before a launch the cloud copy is brought - * down where it is newer, after the game exits what changed goes up - newest wins, file by file. - * Before a download overwrites anything, the folder is copied to the app's storage (the last - * [BACKUPS] per game). Both launch paths reach it: the compat tool's request from - * droiddeck-store-launch ([StoreLaunchRequests]) for every launch, and the Games tab's Manage saves. - * - * The save folder: GOG's remote-config location template (by the game's Galaxy client id) or - * Epic's CloudSaveFolder catalog attribute, cached per game, made concrete by [CloudSavePaths] - * inside the game's Proton prefix. A game whose store gives none has no cloud saves. - */ object CloudSaves { private const val TAG = "CloudSaves" private const val BACKUPS = 3 private const val PARALLEL = 6 - /** What a sync did, for the log and the Manage saves row. */ - /** - * What a sync did, for the log and the Manage saves row. [result]: ok, skipped (nothing to do or - * not possible: [reason] says why), deferred (no prefix yet; it runs once Proton makes one), - * failed. [conflicts]: files changed on both sides, left alone. - */ class Result(val result: String, val files: Int, val bytes: Long, val reason: String, val conflicts: Int = 0) { val ok: Boolean get() = result != "failed" val line: String get() = "result=$result files=$files bytes=$bytes reason=$reason" + if (conflicts > 0) " conflicts=$conflicts" else "" } - /** A game's cloud state for the Manage saves row. */ class Status(val supported: Boolean, val folder: File?, val lastSync: Long, val reason: String, val conflicts: List<String> = emptyList()) private fun game(context: Context, store: Store, id: String): Pair<File, StoreGameSidecar>? = @@ -52,7 +34,6 @@ object CloudSaves { StoreGameSidecar.read(f)?.takeIf { it.store == store && it.id == id && it.isInstalled }?.let { f to it } } - /** The game's prefix: compatdata/<its shortcut appid>/pfx, wherever that library is. */ fun prefix(context: Context, folder: File): File? { val appId = AddedGames.scan(context).firstOrNull { it.folder.absolutePath == folder.absolutePath }?.appId ?: return null return Library.protonPrefix(context, appId and 0xFFFFFFFFL) @@ -64,9 +45,7 @@ object CloudSaves { private fun markSynced(context: Context, store: Store, id: String) = prefs(context, store).edit().putLong("cloud_sync_$id", System.currentTimeMillis()).apply() - // ---- the save folder ------------------------------------------------------------------------ - /** The store's template for this game: cached, else fetched; "" when the store gives none, null when it could not be asked. */ fun template(context: Context, store: Store, id: String, folder: File, sidecar: StoreGameSidecar): String? { val p = prefs(context, store) if (p.contains("cloud_template_$id")) return p.getString("cloud_template_$id", "") @@ -89,7 +68,6 @@ object CloudSaves { GogPrefs.get(context).getString("client_id_$id", null)?.ifEmpty { null } ?: runCatching { JSONObject(File(folder, "goggame-$id.info").readText()).optString("clientId").ifEmpty { null } }.getOrNull() - /** GOG's public remote-config: `content.Windows.cloudStorage.locations`, the one named "saves" first. */ private fun gogTemplate(context: Context, id: String, folder: File): String? { val clientId = gogClientId(context, id, folder) ?: return "" return try { @@ -109,7 +87,6 @@ object CloudSaves { } } - /** The concrete save folder in the prefix, or null with why. */ private fun saveFolder(context: Context, store: Store, id: String): Pair<File?, String> { val (folder, sidecar) = game(context, store, id) ?: return null to "not-installed" val template = template(context, store, id, folder, sidecar) ?: return null to "store-unreachable" @@ -128,7 +105,6 @@ object CloudSaves { return Status(reason != "no-cloud-saves" && reason != "not-installed", dir, lastSync(context, store, id), reason, conflicts(context, store, id)) } - // ---- transport ------------------------------------------------------------------------------ private fun transport(context: Context, store: Store, id: String, folder: File): CloudTransport? = when (store) { Store.GOG -> { @@ -147,13 +123,11 @@ object CloudSaves { Store.AMAZON -> null } - /** GOG cloud storage wants a token issued to the game's own client: the Galaxy refresh token, exchanged with the game's id and secret. */ private fun gogScopedToken(context: Context, id: String, clientId: String): String? { val secret = GogPrefs.get(context).getString("client_secret_$id", null)?.ifEmpty { null } ?: return null return GogAuth.gameToken(context, clientId, secret)?.accessToken } - // ---- sync ----------------------------------------------------------------------------------- private fun baseline(context: Context, store: Store, id: String): Map<String, CloudPlan.Entry>? = CloudPlan.fromJson(prefs(context, store).getString("cloud_baseline_$id", null)) @@ -161,29 +135,21 @@ object CloudSaves { private fun saveBaseline(context: Context, store: Store, id: String, b: Map<String, CloudPlan.Entry>) = prefs(context, store).edit().putString("cloud_baseline_$id", CloudPlan.toJson(b)).apply() - /** Files changed on both sides since the last sync, waiting for Keep cloud / Keep local. */ fun conflicts(context: Context, store: Store, id: String): List<String> = prefs(context, store).getString("cloud_conflicts_$id", "").orEmpty().split('\n').filter { it.isNotEmpty() } private fun saveConflicts(context: Context, store: Store, id: String, names: Collection<String>) = prefs(context, store).edit().putString("cloud_conflicts_$id", names.joinToString("\n")).apply() - /** Cloud → device where the cloud copy changed (the first time: wherever it differs). [force]: conflicts too (Keep cloud). Blocking. */ fun download(context: Context, store: Store, id: String, force: Boolean = false): Result = sync(context, store, id, "down") { dir, cloud -> downWith(context, store, id, dir, cloud, force) } - /** Device → cloud where the local copy changed, once a download has set the baseline. [force]: conflicts too (Keep local). Blocking. */ fun upload(context: Context, store: Store, id: String, force: Boolean = false): Result = sync(context, store, id, "up") { dir, cloud -> upWith(context, store, id, dir, cloud, force) } fun keepCloud(context: Context, store: Store, id: String): Result = download(context, store, id, force = true) fun keepLocal(context: Context, store: Store, id: String): Result = upload(context, store, id, force = true) - /** - * The pre-launch download, or - when Proton has not made the game's prefix yet (a first launch) - * - a deferred one: answered at once, then run in the background as soon as the prefix's user - * folder appears (within [PREFIX_WAIT_MS]), so the launch is not held up for it. - */ fun downloadOrDefer(context: Context, store: Store, id: String): Result { val app = context.applicationContext val g = game(app, store, id) ?: return done(store, id, "down", Result("skipped", 0, 0, "not-installed")) @@ -241,7 +207,6 @@ object CloudSaves { internal fun md5(data: ByteArray): String = MessageDigest.getInstance("MD5").digest(data).joinToString("") { "%02x".format(it) } - /** The cloud listing, with MD5 and time filled in (one HEAD each where the listing lacks them, in parallel). */ private fun remote(cloud: CloudTransport, wanted: (String) -> Boolean): Map<String, CloudFile> { val files = cloud.list() val pool = Executors.newFixedThreadPool(PARALLEL, com.droiddeck.launcher.stores.download.DownloadQueue.workerFactory("cloud")) @@ -272,7 +237,6 @@ object CloudSaves { next[name] = CloudPlan.Entry(md5(data), f.md5, if (modified > 0) modified else f.modifiedMs) } for (name in plan.same) remote[name]?.let { r -> next[name] = CloudPlan.Entry(local.getValue(name).md5, r.md5, r.modifiedMs) } - // A download that completed sets the baseline even when the cloud was empty: uploads may follow. saveBaseline(context, store, id, next) val left = if (force) emptyList() else plan.conflicts saveConflicts(context, store, id, left) @@ -290,7 +254,6 @@ object CloudSaves { val next = HashMap(base) for (name in plan.same) remote[name]?.let { r -> next[name] = CloudPlan.Entry(local.getValue(name).md5, r.md5, r.modifiedMs) } if (send.isNotEmpty()) { - // The cloud copies about to be replaced are kept first, as a download keeps the local ones. backupCloud(context, store, id, cloud, send.mapNotNull { remote[it] }) val data = send.associateWith { files.getValue(it).readBytes() } val stamped = cloud.put(data) @@ -307,15 +270,7 @@ object CloudSaves { return Result("ok", send.size, send.sumOf { files.getValue(it).length() }, if (send.isEmpty()) "up-to-date" else "uploaded", left.size) } - // ---- uploads that do not depend on one hook --------------------------------------------------- - /** - * A game launched with cloud saves is marked dirty in the app's storage when its pre-launch step - * runs. The upload happens on the first of: the compat tool's exit request (`exit`), the end of - * the session (`session-end`), or the app's next start for a mark a killed app left (`recovery`) - * - so a session closed from the drawer, a game Steam killed, or a crash still uploads. The mark - * goes only after an upload that went through, or one skipped on purpose with its reason logged. - */ private fun marks(context: Context) = context.applicationContext.getSharedPreferences("stores.cloud", Context.MODE_PRIVATE) fun markDirty(context: Context, store: Store, id: String) { @@ -329,10 +284,8 @@ object CloudSaves { private val markLock = Any() - /** Why a skip is final: retrying cannot change it, so the mark goes. */ private val FINAL_SKIPS = setOf("off", "no-cloud-saves", "no-baseline", "not-installed", "nothing-local") - /** Uploads [id] if it is marked dirty; null when it is not (another trigger got there first). */ fun uploadDirty(context: Context, store: Store, id: String, trigger: String): Result? = synchronized(markLock) { val key = "dirty:${store.id}:$id" if (!marks(context).contains(key)) return null @@ -341,10 +294,6 @@ object CloudSaves { r } - /** - * Every dirty game, uploaded; [running] says a game session is up, in which case `recovery` waits - * - a marked game may be the one running. - */ fun uploadAllDirty(context: Context, trigger: String, running: Boolean = com.droiddeck.launcher.session.SessionState.running) { if (trigger == "recovery" && running) return for ((store, id) in dirty(context)) uploadDirty(context, store, id, trigger) @@ -353,7 +302,6 @@ object CloudSaves { private fun backupRoot(context: Context, store: Store, id: String) = File(context.filesDir, "stores/cloud-backups/${store.id}-${id.replace(Regex("[^A-Za-z0-9._-]"), "_")}") - /** The local folder as it is, before a download changes it; the last [BACKUPS] of each kind are kept. */ private fun backup(context: Context, store: Store, id: String, dir: File, kind: String) { val root = backupRoot(context, store, id) runCatching { dir.copyRecursively(File(root, "$kind-${System.currentTimeMillis()}"), overwrite = true) } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/CloudTransport.kt b/app/src/main/java/com/droiddeck/launcher/stores/CloudTransport.kt index 3446bf4ab..becf14e21 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CloudTransport.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CloudTransport.kt @@ -1,9 +1,3 @@ -/* - * The two stores' cloud-save services, after Bannerlator's GogCloudSaveManager and - * EpicCloudSaveManager (device-proven there, August 2026): GOG's cloudstorage (one object per - * file, Last-Modified and an MD5 ETag on a HEAD) and Epic's savesync data storage (a listing with - * lastModified and signed read links; signed write links asked for per upload). - */ package com.droiddeck.launcher.stores import java.io.ByteArrayOutputStream @@ -16,17 +10,12 @@ import java.util.TimeZone import org.json.JSONArray import org.json.JSONObject -/** One file in a game's cloud: its key (a path with '/'), when it was written (-1 unknown) and its MD5 when known. */ class CloudFile(val name: String, var modifiedMs: Long, var md5: String? = null, val readLink: String? = null) -/** A store's cloud for one game. Every call is blocking network. */ interface CloudTransport { fun list(): List<CloudFile> - /** Fills in [CloudFile.modifiedMs] / [CloudFile.md5] where the listing left them out. */ fun details(file: CloudFile) {} - /** The file's bytes and its cloud time (-1 unknown). */ fun get(file: CloudFile): Pair<ByteArray, Long> - /** Writes [files] (key → bytes); the cloud times assigned, where the service says (-1 unknown). */ fun put(files: Map<String, ByteArray>): Map<String, Long> } @@ -60,15 +49,12 @@ internal fun isoDate(s: String?): Long { } catch (e: Exception) { -1L } } -/** Thrown when the store says the game has no cloud saves for this client. */ class NoCloudSaves : Exception("no cloud saves") -/** GOG cloudstorage: `/v1/<userId>/<clientId>[/<key>]`. */ class GogCloud(private val userId: String, private val clientId: String, private val token: String) : CloudTransport { private val base = "https://cloudstorage.gog.com/v1/$userId/$clientId" private val ua = "GOG Galaxy" - /** A path segment: '+' from a space is wrong in a path (it stores a second, '+'-named copy). */ private fun key(name: String) = URLEncoder.encode(name, "UTF-8").replace("+", "%20") override fun list(): List<CloudFile> { @@ -84,7 +70,6 @@ class GogCloud(private val userId: String, private val clientId: String, private val text = c.body().decodeToString().trim() c.disconnect() if (text.isEmpty() || text == "[]") return emptyList() - // A plain newline-separated list of keys (checked on device); a JSON array is read too. if (text.startsWith("[")) { val a = JSONArray(text) return (0 until a.length()).mapNotNull { a.optJSONObject(it) }.mapNotNull { o -> @@ -122,12 +107,10 @@ class GogCloud(private val userId: String, private val clientId: String, private val code = c.responseCode c.disconnect() if (code !in 200..299) throw java.io.IOException("GOG cloud put HTTP $code") - // The server stamps the object itself; that time, so the next sync sees the two as equal. CloudFile(name, -1L).also { details(it) }.modifiedMs } } -/** Epic savesync: `/api/v1/access/egstore/savesync/<accountId>/<appName>/`. */ class EpicCloud(private val accountId: String, private val appName: String, private val token: String) : CloudTransport { private val base = "https://datastorage-public-service-liveegs.live.use1a.on.epicgames.com/api/v1/access/egstore/savesync/$accountId/$appName/" private val ua = "EpicGamesLauncher/15.17.1-22692490" @@ -146,7 +129,6 @@ class EpicCloud(private val accountId: String, private val appName: String, priv }.toList() } - /** The listing names a file `<...>/<appName>/<key>`; the key is what is under the save folder. */ private fun strip(key: String): String { val marker = "/$appName/" val i = key.indexOf(marker) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/CredentialCipher.kt b/app/src/main/java/com/droiddeck/launcher/stores/CredentialCipher.kt index 2a7e84357..aa0748423 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/CredentialCipher.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/CredentialCipher.kt @@ -11,44 +11,24 @@ import javax.crypto.KeyGenerator import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec -/** - * The stores' sign-in files at rest: AES-256-GCM under a key that lives in the AndroidKeyStore and - * never leaves it, as a small versioned envelope `{"v":1,"alg":"AES/GCM","iv":...,"ct":...}` - * (base64). The session's Linux side shares the app's files directory but not its Keystore, so - * what it can see of a sign-in is the envelope only. - */ object CredentialCipher { const val VERSION = 1 const val ALG = "AES/GCM" private const val TRANSFORMATION = "AES/GCM/NoPadding" private const val TAG_BITS = 128 - /** Where the key comes from: the AndroidKeyStore on a device, a plain key in tests. */ fun interface KeyProvider { - /** The key, created on first use. Throws when no key can be had (no Keystore on this ROM). */ fun key(): SecretKey - /** The key that sealed earlier files: never a new one. Throws [KeyMissing] when there is none. */ fun existing(): SecretKey = key() } - /** The Keystore answered and holds no key: nothing sealed before can open again. */ class KeyMissing : java.security.GeneralSecurityException("no key") - /** - * Whether [e], from [open], means the file can never open: it does not authenticate (tampered, - * or sealed under another key), the key is permanently invalidated or gone, or the file is not - * an envelope. Anything else - the Keystore busy or erroring, an I/O error - may pass. - */ fun isPermanent(e: Throwable): Boolean = e is javax.crypto.AEADBadTagException || e is android.security.keystore.KeyPermanentlyInvalidatedException || e is KeyMissing || e is org.json.JSONException || e is IllegalArgumentException - /** - * An AES-256 key in the AndroidKeyStore: not exportable, encrypt/decrypt with GCM only, no user - * authentication (background downloads and launch-time code fetches use it with the screen - * off). StrongBox where the device has it, the TEE otherwise. - */ object Keystore : KeyProvider { private const val ALIAS = "droiddeck-store-credentials" private const val PROVIDER = "AndroidKeyStore" @@ -64,7 +44,7 @@ object CredentialCipher { val ks = KeyStore.getInstance(PROVIDER).apply { load(null) } (ks.getKey(ALIAS, null) as? SecretKey)?.let { return it } if (Build.VERSION.SDK_INT >= 28) { - try { return generate(strongBox = true) } catch (e: Exception) { /* no StrongBox: the TEE */ } + try { return generate(strongBox = true) } catch (e: Exception) { } } return generate(strongBox = false) } @@ -81,7 +61,6 @@ object CredentialCipher { } } - /** [plain] sealed into the envelope. The cipher picks the IV (the Keystore requires it). */ fun seal(plain: String, keys: KeyProvider): JSONObject { val cipher = Cipher.getInstance(TRANSFORMATION).apply { init(Cipher.ENCRYPT_MODE, keys.key()) } val ct = cipher.doFinal(plain.toByteArray(Charsets.UTF_8)) @@ -89,7 +68,6 @@ object CredentialCipher { .put("iv", b64(cipher.iv)).put("ct", b64(ct)) } - /** The text inside [envelope]. Throws when it is not ours, of another version, or does not authenticate. */ fun open(envelope: JSONObject, keys: KeyProvider): String { require(envelope.optInt("v", -1) == VERSION && envelope.optString("alg") == ALG) { "unknown envelope" } val iv = Base64.decode(envelope.getString("iv"), Base64.NO_WRAP) @@ -98,7 +76,6 @@ object CredentialCipher { return String(cipher.doFinal(ct), Charsets.UTF_8) } - /** Whether [json] is an envelope rather than plain credentials. */ fun isEnvelope(json: JSONObject): Boolean = json.has("v") && json.has("ct") && json.has("iv") private fun b64(bytes: ByteArray) = Base64.encodeToString(bytes, Base64.NO_WRAP) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/GameFacts.kt b/app/src/main/java/com/droiddeck/launcher/stores/GameFacts.kt index e67922fbb..88b0f0105 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/GameFacts.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/GameFacts.kt @@ -11,38 +11,27 @@ import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.concurrent.ConcurrentHashMap -/** - * What other sites know of a store game, looked up by its title: the Metacritic score (as Steam's - * store search carries it) and HowLongToBeat's times. Neither store publishes these, so every - * lookup is a name match against someone else's catalog, kept on disk a fortnight (a miss for two - * days) and in memory for the process. Blocking: call off the main thread. - */ object GameFacts { private const val TAG = "GameFacts" private const val HIT_TTL_MS = 14L * 24 * 60 * 60 * 1000 private const val MISS_TTL_MS = 2L * 24 * 60 * 60 * 1000 private const val TIMEOUT_MS = 10_000 - /** Metacritic's critic score (0-100) and the game's page there. */ data class Metacritic(val score: Int, val url: String) - /** HowLongToBeat's community times, in seconds (0 when nobody has logged that kind of run), and the game's page there. */ data class Hltb(val main: Int, val extras: Int, val completionist: Int, val url: String) { val isEmpty: Boolean get() = main <= 0 && extras <= 0 && completionist <= 0 } - /** A remembered answer: [value] null is a remembered miss. */ private class Entry<T>(val value: T?, val at: Long) { fun fresh(now: Long) = now - at < if (value != null) HIT_TTL_MS else MISS_TTL_MS } private val memory = ConcurrentHashMap<String, Entry<*>>() - /** The score or the times already in memory, without touching disk or network; null when not yet known. */ fun peekMetacritic(title: String): Lookup<Metacritic>? = peek("metacritic", title) fun peekHltb(title: String): Lookup<Hltb>? = peek("hltb", title) - /** The score, or null when Steam does not list the game or has no score for it. */ fun metacritic(context: Context, title: String): Metacritic? = remembered(context, "metacritic", title, read = { o -> o.optInt("score").takeIf { it > 0 }?.let { Metacritic(it, o.optString("url").ifEmpty { metacriticSearch(title) }) } }, @@ -55,7 +44,6 @@ object GameFacts { write = { o, v -> o.put("main", v.main).put("extras", v.extras).put("completionist", v.completionist).put("url", v.url) }, ) { searchHltb(title) } - // Where a game goes when its own page is not known: the site's search for its name. private fun metacriticSearch(title: String) = "https://www.metacritic.com/search/${encodePath(TitleMatch.searchTerm(title))}/?category=13" private fun hltbSearch(title: String) = "$HLTB/?q=${URLEncoder.encode(TitleMatch.searchTerm(title), "UTF-8")}" private fun encodePath(s: String) = URLEncoder.encode(s, "UTF-8").replace("+", "%20") @@ -66,10 +54,6 @@ object GameFacts { return e.value?.let { Lookup.Found(it) } ?: Lookup.None } - /** - * The memory copy, else the disk copy while fresh, else [fetch]. [fetch] answers a [Fetched]: - * a failed request is not a miss, so it leaves the stale copy (if any) standing. - */ @Suppress("UNCHECKED_CAST") private fun <T : Any> remembered( context: Context, kind: String, title: String, @@ -107,8 +91,6 @@ object GameFacts { private fun fileName(title: String) = TitleMatch.normalize(title).take(80).ifEmpty { "_" + title.hashCode().toUInt() } - // Steam's store search lists a game's Metacritic score beside its name: one request, no key. - // The game's own Metacritic page is in its app details, asked for only once there is a score. private fun searchSteamScore(title: String): Fetched<Metacritic> { val term = URLEncoder.encode(TitleMatch.searchTerm(title), "UTF-8") val body = StoreNet.get("https://store.steampowered.com/api/storesearch/?l=english&cc=US&term=$term", timeoutMs = TIMEOUT_MS) @@ -123,7 +105,6 @@ object GameFacts { }.getOrElse { Fetched.Failed } } - /** Steam's link to the game on Metacritic, without its tracking tag; null when Steam gives none. */ private fun metacriticPage(appId: Long): String? { if (appId <= 0) return null val body = StoreNet.get("https://store.steampowered.com/api/appdetails?appids=$appId&filters=metacritic", timeoutMs = TIMEOUT_MS) ?: return null @@ -133,8 +114,6 @@ object GameFacts { }.getOrNull() } - // HowLongToBeat's own site search: a short-lived token from init, sent back as x-auth-token. - // The token is bound to the User-Agent that asked for it, so both calls send the same one. private const val HLTB = "https://howlongtobeat.com" private val hltbHeaders = mapOf("Referer" to "$HLTB/", "Origin" to HLTB) @Volatile private var hltbToken: Pair<String, Long>? = null @@ -162,7 +141,6 @@ object GameFacts { .put("filter", "").put("sort", 0).put("randomizer", 0)) .put("useCache", true) .toString() - // A token can lapse between init and search; one fresh one is worth a second try. var body: String? = null for (renew in listOf(false, true)) { val token = hltbToken(renew) ?: return Fetched.Failed @@ -185,16 +163,13 @@ object GameFacts { } } - /** A store's release date ("2015.05.19", "2015-05-19T00:00:00+02:00", ...) in the reader's own words, or null. */ fun releaseLabel(raw: String): String? { val m = Regex("""^(\d{4})[.\-/](\d{2})[.\-/](\d{2})""").find(raw.trim()) ?: return null val (y, mo, d) = m.destructured - // Epic dates a title it has no date for 2099-01-01. val date = runCatching { LocalDate.of(y.toInt(), mo.toInt(), d.toInt()) }.getOrNull()?.takeIf { it.year in 1970..2098 } ?: return null return date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) } - /** Whole hours, with a half when the time is nearer one: "51½". Under an hour, null (the caller says minutes). */ fun hours(seconds: Int): String? { if (seconds < 3600) return null val halves = Math.round(seconds / 1800.0).toInt() @@ -202,38 +177,26 @@ object GameFacts { } } -/** A fact looked up off the page: still asking, found, or not to be had. */ sealed interface Lookup<out T> { object Pending : Lookup<Nothing> object None : Lookup<Nothing> data class Found<T>(val value: T) : Lookup<T> } -/** - * Whether two catalogs mean the same game. Store titles carry marks, editions and subtitles the - * other site spells differently ("The Witcher 3: Wild Hunt" is Steam's "...Wild Hunt — Remastered"); - * a sequel is never the same game ("Hades" is not "Hades II"). - */ internal object TitleMatch { private val MARKS = Regex("[™®©]") private val EDITION = Regex( "(?i)[\\s:\\-–—]*\\b(?:(?:complete|definitive|legendary|deluxe|ultimate|enhanced|gold|premium|standard|special|anniversary|collector'?s|digital deluxe|game of the year|goty)\\s+edition|goty|director'?s cut)\\b", ) - // Where a subtitle starts: a colon or dash with space around it, or an opening bracket. private val SUBTITLE = Regex("\\s*[:–—]\\s*|\\s+-\\s+|\\s*\\(") fun normalize(s: String): String = s.lowercase().replace(MARKS, "").replace("&", "and").replace(Regex("[^\\p{L}\\p{N}]"), "") fun stripEdition(s: String): String = s.replace(EDITION, " ").trim().ifEmpty { s } - /** - * What to type into another site's search: the title without its marks, edition or subtitle - * punctuation (Steam finds nothing for "Disco Elysium - The Final Cut", all of it for the words). - */ fun searchTerm(title: String): String = stripEdition(title.replace(MARKS, "")).replace(Regex("\\s+[-–—]\\s+|[:–—()\\[\\]]"), " ").replace(Regex("\\s+"), " ").trim() - /** 3 the same name, 2 the same but for the edition, 1 one is the other with a subtitle, 0 not the same game. */ fun score(want: String, got: String): Int { val w = normalize(want) val g = normalize(got) @@ -246,11 +209,9 @@ internal object TitleMatch { return 0 } - /** The title up to each place a subtitle could start, normalized. */ private fun heads(title: String): Set<String> = SUBTITLE.findAll(title).map { normalize(title.substring(0, it.range.first)) }.filter { it.isNotEmpty() }.toSet() - /** The best-named of [items] in their order (the site's own ranking breaks a tie), or null when none is the game. */ fun <T> pick(want: String, items: List<T>, names: (T) -> List<String>): T? { var best: T? = null var bestScore = 0 diff --git a/app/src/main/java/com/droiddeck/launcher/stores/LoginFlows.kt b/app/src/main/java/com/droiddeck/launcher/stores/LoginFlows.kt index 54f31f751..7a4f579ae 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/LoginFlows.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/LoginFlows.kt @@ -5,7 +5,6 @@ import android.util.Log import android.webkit.WebView import com.droiddeck.launcher.stores.gog.GogAuth -/** The three stores' sign-in flows for [StoreLoginActivity]. */ internal object LoginFlows { private const val TAG = "StoreLogin" @@ -15,15 +14,9 @@ internal object LoginFlows { Store.AMAZON -> amazonFlow() } - /** Set by the Epic and Amazon backends when they are in the build; null keeps the chip's card honest. */ @Volatile var epicFlow: () -> StoreLoginActivity.Flow? = { null } @Volatile var amazonFlow: () -> StoreLoginActivity.Flow? = { null } - /** - * GOG's implicit grant: the token arrives in the redirect URL's fragment. The CSRF state is - * checked only when GOG echoes one back - its social-login path returns none, and treating - * that as a mismatch threw away good tokens. - */ private class GogFlow(private val state: String) : StoreLoginActivity.Flow { override val startUrl: String get() = GogAuth.authUrl(state) override val userAgent: String get() = GogAuth.GALAXY_UA @@ -39,7 +32,6 @@ internal object LoginFlows { } val access = frag.getQueryParameter("access_token") if (access.isNullOrEmpty()) { - // The keys only, never the values: the fragment carries live tokens. Log.w(TAG, "gog: redirect without access_token; keys=${frag.queryParameterNames}") return activity.getString(com.droiddeck.launcher.R.string.stores_login_error_verification) } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/SizeCache.kt b/app/src/main/java/com/droiddeck/launcher/stores/SizeCache.kt index c89df6b52..ca1078058 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/SizeCache.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/SizeCache.kt @@ -3,12 +3,6 @@ package com.droiddeck.launcher.stores import java.io.File import org.json.JSONObject -/** - * Game sizes as found, kept per game AND the version they were found for, in - * `filesDir/stores/<store>/sizes.json`: a size is never fetched twice for one version, and a - * new version (a library refresh brings it) simply misses and is looked up again. The file carries - * its format; one in another format is discarded, not read. - */ class SizeCache(private val file: File) { private val sizes = HashMap<String, Pair<String, Long>>() diff --git a/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt b/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt index 250a37346..4f0bcfce3 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt @@ -20,23 +20,11 @@ import java.net.Socket import java.net.URL import java.security.SecureRandom -/** - * Adds and removes a store game's shortcut in a Steam client that is running right now, so it - * shows without waiting for the next client start. The runtime starts the client with its - * DevTools port open (droiddeck-session writes it to session/agent/cdp-port) and the client's own - * JavaScript API has `SteamClient.Apps.AddShortcut`; this speaks the small part of the DevTools - * protocol that one `Runtime.evaluate` needs, over a plain socket, nothing else. - * - * Best-effort throughout: no session, no port, no SharedJSContext, or an API that moved and the - * shortcut simply arrives with the shortcuts writer at the next start. The appid Steam derives - * (CRC of the quoted exe plus the name) is the one the writer computes, so the two never disagree. - */ object SteamLiveShortcuts { private const val TAG = "SteamLive" private const val TAG_NAME = "droiddeck-app" private const val COMPAT_TOOL = "droiddeck-proton-arm64" - /** Adds every game in [games] that [include] takes (store games by default) and the client does not list yet. Blocking; worker thread. */ fun sync(context: Context, games: List<AddedGames.Game>, include: (AddedGames.Game) -> Boolean = { it.source != Library.ADDED }) { val wanted = games.filter(include) if (wanted.isEmpty() || !clientRunning()) return @@ -64,17 +52,12 @@ object SteamLiveShortcuts { if (result != "0") StoresState.logLine("Steam: $result shortcut(s) added to the running client") } - /** Removes the shortcut with [appId] from the running client. Blocking; worker thread. */ fun remove(context: Context, appId: Long) { if (!clientRunning()) return val result = evaluate(context, "(async () => { await SteamClient.Apps.RemoveShortcut(${appId and 0xFFFFFFFFL}); return 1; })()") if (result != null) Log.i(TAG, "live shortcuts: removed $appId") } - /** - * The running client's shortcut for [game] made to match it: name, target, Start in and - * launch options, on the same appid (an edit never adds a second shortcut). Blocking. - */ fun update(context: Context, game: AddedGames.Game) { if (!clientRunning()) return val id = game.appId and 0xFFFFFFFFL @@ -91,11 +74,6 @@ object SteamLiveShortcuts { if (evaluate(context, js) != null) Log.i(TAG, "live shortcuts: updated $id") } - /** - * One piece of art for the running client's shortcut [appId]: [assetType] as the client numbers - * them (0 portrait capsule, 1 hero, 2 logo, 3 wide capsule), from [file]; null clears it. The - * icon goes by path ([iconPath]). Blocking. - */ fun setArt(context: Context, appId: Long, assetType: Int?, file: java.io.File?, iconPath: String? = null) { if (!clientRunning()) return val id = appId and 0xFFFFFFFFL @@ -114,7 +92,6 @@ object SteamLiveShortcuts { private fun clientRunning(): Boolean = SessionState.running && SessionState.mode == SessionService.MODE_STEAM && !SessionState.stopRequested - /** `Runtime.evaluate` of [expression] in the client's SharedJSContext; the value as text, or null. */ private fun evaluate(context: Context, expression: String): String? { val portFile = File(LinuxRuntime.sessionRoot(context), "agent/cdp-port") val port = runCatching { portFile.readText().trim().toInt() }.getOrNull() ?: return null @@ -152,7 +129,6 @@ object SteamLiveShortcuts { try { return c.inputStream.bufferedReader().readText() } finally { c.disconnect() } } - /** The least of RFC 6455 a localhost DevTools talk needs: upgrade, masked text out, text in. */ private class WebSocket(url: String, timeoutMs: Int) : AutoCloseable { private val socket: Socket private val out: OutputStream @@ -198,7 +174,6 @@ object SteamLiveShortcuts { out.write(frame.toByteArray()); out.flush() } - /** The next text message (fragments joined); null on close. */ fun receiveText(): String? { val message = ByteArrayOutputStream() while (true) { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/Store.kt b/app/src/main/java/com/droiddeck/launcher/stores/Store.kt index 9ff97654b..3288e1c6e 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/Store.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/Store.kt @@ -1,9 +1,5 @@ package com.droiddeck.launcher.stores -/** - * The three storefronts the Stores section talks to. [id] is what the sidecar, the prefs file - * names and the Games badge carry; it never changes once a game is installed under it. - */ enum class Store(val id: String, val label: String, val shortLabel: String) { GOG("gog", "GOG", "GOG"), EPIC("epic", "Epic Games", "Epic"), diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreAccounts.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreAccounts.kt index 77599e0a4..6b5f30186 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreAccounts.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreAccounts.kt @@ -6,22 +6,6 @@ import androidx.annotation.VisibleForTesting import org.json.JSONObject import java.io.File -/** - * Each store's sign-in, kept in the app's private files: `filesDir/stores/<store>/credentials.json`. - * Tokens never go to shared storage and never into a log; the file holds whatever the store's auth - * flow handed back (access and refresh tokens, expiry, the account's display name, and for Amazon - * the device registration), as one JSON object the store's own client reads and writes - sealed - * with [CredentialCipher] under a key in the AndroidKeyStore. Every read and write of a sign-in - * goes through here. - * - * - A plain file an earlier build wrote is sealed the first time it is read ([encryptAll] at app - * start), verified by opening it again, and the plain text is replaced. - * - Where the Keystore does not work (some ROMs), the file stays plain, the log says so once, and - * the next start tries again. - * - A sealed file that can never open again (the key is gone: data cleared, a backup restored on - * another device) is deleted and the store reads as signed out; one that fails for a reason that - * may pass is kept and tried again (see [read]). - */ object StoreAccounts { private const val TAG = "StoreAccounts" private const val FILE = "credentials.json" @@ -30,32 +14,19 @@ object StoreAccounts { @Volatile internal var keys: CredentialCipher.KeyProvider = CredentialCipher.Keystore - /** The Keystore failed once in this process: files stay plain until the next start tries again. */ @VisibleForTesting @Volatile internal var keystoreFailed = false - /** The store's private folder: credentials, caches, the engine's scratch. */ fun dir(context: Context, store: Store): File = File(File(context.filesDir, "stores"), store.id).apply { mkdirs() } fun credentialsFile(context: Context, store: Store): File = File(dir(context, store), FILE) - /** Stores whose sealed sign-in could not be opened this time for a reason that may pass (see [read]). */ private val unavailable = java.util.concurrent.ConcurrentHashMap.newKeySet<Store>() @Volatile private var unavailableLogged = false - /** - * The stored credentials, or null when signed out or unreadable (never logged: the text is the - * secret). - * - * A sealed file is deleted - the store signs in again - only when it can never open: it does - * not authenticate (tampered, or another key: data cleared, restored on another device), the - * key is permanently invalidated or gone from the Keystore, or it is not an envelope at all. Any - * other failure (the Keystore busy or erroring, an I/O error) keeps the file, marks the store - * [isUnavailable] - signed in, its actions failing for now - and the next read tries again. - */ @Synchronized fun read(context: Context, store: Store): JSONObject? { val f = credentialsFile(context, store) @@ -74,7 +45,6 @@ object StoreAccounts { } } - /** The store's sign-in is on disk but could not be opened this time (see [read]). */ fun isUnavailable(store: Store): Boolean = store in unavailable private fun unreadable(f: File, store: Store, e: Exception): JSONObject? { @@ -107,30 +77,19 @@ object StoreAccounts { File(credentialsFile(context, store).path + ".tmp").delete() } - /** At app start: every plain sign-in an earlier build left is sealed now. */ fun encryptAll(context: Context) { Store.entries.forEach { store -> - // An older build's temp file held the plain text too. File(credentialsFile(context, store).path + ".tmp").delete() read(context, store) } } - /** - * The account's display name when signed in (the store's own name for it, or a stand-in), else - * null. A sign-in that could not be opened this time ([isUnavailable]) still counts, under the - * store's name, so the sign-in card does not ask for what is still there. - */ fun signedInAs(context: Context, store: Store): String? { val json = read(context, store) ?: return if (isUnavailable(store)) store.label else null if (json.optString("access_token", "").isEmpty()) return null return json.optString("display_name", "").ifEmpty { json.optString("username", "") }.ifEmpty { store.label } } - /** - * [json] sealed into [f], checked by opening what was written. Null - and [f] as it was - when - * the Keystore cannot seal or the check fails. - */ private fun seal(f: File, json: JSONObject): File? { if (keystoreFailed) return null val text = json.toString() @@ -145,7 +104,6 @@ object StoreAccounts { return f } - /** [text] into [f] through a temp file and a rename, readable and writable by the app alone. */ private fun put(f: File, text: String) { val tmp = File(f.path + ".tmp") tmp.writeText(text) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreAchievements.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreAchievements.kt index 9c1eb5fdc..b5fea11a5 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreAchievements.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreAchievements.kt @@ -13,13 +13,8 @@ import java.time.Instant import java.time.OffsetDateTime import java.util.Locale -/** - * A source's achievements with the account's progress, the last good answer cached on disk. Blocking. - * [source] is the library's id for where a game came from ("gog", "epic"; "steam" once it has one). - */ abstract class StoreAchievements(val source: String, val label: String) { - /** [extra]: what the source needs to find the game (Epic: "namespace"). */ data class Game(val source: String, val id: String, val extra: Map<String, String> = emptyMap()) { companion object { fun of(item: CatalogItem) = Game(item.store.id, item.id, item.extra) @@ -33,7 +28,6 @@ abstract class StoreAchievements(val source: String, val label: String) { val description: String, val hidden: Boolean, val unlocked: Boolean, - /** Unix seconds; 0 when locked or unknown. */ val unlockedAt: Long, val icon: String?, val iconLocked: String?, @@ -69,7 +63,6 @@ abstract class StoreAchievements(val source: String, val label: String) { protected fun locale(): String = Locale.getDefault().toLanguageTag() - /** ISO-8601 (including GOG's "+0000" offsets) to Unix seconds; 0 if unreadable. */ protected fun epochSeconds(iso: String?): Long { if (iso.isNullOrBlank()) return 0 val normalized = iso.replace(Regex("([+-]\\d{2})(\\d{2})$"), "$1:$2") @@ -102,7 +95,6 @@ abstract class StoreAchievements(val source: String, val label: String) { private val all: List<StoreAchievements> by lazy { listOf(GogAchievements, EpicAchievements) } - /** Null for a source without achievements. */ fun forSource(source: String?): StoreAchievements? = all.firstOrNull { it.source == source } } } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreArt.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreArt.kt index 4726c2db7..4b2b0b803 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreArt.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreArt.kt @@ -9,17 +9,9 @@ import java.io.File import java.net.HttpURLConnection import java.net.URL -/** - * The store's art, placed in the game's folder under the names AddedGameArt already looks for - - * `cover.jpg` (the portrait capsule), `hero.jpg` and `header.jpg` (the wide art) - so a store game - * shows real art in the Games tab and the session's shortcuts writer copies it into the client's - * grid. Each image is decoded and re-encoded as an opaque JPEG over a dark ground: a store's PNG - * with an alpha fade would otherwise end in white, which is what the washed-out cards were. - */ object StoreArt { private const val TAG = "StoreArt" - /** Fetches what the sidecar names; a missing or unreadable image is skipped, never an error. Blocking. */ fun fetchInto(folder: File, sidecar: StoreGameSidecar) { sidecar.cover?.let { place(it, File(folder, "cover.jpg")) } val wide = sidecar.hero ?: return diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt index 88d6695d5..98b631827 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreCatalog.kt @@ -1,49 +1,31 @@ package com.droiddeck.launcher.stores -/** - * One title as the Stores page draws it, whichever store produced it: a card on a shelf, a tile - * in a grid, the hero of a game page. Library games and storefront results both project into - * this, so one card composable serves every tab. - */ data class CatalogItem( val store: Store, - /** Store-native id: GOG product id, Epic catalog item id (offer id for storefront results), Amazon product id. */ val id: String, val title: String, - /** Wide (16:9-ish) art for cards and heroes. */ val imageUrl: String?, - /** Tall (2:3) box art when the store publishes one; falls back to [imageUrl]. */ val tallImageUrl: String? = null, - /** Comma-joined genre / tag line. */ val tags: String = "", val isFree: Boolean = false, - /** False when the endpoint gave no price at all: the price row then draws nothing. */ val hasPrice: Boolean = false, - /** Pre-formatted by the store ("$9.99"). */ val finalPrice: String = "", val originalPrice: String = "", val discountPercent: Int = 0, - /** The web product page: where "Get for free" and "View on <store>" land. */ val storeUrl: String = "", val developer: String = "", val publisher: String = "", val releaseDate: String = "", val description: String = "", - /** True for a title the signed-in account owns (a library entry). */ val owned: Boolean = false, - /** The install size in bytes when known, else 0. */ val sizeBytes: Long = 0L, - /** Per-store identifiers the install needs (Epic namespace / catalog id / app name, Amazon entitlement / sku). */ val extra: Map<String, String> = emptyMap(), - /** Rated or tagged for adults by the store's own data; hidden from the storefront unless Show mature content is on. */ val mature: Boolean = false, ) { val isDiscounted: Boolean get() = discountPercent > 0 && originalPrice.isNotBlank() - /** Stable key across stores ("gog:1207658924"). */ val key: String get() = "${store.id}:$id" } -/** A store's storefront shelves: what the Store tab shows above the account's own library. */ class StoreShelves( val whatsNew: List<CatalogItem> = emptyList(), val deals: List<CatalogItem> = emptyList(), @@ -54,24 +36,12 @@ class StoreShelves( val all: List<CatalogItem> get() = (whatsNew + deals + free + trending).distinctBy { it.id } } -/** A game a store installed, as found on disk through its sidecar. */ -/** Store tags that mark adult content, by slug or name, lowercased. */ private val MATURE_TAGS = setOf("mature", "nsfw", "adult", "adult only", "adults only", "nudity", "sexual content", "sexual-content") -/** Whether a store's own tags mark a title as adult. */ fun matureByTags(tags: Collection<String>): Boolean = tags.any { it.trim().lowercase() in MATURE_TAGS } -/** - * Whether a store's own age ratings mark a title as adult: ESRB M (17) or AO (18), PEGI / USK / the - * store's own rating 18, or any rating system's 17+. Ratings are ages, as GOG gives them. - */ fun matureByAge(ages: Collection<Int>): Boolean = ages.any { it >= 17 } -/** - * The Installed tab's cards for [store], from the installs on disk - the same source as its count: - * each joined to its library item by store id (by title when the ids differ), else a card made - * from the sidecar's own title and art, so an install shows even before the library has loaded. - */ fun installedCards(store: Store, installed: List<InstalledStoreGame>, library: List<CatalogItem>): List<CatalogItem> { val byId = library.associateBy { it.id } val byTitle = library.associateBy { it.title.lowercase() } @@ -82,11 +52,6 @@ fun installedCards(store: Store, installed: List<InstalledStoreGame>, library: L }.distinctBy { it.id }.sortedBy { it.title.lowercase() } } -/** - * A refreshed list with every unchanged item kept as the very object already shown, so a refresh - * that lands while the grid is on screen recomposes only the cards that changed - no reset, the - * scroll position and the pad's focus stay. - */ fun mergeItems(old: List<CatalogItem>?, new: List<CatalogItem>): List<CatalogItem> { if (old.isNullOrEmpty()) return new val before = old.associateBy { it.key } @@ -103,12 +68,6 @@ private val HTML_TAG = Regex("<[^>]+>") private val ENTITY = Regex("&(#\\d+|#x[0-9a-fA-F]+|[a-zA-Z]+);") private val NAMED_ENTITIES = mapOf("amp" to "&", "lt" to "<", "gt" to ">", "quot" to "\"", "apos" to "'", "nbsp" to " ", "ndash" to "–", "mdash" to "—", "hellip" to "…", "trade" to "™", "reg" to "®", "copy" to "©") -/** - * Store copy as plain text: GOG's account endpoints return template keys - * (`product_description_2015545325`) for some products instead of words, and every store's - * descriptions carry HTML. Those keys go, tags go, entities are decoded, whitespace is collapsed; - * "" when nothing readable is left, so a page shows nothing rather than a placeholder. - */ fun cleanStoreText(raw: String?): String { if (raw.isNullOrBlank()) return "" var s = raw.replace(TEMPLATE_KEY, " ") @@ -124,7 +83,6 @@ fun cleanStoreText(raw: String?): String { return s.lines().map { it.replace(Regex("[ \\t\\u00A0]+"), " ").trim() }.filter { it.isNotEmpty() }.joinToString("\n") } -/** Human sizes the way the Downloads page and the cards print them ("810.2 MB", "3.9 GB"). */ fun formatBytes(bytes: Long): String = when { bytes >= 1_073_741_824L -> "%.1f GB".format(bytes / 1_073_741_824.0) bytes >= 1_048_576L -> "%.1f MB".format(bytes / 1_048_576.0) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreExe.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreExe.kt index f1a97541b..ad2d478c1 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreExe.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreExe.kt @@ -2,12 +2,6 @@ package com.droiddeck.launcher.stores import java.io.File -/** - * Which .exe in an install folder is the game. The store's own hint (a manifest's launch - * executable) wins when the file exists; else the candidates, helpers and installers left out, - * the one named most like the title first, then the shallowest. Relative to the folder, forward - * slashes; "" when the folder has none. - */ object StoreExe { private val SKIP = listOf("redist", "unins", "setup", "crash", "report", "helper", "dotnet", "vcredist", "directx", "eac", "easyanticheat", "battleye", "launcher", "updater", "config") diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt index 149b6c455..1753003d5 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt @@ -5,25 +5,6 @@ import org.json.JSONArray import org.json.JSONObject import java.io.File -/** - * What a store install leaves beside the game so the rest of the app can treat the folder like - * any added game: `.droiddeck-store.json` in the game's folder. - * - * [AddedGames.scanGame] reads it to pick the exe (instead of guessing from the folder), to name - * the shortcut after the store's title and to tag the game with its [store]. [launcher] is the - * small .bat the install writes when the game needs arguments or environment at launch (Epic's - * identity arguments, Amazon's FuelPump variables): a Steam shortcut can only carry a fixed Exe, - * so the launcher is what the shortcut runs and [exe] is what it starts. Every store install is a - * Steam shortcut; there is no switch for it (an `addToSteam` field in an older sidecar is ignored). - * - * Paths are relative to the game folder with forward slashes, so the folder can move between - * storage volumes and stay valid. - * - * An install writes the sidecar twice: at its start with [state] `installing` (store, id, title - - * no exe yet), and at its end with the exe, the launcher and `installed`. A folder whose sidecar - * still says `installing` is an unfinished install, never a game: the scan leaves it out and the - * Stores page offers to resume it. A sidecar without the field (an earlier build) is installed. - */ class StoreGameSidecar( val store: Store, val id: String, @@ -36,12 +17,9 @@ class StoreGameSidecar( val installedAt: Long = 0L, val cover: String? = null, val hero: String? = null, - /** Per-store identifiers the launcher needs again (Epic namespace / catalog id, Amazon entitlement). */ val extra: Map<String, String> = emptyMap(), val state: String = STATE_INSTALLED, - /** An Epic game's launch choices (the Games tab's Epic card); both launch paths read them from here. */ val epic: EpicOptions = EpicOptions(), - /** GOG / Epic cloud saves for this game: down before a launch, up after it (the game card's switch). */ val cloud: Boolean = true, val achievements: Boolean = true, ) { @@ -93,17 +71,11 @@ class StoreGameSidecar( fun file(folder: File): File = File(folder, FILE_NAME) - /** An installed game's sidecar; walks the install folders. */ fun find(context: Context, store: Store, id: String): StoreGameSidecar? = StoreInstallRoot.gameFolders(context.applicationContext).asSequence() .mapNotNull { read(it) } .firstOrNull { it.store == store && it.id == id } - /** - * Changes an Epic game's launch choices on disk: reads the sidecar fresh (never a copy held - * by a screen), applies [change], writes it, and reads it back. The written sidecar, or null - * when there is none or the write did not take. Blocking. - */ fun updateEpic(folder: File, change: (EpicOptions) -> EpicOptions): StoreGameSidecar? { val current = read(folder) ?: return null val next = current.copy(epic = change(current.epic)) @@ -111,7 +83,6 @@ class StoreGameSidecar( return read(folder)?.takeIf { it.epic == next.epic } } - /** As [updateEpic], for the cloud-saves switch. */ fun updateCloud(folder: File, on: Boolean): StoreGameSidecar? { val current = read(folder) ?: return null current.copy(cloud = on).write(folder) @@ -124,14 +95,12 @@ class StoreGameSidecar( return read(folder)?.takeIf { it.achievements == on } } - /** The sidecar in [folder], or null when there is none or it cannot be read. */ fun read(folder: File): StoreGameSidecar? { val f = file(folder) if (!f.isFile) return null return runCatching { parse(f.readText()) }.getOrNull() } - /** Pure parse; null for anything that is not a usable sidecar (so a scan falls back to guessing). */ fun parse(text: String): StoreGameSidecar? { val o = runCatching { JSONObject(text) }.getOrNull() ?: return null val store = Store.byId(o.optString("store")) ?: return null @@ -140,9 +109,7 @@ class StoreGameSidecar( val exe = o.optString("exe", "").replace('\\', '/') val state = o.optString("state", STATE_INSTALLED).ifEmpty { STATE_INSTALLED } if (id.isEmpty() || title.isEmpty()) return null - // An unfinished install has no exe yet; a finished one must name one. if (exe.isEmpty() && state == STATE_INSTALLED) return null - // A relative path only: a sidecar that points outside its folder is not trusted. if (exe.isNotEmpty() && !relativeInside(exe)) return null val launcher = o.optString("launcher", "").replace('\\', '/').takeIf { it.isNotEmpty() && relativeInside(it) } val args = o.optJSONArray("args")?.let { a -> List(a.length()) { a.optString(it) }.filter { it.isNotEmpty() } } ?: emptyList() @@ -158,24 +125,17 @@ class StoreGameSidecar( hero = o.optString("hero", "").ifEmpty { null }, extra = extra, state = state, - // An "overlay" (or "v") field from an earlier build is read past: there is no overlay. epic = o.optJSONObject("epic")?.let { e -> EpicOptions(eos = e.optBoolean("eos", true), offline = e.optBoolean("offline", false)) } ?: EpicOptions(), cloud = o.optBoolean("cloud", true), achievements = o.optBoolean("achievements", true), ) } - /** Relative, no drive letter, no step up: an absolute or escaping path is refused, not trimmed into shape. */ private fun relativeInside(path: String): Boolean = path.isNotEmpty() && !path.startsWith("/") && !path.contains(":") && path.split('/').none { it == ".." || it.isEmpty() } } } -/** - * An Epic game's launch choices. [eos]: pass a fresh exchange code (Epic sign-in). [offline]: start - * with the Epic identity arguments only, no code - for a game that will not start when sign-in fails. - */ data class EpicOptions(val eos: Boolean = true, val offline: Boolean = false) { - /** Whether a launch asks for a code at all. */ val wantsCode: Boolean get() = eos && !offline } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt index 468f92419..e122aa057 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstallRoot.kt @@ -8,52 +8,31 @@ import com.droiddeck.launcher.session.GameStorage import java.io.File import java.util.zip.CRC32 -/** - * Where store games are installed: `<root>/<Store>/<title>`. - * - * The default root is the app's own internal storage - the runtime's tree, where the folder sits at - * /root/Games/Stores and needs no bind - whatever the session's Game storage setting says. An SD - * card the app may write (its app folder, `GameStorage.options`) is offered as a second root at - * Install time and never chosen silently. Every root is scanned, so a game stays in the library - * wherever it went. A card that is the Steam library is already bound (`/mnt/droiddeck-sd`); any - * other card's root is bound at a fixed place of its own (`/mnt/droiddeck-stores/<volume>`), so a - * shortcut's path - and with it its appid - stays the same from one session to the next. - */ object StoreInstallRoot { private const val INTERNAL = "root/Games/Stores" private const val ON_VOLUME = "Games" const val GUEST_EXTERNAL = "/mnt/droiddeck-stores" - /** One place a game can be installed: the folder, its name for the dialog, and whether it is a card. */ class Target(val root: File, val label: String, val removable: Boolean) { val freeBytes: Long get() = try { StatFs((if (root.isDirectory) root else root.parentFile ?: root).path).availableBytes } catch (e: Exception) { 0L } } - /** The internal root: always there, always the default. */ fun internalRoot(context: Context): File = File(LinuxRuntime.rootDir(context), INTERNAL) - /** The roots a game can go to right now: internal first, then every writable card. */ fun targets(context: Context): List<Target> { val internal = Target(internalRoot(context), context.getString(com.droiddeck.launcher.R.string.games_internal), removable = false) val cards = GameStorage.options(context).map { Target(File(it.path, ON_VOLUME), it.name, removable = true) } return listOf(internal) + cards } - /** The default root, when nothing asks: internal storage. */ fun installRoot(context: Context): File = internalRoot(context) - /** Every root that may hold store games: the internal one, every card's, and the Steam library's (older installs). */ fun roots(context: Context): List<File> { val cards = GameStorage.options(context).map { File(it.path, ON_VOLUME) } val library = GameStorage.effective(context)?.let { File(it.path, ON_VOLUME) } return (listOf(internalRoot(context)) + cards + listOfNotNull(library)).distinctBy { it.absolutePath } } - /** - * The card roots that need a bind of their own, with where the session sees them. A root under - * the Steam library is left out: the library's bind already covers it, and the games there keep - * the guest path (and appid) they were registered with. - */ fun externalRoots(context: Context): List<Pair<File, String>> { val library = GameStorage.effective(context)?.path return roots(context) @@ -61,7 +40,6 @@ object StoreInstallRoot { .map { it to guestFor(context, it) } } - /** A card root's guest path, keyed on the volume (its UUID, else a hash of the path), so it is the same across sessions. */ private fun guestFor(context: Context, root: File): String { val sm = context.getSystemService(StorageManager::class.java) val uuid = runCatching { sm?.getStorageVolume(root)?.uuid }.getOrNull()?.takeIf { it.isNotBlank() }?.replace(Regex("[^A-Za-z0-9-]"), "") @@ -71,7 +49,6 @@ object StoreInstallRoot { fun isInternal(context: Context, root: File): Boolean = root.absolutePath == internalRoot(context).absolutePath - /** Where a host path under a card root appears inside the session, or null when it is not under one. */ fun externalGuestPath(context: Context, host: File): String? { val path = host.absolutePath for ((root, guest) in externalRoots(context)) { @@ -82,7 +59,6 @@ object StoreInstallRoot { return null } - /** The target a game folder sits under, for the page and the Downloads row; internal when unknown. */ fun labelFor(context: Context, folder: File): String { val path = folder.absolutePath for (t in targets(context)) if (path.startsWith(t.root.absolutePath + "/")) return t.label @@ -93,23 +69,15 @@ object StoreInstallRoot { fun storeDir(root: File, store: Store): File = File(root, store.shortLabel) - /** The game folders under every root, by store: what AddedGames scans in addition to the user's folders. */ fun gameFolders(context: Context): List<File> = roots(context).flatMap { root -> Store.entries.flatMap { store -> storeDir(root, store).listFiles { f -> f.isDirectory }?.sortedBy { it.name.lowercase() } ?: emptyList() } } - /** - * The folder a game installs into under [root]. An existing folder wins over a fresh one, so a - * repair, an update or a resume lands on what is already there: one with this game's sidecar - * anywhere, else one under any root with this game's folder name and no finished sidecar (an - * install an earlier build left without one, or that stopped before writing it). - */ fun folderFor(context: Context, store: Store, id: String, title: String, root: File = installRoot(context)): File = existingFolder(context, store, id, title) ?: File(storeDir(root, store), folderName(title, id)) - /** The folder an earlier install of this game left, finished or not; null when there is none. */ fun existingFolder(context: Context, store: Store, id: String, title: String): File? { val all = roots(context).flatMap { r -> storeDir(r, store).listFiles { f -> f.isDirectory }?.toList().orEmpty() } all.firstOrNull { folder -> StoreGameSidecar.read(folder)?.let { it.store == store && it.id == id } == true }?.let { return it } @@ -117,17 +85,10 @@ object StoreInstallRoot { return all.firstOrNull { it.name == name && StoreGameSidecar.read(it)?.isInstalled != true } } - /** An unfinished install: [id] from its `installing` sidecar, null when it has none (matched by folder name then). */ class Unfinished(val store: Store, val id: String?, val folder: File) - /** - * A folder under a store root is a game only once its sidecar says installed. Without one it is - * an install that never finished (stopped, killed, or from a build that wrote the sidecar last): - * not a Custom game, and not listed in Steam. - */ fun isFinished(folder: File): Boolean = StoreGameSidecar.read(folder)?.isInstalled == true - /** Every unfinished install under every root, for the Stores page's "Resume install". */ fun unfinished(context: Context): List<Unfinished> = roots(context).flatMap { root -> Store.entries.flatMap { store -> storeDir(root, store).listFiles { f -> f.isDirectory }.orEmpty().mapNotNull { folder -> @@ -141,15 +102,9 @@ object StoreInstallRoot { } } - /** A scratch folder in the app's cache for an install's in-flight pieces, when the install itself is on a card. */ fun scratchDir(context: Context, store: Store, id: String): File = File(File(File(context.cacheDir, "stores"), store.id), id.replace(Regex("[^A-Za-z0-9._-]"), "_")) - /** - * A folder name from a store title: letters, digits and a few safe marks, single spaces, at most - * 60 characters; the store id when nothing printable is left. The name is the shortcut's folder - * for ever, so it is chosen once at install and read back from the sidecar afterwards. - */ fun folderName(title: String, id: String): String { val cleaned = title.replace(Regex("[^A-Za-z0-9 ._'()\\-]"), " ").replace(Regex("\\s+"), " ").trim().trimEnd('.') val cut = if (cleaned.length > 60) cleaned.take(60).trimEnd().trimEnd('.') else cleaned diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt index 97a6c9032..4616c51e6 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreInstalls.kt @@ -8,61 +8,31 @@ import com.droiddeck.launcher.frontend.LibraryCache import com.droiddeck.launcher.frontend.Library import java.io.File -/** - * What happens around a store install so the rest of the app sees the game: the sidecar is - * written, the launcher .bat beside it when the game needs one, and Steam is told. - * - * Telling Steam is the same path an added folder takes: the game is one more folder the - * [AddedGames] scan finds, the scan's listing (`session/added-games.json`) is what the runtime's - * shortcuts writer reads at the client's next start, and here the listing is rewritten at once so - * a Steam client that restarts inside the running session already has it. When the client is up, - * [SteamLiveShortcuts] adds the shortcut over the client's DevTools port as well, so the game shows - * without a restart; that part is best-effort. - */ object StoreInstalls { private const val TAG = "StoreInstalls" - /** - * Marks [folder] as an install under way before anything is fetched: a sidecar in state - * `installing` with the store, id and title. Whatever stops the install from here on - a - * failure, a cancel, the process killed - leaves a folder the scan skips and the Stores page - * offers to resume, never a half-written Custom game. A finished install being repaired or - * updated keeps its finished sidecar, so the game stays in Steam meanwhile. - */ fun begin(folder: File, store: Store, id: String, title: String, cover: String?, hero: String?) { if (StoreGameSidecar.read(folder)?.isInstalled == true) return StoreGameSidecar(store, id, title, exe = "", cover = cover, hero = hero, state = StoreGameSidecar.STATE_INSTALLING).write(folder) } - /** - * Finishes an install: writes the launcher and the finished sidecar, then the art, then - * registers the game. Runs on the caller's worker thread. The launcher and sidecar writes - * throw, so a failure there fails the download with its message; the art and the Steam side - * are best-effort and only logged. - */ fun complete(context: Context, folder: File, sidecar: StoreGameSidecar, onStep: ((Int, Int) -> Unit)? = null) { val app = context.applicationContext onStep?.invoke(0, STEPS) - // The user's launch choices survive an update or a repair. val before = StoreGameSidecar.read(folder) val finished = StoreLaunch.writeLauncher(folder, sidecar.copy(state = StoreGameSidecar.STATE_INSTALLED, epic = before?.epic ?: sidecar.epic, cloud = before?.cloud ?: sidecar.cloud, achievements = before?.achievements ?: sidecar.achievements)) finished.write(folder) if (StoreGameSidecar.read(folder)?.isInstalled != true) throw java.io.IOException("the install record could not be written in ${folder.name}") onStep?.invoke(1, STEPS) - // The art before the listing, so the listing already carries it for the client's grid. try { StoreArt.fetchInto(folder, finished) } catch (e: Exception) { Log.w(TAG, "art for ${folder.name}: ${e.message}") } onStep?.invoke(2, STEPS) register(app) - // Its Windows components from the store's list (GOG's dependencies, Epic's prerequisite, - // Amazon's install steps), on a thread of their own. AddedGames.single(app, folder.path)?.let { com.droiddeck.launcher.frontend.AutoComponents.refreshLater(app, Library.addedGame(app, it)) } onStep?.invoke(STEPS, STEPS) } - /** The finishing steps [complete] reports: the record and launcher, the art, Steam. */ private const val STEPS = 3 - /** Rewrites the session's listing from a fresh scan and, with a client running, adds live. */ fun register(context: Context) { val app = context.applicationContext try { @@ -75,17 +45,9 @@ object StoreInstalls { } } - /** - * Removes a store game: its folder with the sidecar, then the shortcut (the listing without - * it makes the writer drop the entry; the live client is asked too). Blocking. [removed] runs - * once the files are gone, before the shortcut list is rebuilt (which reads every added game). - */ fun uninstall(context: Context, game: InstalledStoreGame, progress: (Float) -> Unit = {}, removed: () -> Unit = {}) { val app = context.applicationContext - // The shortcut's id as stored for the folder: scanning every added game for it first held - // the bar at 0% for seconds before a file went. val appId = SessionPrefs.addedGameStoredAppId(app, game.folder.path) - // File by file, deepest first, saying how far it has got: a big game takes a while to go. val files = game.folder.walkBottomUp().toList() val step = (files.size / 100).coerceAtLeast(1) files.forEachIndexed { i, f -> @@ -94,7 +56,6 @@ object StoreInstalls { } deleteTree(game.folder) progress(1f) - // The scratch cache an install on a card may have left in the app's cache. deleteTree(StoreInstallRoot.scratchDir(app, game.sidecar.store, game.sidecar.id)) removed() register(app) @@ -102,12 +63,6 @@ object StoreInstalls { StoresState.logLine("uninstalled \"${game.sidecar.title}\"") } - /** - * What Cancel does: everything the install wrote goes - the game folder with its sidecar and - * the caches in [caches] - unless the folder holds a finished install being repaired or updated; - * then only the caches go and the working game stays. Off the caller's thread; the library - * follows when it is done, so the game reads Install again. - */ fun discard(context: Context, folder: File, caches: List<File>) { val app = context.applicationContext Thread({ diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunch.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunch.kt index 4977baefc..a50dfcd90 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunch.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunch.kt @@ -5,27 +5,11 @@ import android.util.Log import com.droiddeck.launcher.frontend.Library import java.io.File -/** - * How a store game is started inside the session. The shortcut Steam gets can only name an exe, - * so when a game needs arguments or environment the install writes a small `.droiddeck-launch.bat` - * beside it and the shortcut runs that: Proton's steam.exe shim hands a .bat to Wine's cmd, the - * script sets the variables, steps into the exe's folder and starts it, and waits for it as a - * batch file does, so Steam sees the game running. - * - * Epic's online sign-in is a fresh exchange code per launch (it expires in minutes), which no fixed - * command line can carry: [prepare] mints one right before a launch and leaves it in - * `.droiddeck-epic-code`; the script reads the file, deletes it and passes the AUTH arguments. With - * no file the game starts in its offline identity mode, which most EOS titles accept. - */ object StoreLaunch { private const val TAG = "StoreLaunch" const val LAUNCHER = ".droiddeck-launch.bat" const val EPIC_CODE = ".droiddeck-epic-code" - /** - * Writes the launcher for [sidecar] when it needs one (arguments, environment, or an Epic - * sign-in), removes a stale one otherwise, and returns the sidecar with [StoreGameSidecar.launcher] set. - */ fun writeLauncher(folder: File, sidecar: StoreGameSidecar): StoreGameSidecar { val needs = sidecar.args.isNotEmpty() || sidecar.env.isNotEmpty() || sidecar.store == Store.EPIC val file = File(folder, LAUNCHER) @@ -34,7 +18,6 @@ object StoreLaunch { return sidecar.copy(launcher = LAUNCHER) } - /** The batch file's text; pure, so it can be checked without a folder. CRLF line ends, as cmd expects. */ fun launcherText(sidecar: StoreGameSidecar): String { val exe = sidecar.exe.replace('/', '\\') val dir = exe.substringBeforeLast('\\', "") @@ -47,9 +30,6 @@ object StoreLaunch { val args = sidecar.args.joinToString(" ") { quoteArg(it) } val run = "\"%~dp0$exe\"" + (if (args.isNotEmpty()) " $args" else "") if (sidecar.store == Store.EPIC) { - // The one-shot code reaches the game on its command line only. cmd expands %DD_X% when it - // parses the last line, so the variable is cleared on that same line before the game - // starts: nothing of it is left in the environment the game inherits (and dumps). lines += "set \"$CODE_VAR=\"" lines += "if exist \"%~dp0$EPIC_CODE\" set /p $CODE_VAR=<\"%~dp0$EPIC_CODE\"" lines += "if exist \"%~dp0$EPIC_CODE\" del \"%~dp0$EPIC_CODE\"" @@ -66,24 +46,13 @@ object StoreLaunch { private val ENV_NAME = Regex("[A-Za-z_][A-Za-z0-9_]*") - /** An argument on cmd's line: quoted when it has a space and is not already quoted; control characters dropped. */ fun quoteArg(arg: String): String { val clean = arg.filter { it >= ' ' && it != '\u007f' } return if (' ' in clean && !clean.contains('"')) "\"$clean\"" else clean } - /** - * Everything a launch needs done first, then [onReady] on the main thread: for an Epic game a - * fresh exchange code written beside its launcher (a few seconds at most; the launch goes - * ahead without one when the store cannot be reached). Other stores need nothing. - */ - /** The launcher's variable for the one-shot code; a plain name, cleared before the game starts. */ private const val CODE_VAR = "DD_X" - /** - * Rewrites a game's launcher when its text is not what this build writes - an install from an - * earlier build keeps its old .bat otherwise. Called before every Epic launch. - */ fun refreshLauncher(folder: File, sidecar: StoreGameSidecar) { if (sidecar.launcher != LAUNCHER) return val file = File(folder, LAUNCHER) @@ -103,16 +72,8 @@ object StoreLaunch { }, "stores-launch-prep").start() } - /** What [epicCode] did, for the guest's request and the log: whether a code was written, and why not. */ class CodeResult(val written: Boolean, val reason: String) - /** - * Mints an Epic exchange code for the installed game [id] and writes it as [EPIC_CODE] into the - * very folder its sidecar and launcher are in, where the launcher .bat reads and deletes it. - * Blocking (network). Logs `epic launch id=<id> code=yes|no reason=...` - never the code. Every - * launch path comes here: the Games tab and Stores before they launch, and the compat tool for - * any launch, the Steam client's own Play button included ([StoreLaunchRequests]). - */ fun epicCode(context: Context, id: String): CodeResult { val app = context.applicationContext val result = try { @@ -152,9 +113,7 @@ object StoreLaunch { } } -/** What the Epic backend adds for launches: a short-lived exchange code for the signed-in account. */ interface EpicLaunchSupport { fun exchangeCode(context: Context): String? - /** True when the stored sign-in can no longer be refreshed: the user has to sign in again. */ fun signInExpired(context: Context): Boolean } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunchRequests.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunchRequests.kt index 03864d99c..968aec7cc 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunchRequests.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunchRequests.kt @@ -7,24 +7,15 @@ import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File import org.json.JSONObject -/** - * The app's half of droiddeck-store-launch: the compat tool asks, before Proton starts a store - * game, for what the store needs at launch, through files under the session root's `stores/` - * (`req/<id>.json` in, `resp/<id>.json` out, both written tmp + rename). Today that is an Epic - * game's exchange code, which [StoreLaunch.epicCode] writes into the game's own folder; the answer - * says only whether it did and why not. Watched while a session runs. - */ object StoreLaunchRequests { private const val TAG = "StoreLaunchRequests" private val lock = Any() private var observer: FileObserver? = null - /** The store game this session last started; covers Steam's own Play button. */ @Volatile var lastLaunched: Pair<Store, String>? = null fun dir(context: Context) = File(LinuxRuntime.sessionRoot(context), "stores") - /** Before each session: nothing of the last one's requests; then watch for new ones. */ fun start(context: Context) { val app = context.applicationContext synchronized(lock) { @@ -56,7 +47,7 @@ object StoreLaunchRequests { val request = try { JSONObject(file.readText()).also { file.delete() } } catch (e: Exception) { - return // withdrawn already, or not complete yet (the rename brings it again) + return } val reply = when (request.optString("op")) { "gog-achievements" -> runCatching { com.droiddeck.launcher.stores.gog.GogComet.prepare(app, request.optString("id")) } @@ -70,15 +61,12 @@ object StoreLaunchRequests { JSONObject().put("ok", true).put("code", code.written).put("reason", code.reason) } } - // Cloud saves: down before Proton starts (the wrapper waits a bounded time), up after the - // game exits (answered at once; the upload runs on without holding Steam). "cloud-down" -> { val store = Store.byId(request.optString("store")) val id = request.optString("id") if (store == null || id.isEmpty()) JSONObject().put("ok", false).put("reason", "no-game") else { lastLaunched = store to id - // Marked before the game runs: whatever ends it, an upload follows (CloudSaves.uploadDirty). CloudSaves.markDirty(app, store, id) CloudSaves.downloadOrDefer(app, store, id).let { JSONObject().put("ok", it.ok).put("result", it.result).put("files", it.files).put("reason", it.reason) } } @@ -89,8 +77,6 @@ object StoreLaunchRequests { if (store != null && id.isNotEmpty()) Thread({ CloudSaves.uploadDirty(app, store, id, "exit") }, "cloud-up").start() JSONObject().put("ok", true) } - // A browser a game opens inside the session (an EOS device sign-in, a store page): - // http(s) only, handed to Android's browser. "open-url" -> { val url = request.optString("url") if (!(url.startsWith("https://") || url.startsWith("http://"))) JSONObject().put("ok", false).put("reason", "not-http") diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreLogFiles.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreLogFiles.kt index eb4a14fc2..0af659012 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreLogFiles.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreLogFiles.kt @@ -8,13 +8,6 @@ import java.util.Date import java.util.Locale import java.util.concurrent.Executors -/** - * The stores' log on disk, app-private: `filesDir/logs/stores/stores-<yyyy-MM-dd>.log` (beside the - * session folders, where every log lives), one file a - * day, the last [KEEP_DAYS] kept, each capped near [MAX_BYTES] (the full one moves to `.1`). Lines - * arrive already redacted ([StoreLog.redactLine]). Written on one background thread, in order. The - * session's Share logs zip carries these files, so they can be attached to a report when asked. - */ object StoreLogFiles { private const val TAG = "StoreLogFiles" private const val KEEP_DAYS = 7 @@ -31,10 +24,6 @@ object StoreLogFiles { Thread({ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); r.run() }, "store-log-flush").apply { isDaemon = true } } - /** - * Queues one stamped line for today's file. Any thread; nothing touches the disk here. Lines are - * written in batches, at most every half second, on [writer] in order. - */ fun append(context: Context, line: String) { val app = context.applicationContext pending.add(line) @@ -61,7 +50,6 @@ object StoreLogFiles { } } - /** Drops the files older than [KEEP_DAYS] days. Called as the app starts. */ fun prune(context: Context) { val app = context.applicationContext writer.execute { @@ -73,7 +61,6 @@ object StoreLogFiles { } } - /** The files there are, oldest first, for a log share. */ fun files(context: Context): List<File> = dir(context).listFiles { f -> f.isFile && NAME.matches(f.name) }?.sortedBy { it.name }.orEmpty() } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreLoginActivity.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreLoginActivity.kt index 277d42bb5..ce678d3f6 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreLoginActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreLoginActivity.kt @@ -31,26 +31,11 @@ import com.droiddeck.launcher.R import com.droiddeck.launcher.stores.gog.GogAuth import java.util.concurrent.atomic.AtomicBoolean -/** - * A store's own sign-in page in a WebView, one activity for the three stores: GOG's OAuth page - * (the token comes back in the redirect's fragment), Epic's web login (an authorization code on - * a JSON page), Amazon's device sign-in (a PKCE code on the return URL). Plain views, as the - * proven GOG flow was: the page is rendered with the store's own client identity, and a social - * sign-in hop to Google / Facebook / Apple runs under a real browser identity in a popup, since - * those providers refuse an embedded browser. - * - * Nothing from the page is logged but redacted URLs and key names; the tokens go straight to - * StoreAccounts' private file and the activity finishes. - */ class StoreLoginActivity : ComponentActivity() { companion object { private const val TAG = "StoreLogin" const val EXTRA_STORE = "store" private const val KEY_STATE = "oauth_state" - /** - * The colour the Stores page flooded to before opening this page, read once: the page opens - * on it, with no window animation, and its chrome rises in, as a session opens on Play's blue. - */ @Volatile internal var floodColor: Int? = null private const val CHROME_UA = "Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.103 Mobile Safari/537.36" @@ -61,15 +46,11 @@ class StoreLoginActivity : ComponentActivity() { } } - /** What one store's flow decides: the page to open, the identity to open it with, and the redirect that ends it. */ internal interface Flow { val startUrl: String val userAgent: String - /** True when [url] is the flow's end; the activity then calls [finish]. */ fun isRedirect(url: String): Boolean - /** Runs off the main thread with the redirect; returns null on success, else a message to show. */ fun finish(activity: StoreLoginActivity, view: WebView, url: String): String? - /** Whether a JSON page at the redirect must be read out of the document instead of the URL (Epic). */ val readsPage: Boolean get() = false } @@ -85,7 +66,6 @@ class StoreLoginActivity : ComponentActivity() { private lateinit var titleText: TextView private var oauthState: String? = null private val captured = AtomicBoolean(false) - /** Opened behind a flood: it opens and closes with no window animation. */ private var flooded = false private fun dp(value: Int): Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value.toFloat(), resources.displayMetrics).toInt() @@ -126,7 +106,6 @@ class StoreLoginActivity : ComponentActivity() { override fun finish() { super.finish() - // Back to the Stores page, which is still covered in the flood and drains it from there. @Suppress("DEPRECATION") if (flooded) overridePendingTransition(0, 0) } @@ -193,7 +172,6 @@ class StoreLoginActivity : ComponentActivity() { settings.javaScriptEnabled = true settings.domStorageEnabled = true settings.userAgentString = userAgent - // The stores' social-login buttons are window.open() popups; without these the call is dropped silently. settings.setSupportMultipleWindows(true) settings.javaScriptCanOpenWindowsAutomatically = true if (isPopup) CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) @@ -224,7 +202,6 @@ class StoreLoginActivity : ComponentActivity() { override fun onCloseWindow(window: WebView?) { if (isPopup || window === popupWebView) dismissPopup() } } - /** Deferred: destroying a WebView from inside one of its own callbacks crashes chromium. */ private fun dismissPopup() { val child = popupWebView ?: return popupWebView = null @@ -243,12 +220,10 @@ class StoreLoginActivity : ComponentActivity() { private inner class Client(private val isPopup: Boolean) : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) { Log.d(TAG, "page[popup=$isPopup]: ${StoreLog.redactUrl(url)}") - // Amazon's return URL lands as a page load rather than a navigation we can intercept. if (url != null && view != null && flow?.isRedirect(url) == true && flow?.readsPage != true) complete(view, url) } override fun onPageFinished(view: WebView?, url: String?) { - // Epic's redirect is a JSON page: the code is in the document, read once it is loaded. if (url != null && view != null && flow?.readsPage == true && flow?.isRedirect(url) == true) complete(view, url) } @@ -277,9 +252,6 @@ class StoreLoginActivity : ComponentActivity() { val f = flow ?: return false val url = uri.toString() if (f.isRedirect(url) && !f.readsPage) { complete(view, url); return true } - // A social sign-in hop that stays in the main view: the provider gets a browser identity, - // and the store gets its own identity back afterwards (a plain Chrome UA makes GOG's - // page serve a form that never renders in a WebView). if (!isPopup && store == Store.GOG) { if (GogAuth.isGogHost(uri.host) && view.settings.userAgentString != f.userAgent) { view.settings.userAgentString = f.userAgent; return false } if (GogAuth.isIdentityProviderHost(uri.host) && view.settings.userAgentString != CHROME_UA) { @@ -303,7 +275,6 @@ class StoreLoginActivity : ComponentActivity() { if (!f.readsPage) view.stopLoading() } if (f.readsPage) { - // The document's text, then the exchange off the main thread. view.evaluateJavascript("(function(){ try { return document.body.innerText; } catch(e){ return ''; } })()") { json -> Thread({ settle(f.finish(this, view, json ?: "")) }, "store-login").start() } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreNet.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreNet.kt index 4aeac3517..8517a1aa0 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreNet.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreNet.kt @@ -7,16 +7,9 @@ import java.io.OutputStreamWriter import java.net.HttpURLConnection import java.net.URL -/** - * The small HTTP the storefront catalogs and account calls need: a GET or a JSON POST with the - * right User-Agent, a bearer token when the call is signed in, and a body or null. Store URLs - * carry their authorization in the query string or the path, so a URL is only ever logged - * through [StoreLog.redactUrl]. Plain HttpURLConnection, like the rest of the app. - */ internal object StoreNet { private const val TAG = "StoreNet" - /** A desktop browser: GOG's catalog and Epic's store GraphQL answer it; Epic's edge refuses the bare Java UA. */ const val BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36" fun get(url: String, bearer: String? = null, userAgent: String = BROWSER_UA, timeoutMs: Int = 20_000, headers: Map<String, String> = emptyMap()): String? { @@ -66,10 +59,6 @@ internal object StoreNet { } } - /** - * A 600x900 poster for [title] from SteamGridDB by name, or "" - the lookup the GOG library and - * the Amazon poster backfill use, since those stores publish no tall art of their own. - */ fun sgdbPoster(title: String): String = try { val enc = java.net.URLEncoder.encode(title, "UTF-8") val search = get("https://www.steamgriddb.com/api/v2/search/autocomplete/$enc", bearer = SGDB_KEY) @@ -95,14 +84,7 @@ internal object StoreNet { } } -/** - * Keeps credentials out of the logs. A store's CDN, manifest and token URLs carry their - * authorization in the query string (signed, time-limited tokens, `client_secret`, - * `refresh_token`) or in the authority's userinfo; one such line in logcat or a shared log hands a - * reader a live download. Every store URL that is written anywhere goes through [redactUrl] first. - */ object StoreLog { - /** `scheme://host/path` only: query, fragment and `user:pass@` dropped; "[url]" when it cannot be parsed. */ @JvmStatic fun redactUrl(url: String?): String? { if (url == null) return null @@ -126,11 +108,6 @@ object StoreLog { } } - /** - * What any store line may say, for the stores' log and logcat alike: URLs cut to their host and a - * plain first path segment, token values and Authorization / Cookie headers blanked - the shared - * rules of [com.droiddeck.launcher.core.SecretScrub], which a shared log zip applies too. - */ @JvmStatic fun redactLine(text: String): String = com.droiddeck.launcher.core.SecretScrub.scrub(text, com.droiddeck.launcher.core.SecretScrub.Urls.SHORT, "…") diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreSizes.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreSizes.kt index 122b3778f..d8253de87 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreSizes.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreSizes.kt @@ -15,17 +15,9 @@ import com.droiddeck.launcher.stores.epic.EpicPrefs import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors -/** - * Install sizes for owned Epic and Amazon games, whose library answers carry none: Epic's from the - * game's build manifest (the files this device installs), Amazon's from its download manifest. - * Asked for a card as it comes on screen, three at a time in the background, once per game - the - * answer is kept in the store's prefs (`size_<id>`, where the library reads it too). Until it is - * known the card shows nothing. - */ object StoreSizes { private const val TAG = "StoreSizes" - /** Sizes found this run, by card key; the cards read them. */ val known = mutableStateMapOf<String, Long>() private val asked = ConcurrentHashMap.newKeySet<String>() @@ -36,7 +28,6 @@ object StoreSizes { private val caches = ConcurrentHashMap<Store, SizeCache>() private fun cache(context: Context, store: Store) = caches.getOrPut(store) { SizeCache.forStore(context.applicationContext.filesDir, store) } - /** A card for [item] is on screen (or about to be): its size from the cache for this version, else looked up. */ fun request(context: Context, item: CatalogItem) { if (!item.owned || item.sizeBytes > 0 || item.store == Store.GOG) return if (known.containsKey(item.key) || !asked.add(item.key)) return @@ -58,7 +49,6 @@ object StoreSizes { } } - /** An installed game's size on disk, by card key; worked out once per install. */ val onDisk = mutableStateMapOf<String, Long>() fun requestDisk(context: Context, key: String, game: InstalledStoreGame) { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoresNative.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoresNative.kt index 6d626353f..a01558661 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoresNative.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoresNative.kt @@ -2,12 +2,6 @@ package com.droiddeck.launcher.stores import android.util.Log -/** - * The native download engine for the three stores: `libdroiddeckstores.so`, loaded the first time - * a store needs it, never at process start. The app has to run without it (a build without the - * Rust side, or an ABI the library was not built for), so [available] is a plain false then and - * Setup says "Stores engine not built"; the store managers fall back to their Java fetch loops. - */ object StoresNative { private const val TAG = "StoresNative" private const val LIBRARY = "droiddeckstores" @@ -16,13 +10,10 @@ object StoresNative { @Volatile private var versionText: String? = null @Volatile private var failure: String? = null - /** True once the library is loaded and its version symbol binds; false when it cannot be. Cached. */ val available: Boolean get() = ensureLoaded() - /** The engine's own version string, or null when the library is not there. */ val version: String? get() { ensureLoaded(); return versionText } - /** Why the library did not load, for the Setup row; null when it did. */ val problem: String? get() { ensureLoaded(); return failure } @Synchronized @@ -34,8 +25,6 @@ object StoresNative { Log.i(TAG, "engine ${versionText ?: "?"} loaded") true } catch (t: Throwable) { - // UnsatisfiedLinkError when the .so is missing or a symbol is not there; anything else is - // a surprise worth the same treatment: the Java paths carry on. failure = t.javaClass.simpleName + (t.message?.let { ": $it" } ?: "") Log.w(TAG, "engine unavailable: $failure") false @@ -44,11 +33,9 @@ object StoresNative { return ok } - /** Resets the cached answer, so a probe after an update tries the library again. */ @Synchronized fun forget() { loaded = null } - /** The crate's version string; only after [ensureLoaded] said yes. */ @JvmStatic external fun nativeVersion(): String } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt index 4f1cb172c..5551535d3 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoresState.kt @@ -14,65 +14,39 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -/** - * The Stores section's state for the life of the process (like the Flathub store's StoreState): - * who is signed in where, each store's library and shelves, what is installed on disk and what is - * being downloaded, so a download carries on - and its progress shows - while the user moves - * around the launcher or plays something meanwhile. - * - * Everything here is read on the main thread by Compose; the workers post their results back. - * The stores themselves plug in as [StoreBackend]s; a store with no backend in this build shows - * its sign-in card and nothing else. - */ object StoresState { private const val TAG = "Stores" private val main = Handler(Looper.getMainLooper()) - /** The signed-in account name per store; absent = signed out. */ val accounts = mutableStateMapOf<Store, String>() - /** Each store's owned games, once synced; absent until then. */ val library = mutableStateMapOf<Store, List<CatalogItem>>() - /** Each store's storefront shelves, once fetched. */ val shelves = mutableStateMapOf<Store, StoreShelves>() - /** What a store is doing for the user right now ("Syncing 12 games…"); absent when idle. */ val status = mutableStateMapOf<Store, String>() - /** Why the last sync or shelf fetch failed, per store; cleared by the next success. */ val problems = mutableStateMapOf<Store, String>() - /** Games the stores installed, as their sidecars say; rebuilt by [refresh]. */ var installed by mutableStateOf<List<InstalledStoreGame>>(emptyList()) private set - /** - * The download queue as the Downloads page lists it, newest first. Replaced only when a row is - * added, removed, reordered or changes state - not on progress - so whatever iterates it does - * not recompose on every byte. A row's progress is read through [download]. - */ var downloads by mutableStateOf<List<DownloadEntry>>(emptyList()) private set - /** Downloads queued, running or paused across the stores (the rail badge); changes only when the count does. */ var activeDownloads by mutableStateOf(0) private set private val rows = HashMap<String, androidx.compose.runtime.MutableState<DownloadEntry?>>() - /** One row's live entry: a composable reading it recomposes for that row's progress only. Main thread. */ fun download(key: String): DownloadEntry? = rows.getOrPut(key) { mutableStateOf(null) }.value - /** Main thread: each row's holder set (equal entries change nothing), the list and the count only when they change. */ - /** The newest list as published, progress included; for the notification, which is not composed. */ @Volatile var latestDownloads: List<DownloadEntry> = emptyList() private set private var notifiedAt = 0L internal fun publishDownloads(list: List<DownloadEntry>) { latestDownloads = list - // The notification follows at most once a second. val now = android.os.SystemClock.uptimeMillis() if (now - notifiedAt >= 1000) { notifiedAt = now; com.droiddeck.launcher.stores.download.StoreDownloadService.tick() } val keys = list.mapTo(HashSet()) { it.key } @@ -84,22 +58,18 @@ object StoresState { } - /** The native engine's version once probed, "" when the probe said it is missing, null before. */ var engine by mutableStateOf<String?>(null) private set private val backends = HashMap<Store, StoreBackend>() - /** A store's implementation, registered once as the app starts; null when this build lacks it. */ fun backend(store: Store): StoreBackend? = backends[store] fun register(backend: StoreBackend) { backends[backend.store] = backend } - /** Reads what is on disk for every store: sign-ins, installed games, the engine probe. Off the main thread. */ fun refresh(context: Context) { val app = context.applicationContext - // The first refresh after the app starts brings back what was in the download queue. com.droiddeck.launcher.stores.download.SavedQueue.restore(app) Thread({ val found = Store.entries.mapNotNull { store -> StoreAccounts.signedInAs(app, store)?.let { store to it } } @@ -120,17 +90,14 @@ object StoresState { fun isSignedIn(store: Store): Boolean = accounts.containsKey(store) - /** A store action's error line when it found no usable sign-in: gone, or on disk but unreadable for now. */ fun notSignedInLine(store: Store): String = if (StoreAccounts.isUnavailable(store)) "${store.shortLabel} sign-in could not be read: try again." else "${store.shortLabel} session expired: sign in again." - /** Stores whose sign-in has run out (a launch could not get a code): their chip dims until the user signs in again. */ val expired = mutableStateMapOf<Store, Boolean>() fun markSignInExpired(store: Store) = post { expired[store] = true } - /** Installs that stopped before they finished; their Install button reads "Resume install". */ var unfinished by mutableStateOf<List<StoreInstallRoot.Unfinished>>(emptyList()) internal set @@ -138,7 +105,6 @@ object StoresState { it.store == item.store && (it.id == item.id || (it.id == null && it.folder.name == StoreInstallRoot.folderName(item.title, item.id))) } - /** Deletes an unfinished install's folder and caches, as Cancel would; drops its row if there is one. */ fun clearUnfinished(context: Context, item: CatalogItem) { val app = context.applicationContext val key = "${item.store.id}:${item.id}" @@ -151,7 +117,6 @@ object StoresState { fun installedGame(store: Store, id: String): InstalledStoreGame? = installed.firstOrNull { it.sidecar.store == store && it.sidecar.id == id } - /** The user's library and the shelves for [store], from the cache first and the network when stale. */ fun open(context: Context, store: Store, force: Boolean = false) { val backend = backends[store] ?: return val app = context.applicationContext @@ -160,7 +125,6 @@ object StoresState { if (force || !shelves.containsKey(store)) backend.loadShelves(app, force) } - /** A store not signed into: its public storefront only, for its sign-in page to show. */ fun preview(context: Context, store: Store) { if (!shelves.containsKey(store)) backends[store]?.loadShelves(context.applicationContext, false) } @@ -183,13 +147,8 @@ object StoresState { }, "stores-signout").start() } - /** A notification asked for the Downloads chip; the Stores page takes it and clears it. */ var openDownloads by mutableStateOf(false) - /** - * Android 13+: notifications need permission. Asked once, the first time a download starts from - * a screen; refused, downloads still run, without their notification. - */ private fun askForNotifications(context: Context) { val activity = context as? android.app.Activity ?: return if (android.os.Build.VERSION.SDK_INT < 33) return @@ -200,23 +159,15 @@ object StoresState { runCatching { activity.requestPermissions(arrayOf("android.permission.POST_NOTIFICATIONS"), 7102) } } - /** An install waiting for the user to say where: the page shows the choice while this is set. */ var pendingInstall by mutableStateOf<CatalogItem?>(null) - /** - * The way a page starts an install: with a card in the device the user is asked where (the - * dialog's default is last time's pick, never a silent choice); with none it goes to internal - * storage at once. - */ fun requestInstall(context: Context, item: CatalogItem) { askForNotifications(context) - // A resume goes where the files already are; there is nothing to choose. if (isUnfinished(item)) { install(context, item); return } val targets = StoreInstallRoot.targets(context) if (targets.size > 1) pendingInstall = item else install(context, item, targets.first().root) } - /** Starts an install into [root] (the internal root when null). */ fun install(context: Context, item: CatalogItem, root: java.io.File? = null) { val app = context.applicationContext val target = root ?: StoreInstallRoot.installRoot(app) @@ -224,14 +175,8 @@ object StoresState { backends[item.store]?.install(app, item, target) ?: logLine("${item.store.label}: this build cannot install yet") } - /** Games being removed, by key ("store:id"), and how far along (0..1): their page shows it. */ val uninstalling = mutableStateMapOf<String, Float>() - /** - * Removes the game's folder and its shortcut, then [onDone] on the main thread. The game reads - * as removed the moment it is gone (its page shows Install, its card its size), not once the - * whole library has been read again. - */ fun uninstall(context: Context, game: InstalledStoreGame, onDone: () -> Unit = {}) { val app = context.applicationContext val key = "${game.sidecar.store.id}:${game.sidecar.id}" @@ -248,18 +193,14 @@ object StoresState { }, "stores-uninstall").start() } - /** Told on the main thread when an install or removal changed what is on disk; the launcher rebuilds its Games list. */ @Volatile var libraryListener: (() -> Unit)? = null - /** A game landed or went: the installed set is re-read and the launcher told. Any thread. */ fun notifyLibraryChanged(context: Context) { refresh(context) main.post { libraryListener?.invoke() } } - /** Appends to the engine log with a clock, keeping the last 80 lines. Any thread. */ fun logLine(raw: String) { - // Every store line passes here, the engines' included: no token or full URL is kept. val text = StoreLog.redactLine(raw) Log.i(TAG, text) val app = appContext ?: return @@ -268,14 +209,11 @@ object StoresState { private var appContext: Context? = null - /** Called once as the app starts: where the log file goes, and the old days pruned. */ fun init(context: Context) { appContext = context.applicationContext StoreLogFiles.prune(context) - // The EOS overlay an earlier build downloaded (656 MB) is gone with the feature. val overlay = java.io.File(com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context), "root/.local/share/droiddeck/epic-overlay") if (overlay.exists()) Thread({ overlay.deleteRecursively(); Log.i(TAG, "removed the old EOS overlay download") }, "epic-overlay-cleanup").start() - // Cloud saves a killed app never uploaded; not while a session (and maybe that game) runs. val app = context.applicationContext Thread({ CloudSaves.uploadAllDirty(app, "recovery") }, "cloud-recovery").start() } @@ -285,15 +223,12 @@ object StoresState { private val CLOCK = SimpleDateFormat("HH:mm:ss.SSS", Locale.US) } -/** What one store contributes: sign-in, its library, its shelves, installs and removals. */ interface StoreBackend { val store: Store fun signIn(context: Context) fun signOut(context: Context) fun syncLibrary(context: Context, force: Boolean) fun loadShelves(context: Context, force: Boolean) - /** Queues an install of [item] under [root] (a StoreInstallRoot target). */ fun install(context: Context, item: CatalogItem, root: java.io.File) - /** The store's own records of the install, before the folder goes (the queue row, cached ids). */ fun uninstall(context: Context, game: InstalledStoreGame) } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonApiClient.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonApiClient.java index 7683217d9..a17b6a7cc 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonApiClient.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonApiClient.java @@ -16,16 +16,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -/** - * Amazon's distribution API, as the launcher calls it: every gaming call carries `X-Amz-Target`, - * `x-amzn-token`, the launcher's User-Agent and `Content-Encoding: amz-1.0`. - */ public final class AmazonApiClient { private static final String TAG = "AmazonApi"; private static final String ENTITLEMENTS_URL = "https://gaming.amazon.com/api/distribution/entitlements"; private static final String DISTRIBUTION_URL = "https://gaming.amazon.com/api/distribution/v2/public"; - // As nile sends them: the session's User-Agent, and the launcher's own string under "UserAgent". private static final String GAMING_USER_AGENT = "AGSLauncher/1.0.0"; private static final String LAUNCHER_USER_AGENT = "com.amazon.agslauncher.win/3.0.9202.1"; private static final String DOWNLOAD_USER_AGENT = "nile/0.1 Amazon"; @@ -33,7 +28,6 @@ public final class AmazonApiClient { private AmazonApiClient() {} - /** The owned games, every page, one per productId; null when Amazon refused or could not be reached. */ public static List<AmazonGame> getEntitlements(String accessToken, String deviceSerial) { Map<String, AmazonGame> seen = new HashMap<>(); String nextToken = null; @@ -65,7 +59,6 @@ public final class AmazonApiClient { AmazonGame game = parseEntitlement(entitlements.getJSONObject(i)); if (game != null && !game.productId.isEmpty()) seen.put(game.productId, game); } - // A JSON null reads as "null" through optString; the last page carries none. nextToken = json.isNull("nextToken") ? null : json.optString("nextToken", ""); if (nextToken != null && nextToken.isEmpty()) nextToken = null; } catch (Exception e) { @@ -94,8 +87,6 @@ public final class AmazonApiClient { if (parentId.isEmpty()) parentId = product.optString("parentId", ""); JSONObject detail = product.optJSONObject("productDetail"); if (detail != null) { - // The opaque images only: the square icon, and for the wide card the second - // background (a plain key art) before the first (often faded) or the crown image. game.artUrl = detail.optString("iconUrl", ""); JSONObject details = detail.optJSONObject("details"); if (details != null) { @@ -123,7 +114,6 @@ public final class AmazonApiClient { public String versionId; } - /** The download root and version for a game; takes the entitlement id, not the product id. */ public static GameDownloadSpec getGameDownload(String accessToken, String entitlementId) { try { JSONObject body = new JSONObject().put("entitlementId", entitlementId).put("Operation", "GetGameDownload"); @@ -140,7 +130,6 @@ public final class AmazonApiClient { } } - /** The live version id of a product, for update checks. */ public static String getLiveVersionId(String accessToken, String productId) { try { JSONObject body = new JSONObject().put("adgProductIds", new JSONArray().put(productId)).put("Operation", "GetLiveVersionIds"); @@ -156,7 +145,6 @@ public final class AmazonApiClient { } } - /** `baseUrl/segment`, keeping a query string on the base where it belongs. */ public static String appendPath(String baseUrl, String segment) { int q = baseUrl.indexOf('?'); if (q >= 0) return baseUrl.substring(0, q) + "/" + segment + baseUrl.substring(q); @@ -191,7 +179,6 @@ public final class AmazonApiClient { } } - /** Raw bytes (the manifest). */ public static byte[] getBytes(String urlStr, String accessToken) { try { HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonAuthClient.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonAuthClient.java index 878f659ae..39dce7ca3 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonAuthClient.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonAuthClient.java @@ -16,12 +16,6 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; -/** - * Amazon's device-auth endpoints, as the Amazon Games launcher registers itself: - * POST api.amazon.com/auth/register (the PKCE code for bearer tokens, with a device registration) - * POST api.amazon.com/auth/token (access token renewal; the refresh token stays) - * POST api.amazon.com/auth/deregister (sign-out, best-effort) - */ public final class AmazonAuthClient { private static final String TAG = "AmazonAuth"; @@ -39,13 +33,11 @@ public final class AmazonAuthClient { public String refreshToken; public long expiresIn; public String name; - /** The serial Amazon registered the device under; the entitlements call hashes this one. */ public String deviceSerial; } private AmazonAuthClient() {} - /** Exchanges the PKCE authorization code for bearer tokens; null on failure. */ public static RegisterResult registerDevice(String authCode, String codeVerifier, String deviceSerial, String clientId) { try { JSONObject authData = new JSONObject(); @@ -93,7 +85,6 @@ public final class AmazonAuthClient { } } - /** A new access token; the refresh token is not returned and stays as it was. */ public static RegisterResult refreshAccessToken(String refreshToken) { try { JSONObject body = new JSONObject(); diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonBackend.kt b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonBackend.kt index 960666133..10b74d0df 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonBackend.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonBackend.kt @@ -23,11 +23,6 @@ import com.droiddeck.launcher.stores.download.DownloadStage import java.io.File import java.util.concurrent.atomic.AtomicBoolean -/** - * Amazon Games in the Stores section. Amazon has no browsable catalog for third-party clients - - * Prime Gaming's claims live on gaming.amazon.com, signed in - so the Store tab's shelves are cut - * from the account's own library: owned-not-installed as Trending, installed as Your library. - */ object AmazonBackend : StoreBackend { private const val TAG = "AmazonBackend" override val store: Store = Store.AMAZON @@ -46,7 +41,6 @@ object AmazonBackend : StoreBackend { override fun syncLibrary(context: Context, force: Boolean) { val app = context.applicationContext Thread({ - // The cache off the main thread; with it on screen a background sync keeps quiet. val cached = AmazonLibrary.cached(app) val quiet = cached.isNotEmpty() && !force StoresState.post { @@ -67,7 +61,6 @@ object AmazonBackend : StoreBackend { }, "amazon-sync").start() } - /** The library and, from it, the shelves. */ private fun publish(app: Context, games: List<AmazonGame>) { val items = games.map { AmazonLibrary.toCatalogItem(app, it) } StoresState.library[store] = com.droiddeck.launcher.stores.mergeItems(StoresState.library[store], items) @@ -75,8 +68,6 @@ object AmazonBackend : StoreBackend { StoresState.shelves[store] = StoreShelves(trending = items.filter { it.id !in installed }, free = emptyList(), deals = emptyList(), whatsNew = emptyList()) } - // Nothing public to fetch; the shelves follow the library sync, which StoresState.open starts - // alongside this. A second sync here only came back Busy and cleared the first one's status. override fun loadShelves(context: Context, force: Boolean) {} override fun install(context: Context, item: CatalogItem, root: File) { @@ -135,7 +126,6 @@ object AmazonBackend : StoreBackend { } } - /** Amazon's device sign-in: a PKCE challenge on the sign-in page, the code on the return URL, a device registration for tokens. */ private class LoginFlow : StoreLoginActivity.Flow { private val serial = AmazonPkce.generateDeviceSerial() private val clientId = AmazonPkce.generateClientId(serial) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonCredentialStore.kt b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonCredentialStore.kt index 95944375d..1f05c13f1 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonCredentialStore.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonCredentialStore.kt @@ -6,10 +6,6 @@ import com.droiddeck.launcher.stores.Store import com.droiddeck.launcher.stores.StoreAccounts import org.json.JSONObject -/** - * The Amazon sign-in in StoreAccounts' private file: the bearer tokens, the device serial and - * client id the registration was made with (the entitlements call hashes the serial), the expiry. - */ object AmazonCredentialStore { private const val TAG = "AmazonAuth" @@ -34,7 +30,6 @@ object AmazonCredentialStore { @JvmStatic fun isLoggedIn(context: Context): Boolean = load(context) != null - /** A fresh access token whatever the stored expiry says: for a call Amazon refused with the stored one. */ @JvmStatic fun refresh(context: Context): String? { val creds = load(context) ?: return null @@ -44,7 +39,6 @@ object AmazonCredentialStore { return result.accessToken } - /** A valid access token, refreshed within five minutes of expiry; null when signed out or the refresh failed. */ @JvmStatic fun getValidAccessToken(context: Context): String? { val creds = load(context) ?: return null diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonDownloadManager.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonDownloadManager.java index 82a8b23ea..2eaaa31a2 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonDownloadManager.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonDownloadManager.java @@ -26,12 +26,6 @@ import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; -/** - * The Amazon Games install pipeline, ported from Bannerlator: GetGameDownload for the download - * root and version, `manifest.proto`, then every file fetched (the native engine when it is - * there, eight Java threads otherwise), SHA-256 checked, written as `.tmp` and renamed. A - * complete file on disk is skipped by size, so a rerun resumes. Blocking; the queue runs it. - */ public final class AmazonDownloadManager { private static final String TAG = "AmazonDownload"; @@ -46,7 +40,6 @@ public final class AmazonDownloadManager { void onProgress(String message, int pct); default void onBytes(long done, long total, long speedBps) {} default void onLog(String line) {} - /** What this run fetches and what the game takes on disk. */ default void onSizes(long downloadBytes, long diskBytes) {} } @@ -62,7 +55,6 @@ public final class AmazonDownloadManager { private AmazonDownloadManager() {} - /** Installs the game into {@code installDir}; null when cancelled, an exception when it fails. */ public static Result install(Context ctx, AmazonGame game, String accessToken, File installDir, AtomicBoolean cancel, Callback cb) throws InstallException { if (game.entitlementId == null || game.entitlementId.isEmpty()) throw new InstallException("This game has no entitlement to download with"); try { @@ -134,7 +126,6 @@ public final class AmazonDownloadManager { } } - /** One entry per manifest file with the exact URL the Java loop would open and the SHA-256 when it would check it. */ static String buildPlan(List<AmazonManifest.ManifestFile> files, String baseUrl) { JSONArray arr = new JSONArray(); try { @@ -211,7 +202,6 @@ public final class AmazonDownloadManager { return false; } - /** Streams one file; false = cancelled. */ private static boolean downloadFile(String urlStr, File tmpFile, AtomicLong totalDownloaded, AtomicLong lastEmit, long totalSize, AtomicLong lastSpeedMs, AtomicLong lastSpeedBytes, AtomicLong speedBps, Callback cb, AtomicBoolean cancel) throws IOException { HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); @@ -240,7 +230,6 @@ public final class AmazonDownloadManager { return true; } - /** The install size from the manifest alone, or -1. Blocking. */ public static long fetchInstallSizeBytes(String accessToken, String entitlementId) { try { AmazonApiClient.GameDownloadSpec spec = AmazonApiClient.getGameDownload(accessToken, entitlementId); diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonGame.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonGame.java index 891602b68..3ce769ade 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonGame.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonGame.java @@ -1,17 +1,10 @@ package com.droiddeck.launcher.stores.amazon; -/** - * One Amazon Games library entry. {@code productId} ("amzn1.adg.product.…") names the game, - * {@code entitlementId} (the top-level UUID of GetEntitlements) is what GetGameDownload takes, - * {@code productSku} goes to the game as AMAZON_GAMES_FUEL_PRODUCT_SKU. - */ public class AmazonGame { public String productId = ""; public String entitlementId = ""; public String title = ""; - /** iconUrl: square art. */ public String artUrl = ""; - /** backgroundUrl1: the wide background. */ public String heroUrl = ""; public String developer = ""; public String publisher = ""; @@ -20,7 +13,6 @@ public class AmazonGame { public boolean isDLC = false; public String parentProductId = ""; - /** The id's last segment, for display when there is no title. */ public String shortId() { int dot = productId.lastIndexOf('.'); return (dot >= 0 && dot < productId.length() - 1) ? productId.substring(dot + 1) : productId; diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLaunchHelper.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLaunchHelper.java index bc1bba667..c95bb74f5 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLaunchHelper.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLaunchHelper.java @@ -12,20 +12,12 @@ import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; -/** - * How an installed Amazon game is started: its `fuel.json` names the exe, a working subfolder and - * arguments when the publisher shipped one; otherwise the exe is scored (Unreal shipping binaries - * first, the title's name, never crash handlers or installers). The FuelPump variables the Amazon - * Games Services SDK reads go into the game's environment. - */ public final class AmazonLaunchHelper { private static final String TAG = "AmazonLaunch"; public static final class LaunchSpec { - /** The exe relative to the install folder, forward slashes. */ public String exeRelative = ""; - /** The arguments fuel.json names, in order. */ public List<String> args = new ArrayList<>(); } @@ -65,7 +57,6 @@ public final class AmazonLaunchHelper { return spec; } - /** The FuelPump variables, as KEY=VALUE, for the game's environment. */ public static String[] buildFuelEnv(String entitlementId, String productSku, String displayName) { return new String[]{ "FUEL_DIR=C:\\ProgramData\\Amazon Games Services\\Legacy", diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLibrary.kt b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLibrary.kt index 0cd3be6b0..93249b7d2 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonLibrary.kt @@ -14,12 +14,6 @@ import java.util.concurrent.Callable import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean -/** - * The signed-in account's Amazon Games library: the entitlements, DLC folded under their base - * game, cached so the tab paints at once, re-synced when forced or 15 minutes stale. Amazon - * publishes a square icon and a wide background only, so a 2:3 poster per title is backfilled - * from SteamGridDB (a few per pass). Ported from Bannerlator's AmazonLibraryRepo. - */ object AmazonLibrary { private const val TAG = "AmazonLibrary" private const val CACHE_KEY = "library_cache" @@ -74,7 +68,6 @@ object AmazonLibrary { val creds = AmazonCredentialStore.load(app) ?: return SyncResult.NotLoggedIn val token = AmazonCredentialStore.getValidAccessToken(app) ?: return SyncResult.Failed(StoresState.notSignedInLine(Store.AMAZON)) onStatus(app.getString(R.string.stores_sync_game_list)) - // Refused with the stored token (revoked, or an expiry read wrong): once more on a fresh one. val all = AmazonApiClient.getEntitlements(token, creds.deviceSerial) ?: AmazonCredentialStore.refresh(app)?.let { AmazonApiClient.getEntitlements(it, creds.deviceSerial) } if (all == null) return if (cachedList.isEmpty()) SyncResult.Failed(app.getString(R.string.stores_sync_amazon_no_answer)) else SyncResult.Ok(cachedList) @@ -105,7 +98,6 @@ object AmazonLibrary { val ed = p.edit() for (f in futures) { val (pid, url) = runCatching { f.get() }.getOrNull() ?: continue - // A miss is recorded as "" so it is not asked again every pass. ed.putString("vcover_$pid", url) } ed.apply() diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonManifest.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonManifest.java index 25fb03178..f40db9c77 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonManifest.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonManifest.java @@ -13,18 +13,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -/** - * Amazon's `manifest.proto`: a big-endian header length, a ManifestHeader protobuf naming the - * compression, then an LZMA (or XZ) body holding the Manifest protobuf - packages of files, each - * with a Windows path, a size and a SHA-256. Read with a tiny protobuf walker; the XZ library the - * app already carries does the decompression. - */ public final class AmazonManifest { public static final class ManifestFile { public final String path; public final long size; - /** 0 = SHA-256. */ public final int hashAlgorithm; public final byte[] hashBytes; diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonNative.kt b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonNative.kt index 511dc946f..165ef455c 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonNative.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonNative.kt @@ -1,4 +1,3 @@ -// JNI symbols depend on this package path and class name (app/src/main/rust/stores/JNI.md). package com.droiddeck.launcher.stores.amazon import android.util.Log @@ -6,22 +5,13 @@ import com.droiddeck.launcher.stores.StoresNative import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -/** - * The native Amazon file fetcher in `libdroiddeckstores.so`: one streamed GET per manifest file, - * SHA-256 checked, written as `<dest>.tmp` and renamed. The manager keeps manifest, auth, markers - * and post-install; [runBlocking] mirrors the Java loop's shape and polls the cancel probe. - */ object AmazonNative { const val TAG = "AmazonNative" private const val CANCEL_POLL_MS = 100L - /** Callbacks from the engine; every method is called on a native thread. */ interface Listener { - /** Once up front with the resume-skipped credit, then after each committed file; [bytesDone] includes skipped files. */ fun onProgress(bytesDone: Long, bytesTotal: Long, filesDone: Long, filesTotal: Long) - /** One diagnostic line; the engine itself does not write to logcat. */ fun onLog(line: String) - /** Exactly once, when the run succeeds, fails or is cancelled. */ fun onComplete(success: Boolean, error: String, bytesWritten: Long) } @@ -32,10 +22,6 @@ object AmazonNative { @JvmStatic fun isAvailable(): Boolean = StoresNative.available - /** - * Runs one download to completion. [planJson]: `[{relPath, url, size, sha256hex}]`, every - * manifest file (the engine skips complete ones itself). [maxWorkers] <= 0 = the engine's default. - */ @JvmStatic fun runBlocking(planJson: String, installDir: String, caBundlePath: String, maxWorkers: Int, processWorkers: Int, isCancelled: CancelCheck?, listener: Listener): RunResult { if (!StoresNative.ensureLoaded()) return RunResult(false, false, "engine not built", 0L) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPkce.java b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPkce.java index 9d0a06403..ff7804782 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPkce.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPkce.java @@ -7,16 +7,13 @@ import java.security.MessageDigest; import java.security.SecureRandom; import java.util.UUID; -/** The PKCE values Amazon's device sign-in takes, generated the way the Amazon Games launcher does. */ public final class AmazonPkce { private static final String DEVICE_TYPE = "A2UMVHOX7UP4V7"; private AmazonPkce() {} - /** One per sign-in: a UUID as uppercase hex. */ public static String generateDeviceSerial() { return UUID.randomUUID().toString().replace("-", "").toUpperCase(); } - /** hex("serial#DEVICE_TYPE"). */ public static String generateClientId(String serial) { byte[] bytes = (serial + "#" + DEVICE_TYPE).getBytes(StandardCharsets.UTF_8); StringBuilder sb = new StringBuilder(); @@ -39,7 +36,6 @@ public final class AmazonPkce { } } - /** SHA-256 of the input as uppercase hex: the entitlements call's hardwareHash. */ public static String sha256Upper(String input) { try { byte[] hash = MessageDigest.getInstance("SHA-256").digest(input.getBytes(StandardCharsets.UTF_8)); diff --git a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPrefs.kt b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPrefs.kt index 6469b63af..ef291535c 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/amazon/AmazonPrefs.kt @@ -3,7 +3,6 @@ package com.droiddeck.launcher.stores.amazon import android.content.Context import android.content.SharedPreferences -/** Amazon's own records that are not credentials: the library cache, posters, versions. Public for the Java clients. */ object AmazonPrefs { @JvmStatic fun get(context: Context): SharedPreferences = context.applicationContext.getSharedPreferences("stores.amazon", Context.MODE_PRIVATE) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadModels.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadModels.kt index 9813f169d..8683efa06 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadModels.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadModels.kt @@ -2,20 +2,9 @@ package com.droiddeck.launcher.stores.download import com.droiddeck.launcher.stores.Store -/** - * The store-agnostic shape every store's download is projected into, so one queue and one - * Downloads page serve all three. Ported from Bannerlator's download registry, trimmed to what - * this app shows: a stage, one byte pair, speed and ETA, and the handles that drive it. - */ -/** Where a download is. The names are what the Downloads page prints over its stage strip. */ enum class DownloadStage { MANIFEST, DOWNLOAD, VERIFY, INSTALL, DONE } -/** - * QUEUED / RUNNING / PAUSED are the active states (in memory only). INSTALLED is the terminal - * success; FAILED and CANCELLED stay listed for the session so the page can offer a retry or a - * dismissal. - */ enum class DownloadState { QUEUED, RUNNING, PAUSED, INSTALLED, FAILED, CANCELLED } data class DownloadEntry( @@ -25,43 +14,28 @@ data class DownloadEntry( val cover: String? = null, val state: DownloadState = DownloadState.QUEUED, val stage: DownloadStage = DownloadStage.MANIFEST, - /** Bytes landed and the total, as the engine reports them (install size). */ val bytesDone: Long = 0L, val bytesTotal: Long = 0L, - /** Smoothed speed in bytes/s (0 = unknown) and seconds left (-1 = unknown). */ val speedBps: Long = 0L, val etaSeconds: Long = -1L, - /** The engine's current line ("Downloading: data.pak"), shown under the bar. */ val detail: String = "", - /** Where it was installed (INSTALLED) or why it failed (FAILED). */ val installPath: String? = null, val error: String? = null, - /** 1-based place in the queue while QUEUED, else 0. */ val queuePosition: Int = 0, - /** When the entry reached a terminal state, for ordering and ageing out. */ val finishedAt: Long = 0L, val startedAt: Long = 0L, - /** Where it is going: the install target's name ("Internal storage", the card's name). */ val location: String = "", - /** - * The active stage's own progress outside Download (which runs on [bytesDone]/[bytesTotal]): - * an amount ([stageDone] of [stageTotal], bytes or items) and, when it counts files, [stageItems] - * of [stageItemsTotal]. Reset when the stage changes. - */ val stageDone: Long = 0L, val stageTotal: Long = 0L, val stageItems: Int = 0, val stageItemsTotal: Int = 0, - /** What the game takes on disk, when the store has said (the download itself is [bytesTotal], often compressed). */ val diskBytes: Long = 0L, - /** Stages already passed, one bit per [DownloadStage] ordinal; stages can come back (Epic checks files before it fetches). */ val stagesPassed: Int = 0, ) { fun passed(s: DownloadStage): Boolean = state == DownloadState.INSTALLED || (stagesPassed and (1 shl s.ordinal)) != 0 val key: String get() = "${store.id}:$id" val isActive: Boolean get() = state == DownloadState.QUEUED || state == DownloadState.RUNNING || state == DownloadState.PAUSED - /** The active stage's progress, 0..1, or -1 while it has nothing to count yet. */ val stageFraction: Float get() = when { state == DownloadState.INSTALLED || stage == DownloadStage.DONE -> 1f stage == DownloadStage.DOWNLOAD -> if (bytesTotal > 0L) (bytesDone.toDouble() / bytesTotal).toFloat().coerceIn(0f, 1f) else -1f @@ -69,7 +43,6 @@ data class DownloadEntry( stageItemsTotal > 0 -> (stageItems.toFloat() / stageItemsTotal).coerceIn(0f, 1f) else -> -1f } - /** 0..1 for a single bar (a card, the game page): the active stage's progress. */ val fraction: Float get() = stageFraction.coerceAtLeast(0f) val percent: Int get() = (fraction * 100).toInt() } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt index 25fd8aa54..93621aa9f 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/DownloadQueue.kt @@ -6,53 +6,23 @@ import com.droiddeck.launcher.session.SessionPrefs import com.droiddeck.launcher.stores.StoresState import java.util.concurrent.atomic.AtomicBoolean -/** - * One queue for every store's downloads, [parallel] of them running at a time (1..3, Setup and - * the Downloads page), the rest waiting as data until a slot frees. A store hands in a - * [DownloadJob] - its whole install: manifest, files, verify, post-install - and reports back - * through the job's [JobHandle]; the queue owns the states, the registry row the UI draws and the - * foreground service that keeps the process alive while anything runs. - * - * Pause for these engines is "stop now, keep the files": every store's install skips files that - * are already complete and verified, so resuming is simply running the job again. Cancel removes - * what was downloaded (the job's own cleanup) and the row. - * - * Thread-safe: one [lock] guards the table; jobs run on their own threads, outside it. - */ object DownloadQueue { private const val TAG = "StoreDownloads" - /** The work for one download. [run] blocks until it is over; [cancel] asks a running job to stop. */ interface DownloadJob { - /** Runs the install; returns the install folder's path on success, throws or returns null on failure. */ fun run(handle: JobHandle): String? - /** Stops a running job; [deleteFiles] false keeps what was fetched for a later resume. */ fun cancel(deleteFiles: Boolean) } - /** What a running job reports with. */ class JobHandle internal constructor(val key: String, internal val cancelled: AtomicBoolean) { val isCancelled: Boolean get() = cancelled.get() - /** Enters [stage] (a repeat of the current one only updates the line); a new stage starts its own count from zero. */ fun stage(stage: DownloadStage, detail: String = "") = enter(key, stage, stripSpeed(detail)) - /** - * The active stage's own count outside Download: an amount and, when it counts files, the - * files. [bytes] = the amount is bytes written, so the stage gets its own rate and ETA. - */ fun stageProgress(done: Long, total: Long, items: Int = 0, itemsTotal: Int = 0, bytes: Boolean = false) = stageCount(key, done, total, items, itemsTotal, bytes) - /** The game's size on disk, once the store has said. */ fun diskSize(bytes: Long) = update(key) { if (bytes > 0) it.copy(diskBytes = bytes) else it } - /** - * Bytes so far (negative = unchanged), the total (<= 0 = unchanged), a line. [speedBps] is the - * engine's own figure: a burst rate at the network side, which jumps while the write side - * paces the bar, so it is not what the page shows - the queue measures the install rate - * from the byte deltas itself ([measure]); the engine's figure stays in its log lines. - */ fun progress(bytesDone: Long, bytesTotal: Long, detail: String? = null, @Suppress("UNUSED_PARAMETER") speedBps: Long = -1L) = measure(key, bytesDone, bytesTotal, detail?.let(::stripSpeed)) fun log(line: String) = StoresState.logLine(line) } - /** "Downloading: data.pak 12.3 MB/s" → "Downloading: data.pak": the engine's burst rate is not shown twice. */ private fun stripSpeed(s: String): String = s.replace(Regex("\\s+\\S+ [KMG]B/s\\s*$"), "").trimEnd() private fun enter(key: String, stage: DownloadStage, detail: String) { @@ -61,16 +31,10 @@ object DownloadQueue { val moved = item.entry.stage != stage || item.entry.state != DownloadState.RUNNING transitionLocked(item, stage) item.entry = item.entry.copy(detail = detail.ifEmpty { item.entry.detail }, state = DownloadState.RUNNING) - // A new stage shows at once; a new line within the same stage (one per file) waits its turn. publishLocked(progressOnly = !moved) } } - /** - * Moves an item to [stage] when it is elsewhere: the old stage is marked passed, the new one - * counts from zero, and the measured speed is cleared - the next stage writes or checks, it - * does not fetch, and a stale rate there would read as a download. Caller holds [lock]. - */ private fun transitionLocked(item: Item, stage: DownloadStage) { val e = item.entry if (e.stage == stage) return @@ -81,7 +45,6 @@ object DownloadQueue { ) } - /** How far back the shown speed looks: a few seconds, so the figure settles instead of following each file. */ private const val SPEED_WINDOW_MS = 3000.0 private fun stageCount(key: String, done: Long, total: Long, files: Int, filesTotal: Int, bytes: Boolean) { @@ -96,13 +59,10 @@ object DownloadQueue { } } - /** One sample of the rate behind [done] (bytes so far in this stage), smoothed over [SPEED_WINDOW_MS]. Caller holds [lock]. */ private fun sample(item: Item, done: Long): Long { val now = System.currentTimeMillis() if (item.speedAt == 0L) { item.speedAt = now; item.speedBytes = done; return item.speedEwma.toLong() } val dt = now - item.speedAt - // Sampled no faster than every quarter second, as an exponential average over - // SPEED_WINDOW_MS: one big file landing moves it, it does not define it. if (dt >= 250) { val inst = (done - item.speedBytes).coerceAtLeast(0) * 1000.0 / dt val alpha = 1.0 - Math.exp(-dt / SPEED_WINDOW_MS) @@ -129,7 +89,6 @@ object DownloadQueue { } private class Item(var entry: DownloadEntry, val factory: () -> DownloadJob, var job: DownloadJob? = null, val cancelled: AtomicBoolean = AtomicBoolean(false), var pauseRequested: Boolean = false) { - /** The shown speed's state: the last sample's clock and bytes, and the running average (bytes/s). */ var speedAt = 0L var speedBytes = 0L var speedEwma = 0.0 @@ -137,16 +96,13 @@ object DownloadQueue { private val lock = Any() private val items = LinkedHashMap<String, Item>() - /** Keys a restart brings back paused: queued as paused, not started. */ private val heldPaused = HashSet<String>() - /** [key]'s next [enqueue] comes in paused (a paused download restored after a restart). */ fun holdPaused(key: String) = synchronized(lock) { heldPaused.add(key) } private var running = 0 fun parallel(context: Context): Int = SessionPrefs.gameStoresParallel(context) - /** Threads for a store's download pool: background priority, so the UI and its render thread come first. */ @JvmStatic fun workerFactory(name: String): java.util.concurrent.ThreadFactory { val n = java.util.concurrent.atomic.AtomicInteger() @@ -167,10 +123,6 @@ object DownloadQueue { fun isActive(key: String): Boolean = entry(key)?.isActive == true - /** - * Queues a download, or starts it when a slot is free. A key already active is left alone; a - * finished or failed row with the same key is replaced. - */ fun enqueue(context: Context, entry: DownloadEntry, factory: () -> DownloadJob) { val app = context.applicationContext synchronized(lock) { @@ -230,20 +182,14 @@ object DownloadQueue { item.cancelled.set(true) item.job?.cancel(deleteFiles = true) } else { - // Never started: nothing is on disk, but the job knows where it would have gone. Thread({ runCatching { item.factory().cancel(deleteFiles = true) } }, "store-dl-cancel").start() } StoresState.logLine("cancelled \"${item.entry.name}\"") StoreDownloadService.finish(app, key) advance(app) - // The row says Cancelled for a moment, then goes; the game reads Install again. Thread({ Thread.sleep(3000); synchronized(lock) { if (items[key]?.entry?.state == DownloadState.CANCELLED) { items.remove(key); publishLocked() } } }, "store-dl-cancelled").start() } - /** - * Clear on a failed or paused row: deletes what it kept exactly as Cancel does (the job's own - * cleanup), then drops the row. An installed or cancelled row is only dropped. - */ fun clear(context: Context, key: String) { val item = synchronized(lock) { items[key] } ?: return if (item.entry.state == DownloadState.RUNNING || item.entry.state == DownloadState.QUEUED) return @@ -254,7 +200,6 @@ object DownloadQueue { StoreDownloadService.finish(context.applicationContext, key) } - /** Drops every finished, failed or cancelled row; files a failed one kept stay (its game page's Clear removes them). */ fun dismissFinished() { synchronized(lock) { items.entries.removeAll { !it.value.entry.isActive } @@ -262,7 +207,6 @@ object DownloadQueue { } } - /** Drops a finished, failed or cancelled row from the list. */ fun dismiss(key: String) { synchronized(lock) { val item = items[key] ?: return @@ -287,18 +231,11 @@ object DownloadQueue { private fun publish(context: Context) { synchronized(lock) { publishLocked() }; StoreDownloadService.sync(context) } - /** At most this often a progress-only change reaches the UI; a state or stage change goes at once. */ private const val PROGRESS_PUBLISH_MS = 250L private var lastPublishAt = 0L private var publishPending = false private val main = android.os.Handler(android.os.Looper.getMainLooper()) - /** - * Newest first: what was started last sits on top, as the preview lists them. Caller holds - * [lock]. Engines report per chunk or per file - hundreds of times a second at full speed - so - * [progressOnly] changes are coalesced to [PROGRESS_PUBLISH_MS]; the UI then recomposes a few - * times a second, not on every callback. - */ private fun publishLocked(progressOnly: Boolean = false) { val now = android.os.SystemClock.uptimeMillis() if (progressOnly) { @@ -312,13 +249,11 @@ object DownloadQueue { } } lastPublishAt = now - // A state change (not progress) is written down, so the queue outlives the app. if (!progressOnly) SavedQueue.save(items.values.map { it.entry }) val list = items.values.map { it.entry }.sortedByDescending { it.startedAt } StoresState.post { StoresState.publishDownloads(list) } } - /** Starts queued items while slots are free. */ private fun advance(context: Context) { while (true) { val next: Item @@ -336,8 +271,6 @@ object DownloadQueue { } private fun runItem(context: Context, item: Item) { - // Downloads run beside the UI, never ahead of it: background priority for this thread and - // (through [workerFactory]) every pool a store's manager starts. android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND) val handle = JobHandle(item.entry.key, item.cancelled) var result: String? = null @@ -367,7 +300,6 @@ object DownloadQueue { when (item.entry.state) { DownloadState.INSTALLED -> { StoresState.logLine("installed \"${item.entry.name}\" → ${result}") - // The folder is on disk and registered: the Installed tab and the Games list follow. StoresState.notifyLibraryChanged(context) val games = runCatching { com.droiddeck.launcher.frontend.Library.launchableGames(context).firstOrNull { it.gameFiles?.absolutePath == result }?.let { "app:${it.appId}" } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/SavedQueue.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/SavedQueue.kt index 310e66b0d..0c9fd0ccb 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/SavedQueue.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/SavedQueue.kt @@ -10,14 +10,6 @@ import org.json.JSONObject import java.io.File import java.util.concurrent.Executors -/** - * The download queue kept on disk, so what was queued, running or paused comes back after the app - * closes or is killed: the queue itself lives in memory, and a queued game that never started has - * nothing on disk to find. Each install records the game and where it goes ([remember]); every - * change of state writes the active rows in queue order ([save]); the first refresh after the app - * starts asks the stores for them again ([restore]) - running and queued ones carry on, paused ones - * come back paused. - */ object SavedQueue { private const val TAG = "StoreDownloads" private const val FILE = "store-queue.json" @@ -31,14 +23,12 @@ object SavedQueue { private fun key(item: CatalogItem) = "${item.store.id}:${item.id}" - /** An install is asked for: what to ask the store for again after a restart. */ @Synchronized fun remember(context: Context, item: CatalogItem, root: File?) { app = context.applicationContext wanted[key(item)] = Wanted(item, root) } - /** The active rows, in queue order, as they are now. Any thread; written off it. */ fun save(entries: List<DownloadEntry>) { val context = app ?: return val rows = synchronized(this) { @@ -60,11 +50,9 @@ object SavedQueue { } } - /** Once per process: queues again what was in the queue when the app last closed. */ fun restore(context: Context) { synchronized(this) { if (restored) return; restored = true } val app = context.applicationContext - // Read before anything may write it: an install started at once must not save over it. val rows = runCatching { JSONArray(File(app.filesDir, FILE).readText()) }.getOrNull() this.app = app rows ?: return diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt index e8c71f248..791dd06b1 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadService.kt @@ -23,21 +23,10 @@ import com.droiddeck.launcher.stores.formatBytes import com.droiddeck.launcher.stores.formatSpeed import java.util.concurrent.ConcurrentHashMap -/** - * Keeps store downloads going with the screen off or the app in the background: a foreground - * service (dataSync) with a notification showing the active download's progress, Pause / Resume - * and a two-step Cancel; a Wi-Fi lock and a partial wake lock while anything is running, so the - * radio and the CPU do not sleep under a multi-gigabyte install; and a notification when a game - * has installed or failed. - * - * [start] is called when a download begins and [finish] when one ends; the service stops itself when - * nothing is running. - */ class StoreDownloadService : Service() { private var wifi: WifiManager.WifiLock? = null private var wake: PowerManager.WakeLock? = null private var wakeAt = 0L - /** The download whose Cancel was tapped once: its actions read Delete / Keep until [confirmUntil]. */ private var confirmKey: String? = null private var confirmUntil = 0L private val main = Handler(Looper.getMainLooper()) @@ -55,7 +44,6 @@ class StoreDownloadService : Service() { when (intent?.action) { ACTION_PAUSE -> key?.let { DownloadQueue.pause(it) - // The progress notification goes with the last running download; this one stays to resume from. pausedNote(this, it) } ACTION_RESUME -> key?.let { @@ -126,11 +114,6 @@ class StoreDownloadService : Service() { runCatching { (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).notify(ID, build()) } } - /** - * While a download runs: a Wi-Fi lock (high-performance before Android 14, low-latency from it, - * where the old mode is deprecated) and a partial wake lock, taken with a timeout and renewed - * as progress comes in rather than held indefinitely. Released as soon as nothing runs. - */ @SuppressLint("WakelockTimeout") private fun locks() { val running = StoresState.latestDownloads.any { it.state == DownloadState.RUNNING } @@ -188,7 +171,6 @@ class StoreDownloadService : Service() { private const val ACTION_CANCEL = "droiddeck.stores.CANCEL" private const val ACTION_DELETE = "droiddeck.stores.DELETE" private const val ACTION_KEEP = "droiddeck.stores.KEEP" - /** MainActivity: which page to open ("stores:downloads", or a Games selection such as "app:<id>"). */ const val EXTRA_NAV = "droiddeck.nav" private val active = ConcurrentHashMap.newKeySet<String>() @Volatile private var instance: StoreDownloadService? = null @@ -212,7 +194,6 @@ class StoreDownloadService : Service() { private fun openDownloads(context: Context, code: Int) = openPage(context, "stores:downloads", code) - /** A download started: the service is up (idempotent). */ fun start(context: Context) { StoresState.latestDownloads.filter { it.state == DownloadState.RUNNING }.forEach { active.add(it.key) } try { @@ -224,17 +205,14 @@ class StoreDownloadService : Service() { } } - /** Progress moved: redraw the notification (StoresState calls it at most once a second). */ fun tick() { instance?.refresh() } - /** The queue changed: redraw the notification. */ fun sync(context: Context) { instance?.refresh() } - /** A download ended: drop it; the service stops with the last one. */ fun finish(context: Context, key: String) { active.remove(key) val inst = instance ?: return @@ -243,7 +221,6 @@ class StoreDownloadService : Service() { private fun pausedId(key: String) = ID + 0x20000 + (key.hashCode() and 0xFFFF) - /** "<game> paused" with Resume, once the progress notification has gone with the service. */ private fun pausedNote(context: Context, key: String) { val entry = StoresState.latestDownloads.firstOrNull { it.key == key } ?: return val i = Intent(context, StoreDownloadService::class.java).setAction(ACTION_RESUME).putExtra(EXTRA_KEY, key) @@ -258,7 +235,6 @@ class StoreDownloadService : Service() { runCatching { context.getSystemService(NotificationManager::class.java)?.notify(pausedId(key), n) } } - /** "<game> installed" (opens it in Games) or "<game> download failed" (opens Downloads); dismissible. */ fun ended(context: Context, entry: DownloadEntry, installed: Boolean, gamesKey: String?) { val app = context.applicationContext channels(app) @@ -274,7 +250,6 @@ class StoreDownloadService : Service() { } } -/** The notification's one line for a download. Pure, for the test. */ object DownloadNotificationText { fun line(d: DownloadEntry, stageName: String, paused: String, queued: String): String { val parts = ArrayList<String>() diff --git a/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadTier.kt b/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadTier.kt index 3b12de899..765ec8a3d 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadTier.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/download/StoreDownloadTier.kt @@ -5,21 +5,11 @@ import androidx.annotation.StringRes import com.droiddeck.launcher.R import com.droiddeck.launcher.session.SessionPrefs -/** - * How hard the native download engine may pull: the ceiling of chunk requests it keeps in flight, - * and the inflate / hash / write threads behind them. Three tiers, one app-wide setting (Setup › - * Stores and the Downloads page), read when a download starts so a change applies to the next one. - * - * The windows are the ones the engine was tuned with (16 / 32 / 96 requests): it ramps towards the - * ceiling only while throughput keeps rising, so a slow link settles well below any of them. - */ enum class StoreDownloadTier(val id: String, @StringRes val label: Int, val networkWindow: Int, val decompressPerCore: Double) { BALANCED("balanced", R.string.stores_tier_balanced, 16, 0.4), FAST("fast", R.string.stores_tier_fast, 32, 0.5), MAX("max", R.string.stores_tier_max, 96, 0.8); - /** Inflate / hash / write workers for this device, at least two so a fetch never waits on a single writer. */ - /** Inflate / hash / write threads: by the tier, but two cores always left for the UI (at least 2 workers). */ val processWorkers: Int get() { val cores = Runtime.getRuntime().availableProcessors() return (cores * decompressPerCore).toInt().coerceIn(2, maxOf(2, minOf(32, cores - 2))) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAchievements.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAchievements.kt index 1c7e6682b..7d278e04d 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAchievements.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAchievements.kt @@ -6,7 +6,6 @@ import com.droiddeck.launcher.stores.StoreAchievements import com.droiddeck.launcher.stores.StoreNet import org.json.JSONObject -/** Epic's GraphQL: the sandbox's achievements, then the player's records in it. */ object EpicAchievements : StoreAchievements(Store.EPIC.id, Store.EPIC.shortLabel) { private const val GRAPHQL = "https://launcher.store.epicgames.com/graphql" private const val USER_AGENT = "EpicGamesLauncher/14.0.8-22004686+++Portal+Release-Live" @@ -44,7 +43,6 @@ object EpicAchievements : StoreAchievements(Store.EPIC.id, Store.EPIC.shortLabel return Result.Found(out) } - /** Achievement name to unlock time, for the ones the player has. */ private fun unlockTimes(token: String, account: String, namespace: String): Map<String, Long> { val out = HashMap<String, Long>() val records = query(token, PLAYER_ACHIEVEMENTS, JSONObject().put("epicAccountId", account).put("sandboxId", namespace)) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicApiClient.java b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicApiClient.java index 44921af76..9ae321671 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicApiClient.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicApiClient.java @@ -10,13 +10,6 @@ import org.json.JSONObject; import java.util.ArrayList; import java.util.List; -/** - * Epic's library, catalog and launcher-assets services, as the launcher calls them: - * library-service.live.use1a.on.epicgames.com/library/api/public/items (owned items, paginated) - * catalog-public-service-prod06.ol.epicgames.com/catalog/api/shared/namespace/{ns}/bulk/items - * launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/v2/platform/Windows/... - * All with a bearer token. - */ public final class EpicApiClient { private static final String TAG = "EpicApi"; @@ -27,7 +20,6 @@ public final class EpicApiClient { private EpicApiClient() {} - /** The owned games, every page; Unreal assets, private sandboxes and non-Windows items left out. */ public static List<EpicGame> getLibraryItems(String accessToken) { List<EpicGame> result = new ArrayList<>(); String cursor = null; @@ -72,7 +64,6 @@ public final class EpicApiClient { return result; } - /** Fills title, developer, art, description and the DLC flag from the catalog; true on success. */ public static boolean enrichFromCatalog(String accessToken, EpicGame game) { if (game.namespace.isEmpty() || game.catalogItemId.isEmpty()) return false; try { @@ -118,10 +109,6 @@ public final class EpicApiClient { } } - /** - * The manifest API answer for a game, flattened to {"manifests": [...], "versionId": "..."} as - * the download manager reads it. {@code appName} is the library record's, not the catalog's. - */ public static String getManifestApiJson(String accessToken, String namespace, String catalogItemId, String appName) { try { String url = MANIFEST_BASE + "/" + namespace + "/catalogItem/" + catalogItemId + "/app/" + appName + "/label/Live"; @@ -136,7 +123,6 @@ public final class EpicApiClient { String ver = first.optString("buildVersion", ""); if (ver.isEmpty()) ver = first.optString("versionId", ""); wrapper.put("versionId", ver); - // The sidecar's EOS deployment id, when the app has one. JSONObject sidecar = first.optJSONObject("sidecar"); if (sidecar != null) { String config = sidecar.optString("config", ""); @@ -151,12 +137,10 @@ public final class EpicApiClient { } } - /** `customAttributes.AdditionalCommandLine.value` for the game, "" when it has none, null when the call failed. */ public static String getAdditionalCommandLine(String accessToken, String namespace, String catalogItemId) { return getCustomAttribute(accessToken, namespace, catalogItemId, "AdditionalCommandLine"); } - /** `customAttributes.<name>.value` for the game (CloudSaveFolder, AdditionalCommandLine), "" when it has none, null when the call failed. */ public static String getCustomAttribute(String accessToken, String namespace, String catalogItemId, String name) { try { String url = CATALOG_BASE + "/" + namespace + "/bulk/items?id=" + catalogItemId + "&country=US"; diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAuthClient.java b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAuthClient.java index 959d4302b..2303d06d1 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAuthClient.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicAuthClient.java @@ -16,14 +16,6 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; -/** - * Epic's OAuth2 token endpoint, with the launcher's public client credentials (the ones every - * third-party Epic client uses): - * POST account-public-service-prod03.ol.epicgames.com/account/api/oauth/token - * Basic base64(clientId:clientSecret), form grant_type=authorization_code|refresh_token, token_type=eg1 - * GET .../account/api/oauth/exchange with a bearer token: a short-lived exchange code for a launch. - * Error bodies are never logged: they carry correlation ids and account context. - */ public final class EpicAuthClient { private static final String TAG = "EpicAuth"; @@ -91,11 +83,9 @@ public final class EpicAuthClient { } } - /** A short-lived exchange code for a game launch (-AUTH_PASSWORD); null on any failure. */ public static String getExchangeCode(String accessToken) { try { String resp = getRequest(EXCHANGE_URL, accessToken); - // getRequest has logged the HTTP status; the code itself is never logged. if (resp == null) { Log.i(TAG, "exchange code: no answer"); return null; } return new JSONObject(resp).optString("code", null); } catch (Exception e) { @@ -104,7 +94,6 @@ public final class EpicAuthClient { } } - /** Authorized GET; the body or null. */ public static String getRequest(String urlStr, String accessToken) { HttpURLConnection conn = null; try { @@ -127,7 +116,6 @@ public final class EpicAuthClient { } } - /** Authorized GET of raw bytes (the manifest binary). */ public static byte[] getBytes(String urlStr, String accessToken) { HttpURLConnection conn = null; try { @@ -166,7 +154,6 @@ public final class EpicAuthClient { return sb.toString(); } - /** "2026-03-29T12:00:00.000Z" to epoch millis, without java.time. */ private static long parseIso8601(String s) { int year = Integer.parseInt(s.substring(0, 4)); int month = Integer.parseInt(s.substring(5, 7)); diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt index 97c3cd3a9..e631cf78b 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt @@ -24,7 +24,6 @@ import org.json.JSONObject import java.io.File import java.util.concurrent.atomic.AtomicBoolean -/** Epic Games in the Stores section: sign-in, the library, the public store, installs and the per-launch sign-in code. */ object EpicBackend : StoreBackend, EpicLaunchSupport { private const val TAG = "EpicBackend" override val store: Store = Store.EPIC @@ -42,7 +41,6 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { override fun syncLibrary(context: Context, force: Boolean) { val app = context.applicationContext Thread({ - // The cache off the main thread; with it on screen a background sync keeps quiet. val cached = EpicLibrary.cached(app).map { EpicLibrary.toCatalogItem(app, it) } val quiet = cached.isNotEmpty() && !force StoresState.post { @@ -74,13 +72,11 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { }, "epic-shelves").start() } - /** Store offers whose catalog item is in the library are owned: their cards offer Install under the library's id. */ private fun markOwned(s: StoreShelves): StoreShelves { val owned = StoresState.library[store] ?: return s val byCatalogItem = owned.associateBy { it.extra["catalogItemId"] ?: "" } fun mark(l: List<CatalogItem>) = l.map { i -> val match = i.extra["items"]?.split(',')?.firstNotNullOfOrNull { byCatalogItem[it] } - // The offer's art, and its facts where the library's catalog record has none. if (match != null) match.copy( imageUrl = i.imageUrl ?: match.imageUrl, tallImageUrl = i.tallImageUrl ?: match.tallImageUrl, developer = match.developer.ifBlank { i.developer }, publisher = match.publisher.ifBlank { i.publisher }, @@ -102,21 +98,15 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { override fun uninstall(context: Context, game: InstalledStoreGame) {} - /** A fresh exchange code for the signed-in account; null when signed out or offline. Blocking. */ override fun exchangeCode(context: Context): String? { val token = EpicCredentialStore.getValidAccessToken(context) ?: return null return EpicAuthClient.getExchangeCode(token) } - /** The access token is past its time and the refresh just failed: only a new sign-in helps. */ override fun signInExpired(context: Context): Boolean = EpicCredentialStore.expired(context) private class InstallJob(val app: Context, val item: CatalogItem, val namespace: String, val catalogItemId: String, val folder: File) : DownloadQueue.DownloadJob { private val cancelled = AtomicBoolean(false) - // On a card the in-flight chunks go to the app's cache instead: the card's write rate paces - // the whole install, and chunks are read back once at assembly. Internal installs keep the - // cache beside the game. A resume keeps whichever cache the stopped run filled: a - // `.chunks` already beside the game is reused, not fetched again into the scratch folder. private val scratch: File? = if (StoreInstallRoot.isRemovable(app, folder) && !hasChunks(File(folder, ".chunks"))) StoreInstallRoot.scratchDir(app, Store.EPIC, item.id) else null private fun hasChunks(dir: File): Boolean = dir.isDirectory && (dir.list()?.isNotEmpty() == true) @@ -161,7 +151,6 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { Store.EPIC, item.id, item.title, exe = exe.ifEmpty { "game.exe" }, args = args, installVersion = result.buildVersion, installedAt = System.currentTimeMillis(), cover = item.tallImageUrl ?: item.imageUrl, hero = item.imageUrl, - // Both kept even when empty: the manifest was read, and names no prerequisite. extra = EpicLaunchData.extras(item.id, namespace, catalogItemId, deploymentId) + mapOf( com.droiddeck.launcher.frontend.WinCompSources.EPIC_PREREQ_PATH to result.prereqPath, com.droiddeck.launcher.frontend.WinCompSources.EPIC_PREREQ_NAME to result.prereqName, @@ -177,10 +166,6 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { } } - /** - * Epic's web sign-in: the login page redirects to a JSON page carrying an authorization code, - * read out of the document and exchanged for tokens. - */ private object LoginFlow : StoreLoginActivity.Flow { private const val REDIRECT = "https://www.epicgames.com/id/api/redirect" override val startUrl: String = "https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Fid%2Fapi%2Fredirect%3FclientId%3D${EpicAuthClient.CLIENT_ID}%26responseType%3Dcode" @@ -189,7 +174,6 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { override fun isRedirect(url: String): Boolean = url.startsWith(REDIRECT) override fun finish(activity: StoreLoginActivity, view: WebView, url: String): String? { - // The document's text comes JSON-quoted from evaluateJavascript. var text = url if (text.startsWith("\"")) text = text.substring(1, text.length - 1) text = text.replace("\\n", "").replace("\\r", "").replace("\\\"", "\"") diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicCredentialStore.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicCredentialStore.kt index abf545ae1..58237eed9 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicCredentialStore.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicCredentialStore.kt @@ -6,11 +6,6 @@ import com.droiddeck.launcher.stores.Store import com.droiddeck.launcher.stores.StoreAccounts import org.json.JSONObject -/** - * The Epic sign-in in StoreAccounts' private file: access and refresh tokens, the account id and - * display name (the launch arguments carry both), the expiry. Refreshed when within five minutes - * of expiring. - */ object EpicCredentialStore { private const val TAG = "EpicAuth" @@ -34,9 +29,7 @@ object EpicCredentialStore { @JvmStatic fun isLoggedIn(context: Context): Boolean = load(context) != null - /** A valid access token, refreshed when within five minutes of expiry; null when signed out or the refresh failed. */ @JvmStatic - /** The stored access token is past its expiry (a refresh, if one was tried, did not replace it). */ fun expired(context: Context): Boolean { val creds = load(context) ?: return false return creds.expiresAt in 1 until System.currentTimeMillis() diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java index ad78a04be..958e9b158 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicDownloadManager.java @@ -37,16 +37,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.zip.Inflater; -/** - * The Epic install pipeline, ported from Bannerlator's EpicDownloadManager: the manifest API's CDN - * list, the manifest binary (ChunksV4, or the older JSON form), the files narrowed to the base - * set plus the device's language, a delta pass that skips files already on disk with the right - * size and SHA-1, the chunk fetch into `.chunks/<GUID>` (the native engine when it is there, the - * Java pool otherwise), and the assembly of files from those chunks. - * - * Chunk URLs carry no auth tokens; the subfolder is the decimal group number; the Cloudflare - * mirror is skipped (it refuses chunks). Blocking; the download queue runs it on its own thread. - */ public final class EpicDownloadManager { private static final String TAG = "EpicDownload"; @@ -56,14 +46,7 @@ public final class EpicDownloadManager { void onProgress(String message, int pct); default void onBytes(long done, long total, long speedBps) {} default void onLog(String line) {} - /** - * The active stage's own count: {@code stage} is "check" (the files already there, before - * anything is fetched), "verify" or "install" (bytes written), {@code done} of - * {@code total} an amount (bytes, or items when there are no bytes), {@code items} of - * {@code itemsTotal} the files, when it counts files. - */ default void onStage(String stage, long done, long total, int items, int itemsTotal) {} - /** What this run fetches (compressed) and what the game takes on disk. */ default void onSizes(long downloadBytes, long diskBytes) {} } @@ -99,7 +82,6 @@ public final class EpicDownloadManager { public long fileSize() { long t = 0; for (ChunkPart p : parts) t += (p.size & 0xFFFFFFFFL); return t; } } - /** The parsed manifest: its chunks, files, and from the meta block the exe the launcher starts. */ public static final class Manifest { public String chunkDir = "ChunksV4"; public List<ChunkInfo> uniqueChunks = new ArrayList<>(); @@ -107,20 +89,16 @@ public final class EpicDownloadManager { public String launchExe = ""; public String launchCommand = ""; public String buildVersion = ""; - /** The prerequisite the launcher runs first (UE4PrereqSetup_x64.exe...): what it is called and its path in the install. */ public String prereqName = ""; public String prereqPath = ""; - /** The prerequisite fields were read (an empty name and path then mean the build names none). */ public boolean prereqRead = false; public List<CdnUrl> cdnUrls = new ArrayList<>(); } - /** What an install leaves for the sidecar. */ public static final class Result { public final String launchExe; public final String buildVersion; public final long bytes; - /** The manifest's prerequisite, for Windows components; "" when it names none. */ public String prereqName = ""; public String prereqPath = ""; Result(String launchExe, String buildVersion, long bytes) { this.launchExe = launchExe; this.buildVersion = buildVersion; this.bytes = bytes; } @@ -132,12 +110,6 @@ public final class EpicDownloadManager { InstallException(String message) { super(message); } } - /** - * Installs the app the manifest API JSON describes into {@code installDir}. {@code installTags} - * null = every file. {@code chunkCacheDirPath} "" keeps the chunk cache beside the game - * ({@code <installDir>/.chunks}); a path puts it there instead - the app's cache when the game - * goes to a card. Returns null when cancelled; throws when it fails. - */ public static Result install(Context ctx, String manifestApiJson, String installDirPath, String chunkCacheDirPath, List<String> installTags, AtomicBoolean cancel, Callback cb) throws InstallException { try { if (cancel.get()) return null; @@ -157,8 +129,6 @@ public final class EpicDownloadManager { File installDir = new File(installDirPath); installDir.mkdirs(); - // "" = the cache beside the game; a path = the scratch cache (the app's own storage when - // the game goes to a card). The engine and the Java loops read the same value. final String cachePath = chunkCacheDirPath == null ? "" : chunkCacheDirPath; final File chunkCacheDir = cachePath.isEmpty() ? new File(installDir, ".chunks") : new File(cachePath); chunkCacheDir.mkdirs(); @@ -168,7 +138,6 @@ public final class EpicDownloadManager { long installBytes = 0; for (FileInfo f : selected) installBytes += f.fileSize(); final long planned = installBytes; - // Free space is checked after the delta pass, against what is still missing (below). cb.onProgress("Checking files…", 0); List<FileInfo> pending = new ArrayList<>(selected.size()); @@ -191,10 +160,6 @@ public final class EpicDownloadManager { return new Result(manifest.launchExe, manifest.buildVersion, planned).withPrereq(manifest); } List<ChunkInfo> needed = uniqueChunksForFiles(manifest, pending); - // The cache holds whole chunk windows (~1 MiB each), and a window is shared with files - // this device does not install (other tags, other builds' data): the cache is often - // larger than the game itself (Metalstorm: 9.1 GB of chunks for 4.4 GB of files). Both - // have to fit at once until assembly, so the check counts both, on their own volumes. long cacheMissing = 0, pendingBytes = 0; for (ChunkInfo c : needed) if (!new File(chunkCacheDir, c.guidStr()).isFile()) cacheMissing += Math.max(c.windowSize, 1); for (FileInfo f : pending) pendingBytes += f.fileSize(); @@ -267,8 +232,6 @@ public final class EpicDownloadManager { if (cancel.get()) return null; if (failCount.get() > 0) throw new InstallException(failCount.get() + " chunk(s) failed to download"); - // The engine's assembler when it starts (it drops each chunk after its last use and the - // cache at the end); the loop below when it does not. if (engine) { NativeOutcome r = runNativeAssembly(manifestBytes, pendingIdx, pending.size(), installDirPath, cachePath, cancel, cb); if (r.started) { @@ -314,7 +277,6 @@ public final class EpicDownloadManager { private static final class NativeOutcome { boolean started, success, cancelled; String error = ""; } - /** The pending files as indices into the manifest's file list, what both native runs take; null when one is not in it. */ private static int[] pendingIndices(Manifest manifest, List<FileInfo> pending) { java.util.IdentityHashMap<FileInfo, Integer> index = new java.util.IdentityHashMap<>(); for (int i = 0; i < manifest.files.size(); i++) index.put(manifest.files.get(i), i); @@ -327,7 +289,6 @@ public final class EpicDownloadManager { return pendingIdx; } - /** The native chunk pool on the same counters; never throws. */ private static NativeOutcome runNativePool(Context ctx, byte[] manifestBytes, int[] pendingIdx, List<ChunkInfo> needed, long totalBytes, String installDirPath, String cachePath, List<CdnUrl> cdnUrls, AtomicBoolean cancel, Callback cb, AtomicLong completedBytes, AtomicInteger completedCount, AtomicLong lastSpeedMs, AtomicLong lastSpeedBytes, AtomicLong speedBps) { @@ -361,7 +322,6 @@ public final class EpicDownloadManager { return r; } - /** The native assembler: the pending files written from the cache, the cache removed on success; never throws. */ private static NativeOutcome runNativeAssembly(byte[] manifestBytes, int[] pendingIdx, int totalFiles, String installDirPath, String cachePath, AtomicBoolean cancel, Callback cb) { NativeOutcome r = new NativeOutcome(); try { @@ -383,7 +343,6 @@ public final class EpicDownloadManager { return r; } - // ---- CDN list and manifest download --------------------------------------------------------- public static List<CdnUrl> parseCdnUrls(String json) { List<CdnUrl> result = new ArrayList<>(); @@ -419,7 +378,6 @@ public final class EpicDownloadManager { return result; } - /** The manifest binary from the first CDN that serves it; the auth query goes on this URL only. */ public static byte[] downloadManifest(String json, List<CdnUrl> cdnUrls) { try { JSONArray manifests = new JSONObject(json).optJSONArray("manifests"); @@ -439,7 +397,6 @@ public final class EpicDownloadManager { return null; } - // ---- manifest parsing ----------------------------------------------------------------------- public static Manifest parseManifest(byte[] bytes) { try { @@ -468,24 +425,22 @@ public final class EpicDownloadManager { ByteBuffer b = ByteBuffer.wrap(body).order(ByteOrder.LITTLE_ENDIAN); Manifest m = new Manifest(); m.chunkDir = chunkDir; - // ManifestMeta: the launch exe and build version live here; the rest is skipped by size. int metaStart = b.position(); int metaSize = b.getInt(); try { - b.get(); // data version - b.getInt(); // feature level - b.get(); // isFileData - b.getInt(); // app id - readFString(b); // app name + b.get(); + b.getInt(); + b.get(); + b.getInt(); + readFString(b); m.buildVersion = readFString(b); m.launchExe = readFString(b).replace('\\', '/'); m.launchCommand = readFString(b); - // PrereqIds (an array), PrereqName, PrereqPath, PrereqArgs. int prereqIds = b.getInt(); for (int i = 0; i < prereqIds && i < 64; i++) readFString(b); m.prereqName = readFString(b); m.prereqPath = readFString(b).replace('\\', '/'); - readFString(b); // PrereqArgs + readFString(b); m.prereqRead = true; } catch (Exception e) { Log.w(TAG, "manifest meta: " + e.getClass().getSimpleName()); @@ -603,11 +558,6 @@ public final class EpicDownloadManager { } } - /** - * A JSON manifest writes every number as a "blob": each byte as three decimal digits, least - * significant first ("013000000000" = 13), as Legendary's blob_to_num reads it. Bits past 64 - * are dropped; a group that is not digits throws. - */ static long blobToNum(String blob) { long num = 0; for (int i = 0, shift = 0; i < blob.length(); i += 3, shift += 8) { @@ -637,7 +587,6 @@ public final class EpicDownloadManager { return new String(bytes, StandardCharsets.US_ASCII); } - // ---- chunk fetch (the built-in pool) -------------------------------------------------------- public static byte[] downloadBytes(String urlStr) { HttpURLConnection conn = null; @@ -656,7 +605,6 @@ public final class EpicDownloadManager { } } - /** One chunk streamed from the first CDN that serves it, SHA-1 checked, published by rename. */ private static boolean downloadChunkStreaming(ChunkInfo chunk, String chunkDir, List<CdnUrl> cdnUrls, File outFile) { String chunkPath = chunk.getPath(chunkDir); File tmp = new File(outFile.getPath() + ".part"); @@ -743,7 +691,6 @@ public final class EpicDownloadManager { while (remaining > 0) { int n = in.read(skip, 0, Math.min(skip.length, remaining)); if (n < 0) throw new IOException("stream ended"); remaining -= n; } } - // ---- selection and verification ------------------------------------------------------------- public static List<FileInfo> getRequiredInstallFiles(Manifest manifest) { List<FileInfo> required = new ArrayList<>(); @@ -751,7 +698,6 @@ public final class EpicDownloadManager { return required.isEmpty() ? new ArrayList<>(manifest.files) : required; } - /** Base files plus any file carrying one of the tags; null tags = every file; nothing matched = base only. */ public static List<FileInfo> resolveInstallFiles(Manifest manifest, List<String> selectedTags) { if (selectedTags == null) return new ArrayList<>(manifest.files); if (selectedTags.isEmpty()) return getRequiredInstallFiles(manifest); @@ -790,7 +736,6 @@ public final class EpicDownloadManager { } } - /** The manifest's prerequisite as {name, path}, from the manifest alone; null on failure. Blocking. */ public static String[] fetchPrereq(String manifestApiJson) { try { List<CdnUrl> cdnUrls = parseCdnUrls(manifestApiJson); @@ -806,7 +751,6 @@ public final class EpicDownloadManager { } } - /** The install size for the tags (null = every file), from the manifest alone; -1 on failure. Blocking. */ public static long fetchInstallSizeBytes(String manifestApiJson, List<String> installTags) { try { List<CdnUrl> cdnUrls = parseCdnUrls(manifestApiJson); @@ -823,7 +767,6 @@ public final class EpicDownloadManager { } } - // ---- helpers -------------------------------------------------------------------------------- static byte[] readFile(File f) throws IOException { try (FileInputStream fis = new FileInputStream(f)) { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicGame.java b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicGame.java index ba91eb32a..43ba8069b 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicGame.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicGame.java @@ -1,10 +1,5 @@ package com.droiddeck.launcher.stores.epic; -/** - * One Epic Games library entry. {@code appName} is the launcher's app slug (the manifest URL and - * the launch arguments take it), {@code namespace} the product's sandbox, {@code catalogItemId} - * the catalog UUID; the three together address every Epic API call for the game. - */ public class EpicGame { public String appName = ""; public String namespace = ""; @@ -12,9 +7,7 @@ public class EpicGame { public String title = ""; public String developer = ""; public String description = ""; - /** DieselGameBoxTall: the tall portrait art. */ public String artCover = ""; - /** DieselGameBox or Thumbnail. */ public String artSquare = ""; public String version = ""; public long installSize = 0L; diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicInstallTags.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicInstallTags.kt index dcb178d50..9ad136a84 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicInstallTags.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicInstallTags.kt @@ -2,11 +2,6 @@ package com.droiddeck.launcher.stores.epic import java.util.Locale -/** - * Epic tags a manifest's files by language so a client installs the base files plus one language - * instead of every language. The table (the launcher's tag names per language) is Legendary's; - * the language is the device's locale, English when it is not one Epic ships. - */ object EpicInstallTags { private val LANGUAGE_TO_TAGS: Map<String, List<String>> = mapOf( "ar" to listOf("Arabic", "ar"), "bg" to listOf("Bulgarian", "bg-BG", "bg"), @@ -34,11 +29,9 @@ object EpicInstallTags { } } - /** The install tags for the device's language; English when Epic has no tags for it. Never empty. */ @JvmStatic fun tagsForDevice(): List<String> = LANGUAGE_TO_TAGS[key(Locale.getDefault()) ?: "en"] ?: LANGUAGE_TO_TAGS.getValue("en") - /** The two-letter code handed to the game as -epiclocale; "en" when the language is not one Epic knows. */ @JvmStatic fun localeCodeForDevice(): String = key(Locale.getDefault())?.substringBefore('-') ?: "en" } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLaunchData.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLaunchData.kt index 6119bda75..1452ef447 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLaunchData.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLaunchData.kt @@ -4,21 +4,9 @@ import android.content.Context import android.util.Log import com.droiddeck.launcher.stores.StoreGameSidecar -/** - * What an Epic game's launch line needs beyond the exe, built once at install time into the - * sidecar's arguments: the portal and identity arguments every EOS title expects - * (`-EpicPortal -epicusername -epicuserid -epicsandboxid -epiclocale -epicapp -epicenv=Prod`), the - * deployment id when the app has an EOS sidecar, and the extra flags Epic ships for the title - * (`AdditionalCommandLine`). The sign-in triple (`-AUTH_LOGIN/-AUTH_PASSWORD/-AUTH_TYPE`) is not - * here: the exchange code lives minutes, so the launcher .bat takes it from a file the app writes - * right before a launch (StoreLaunch.prepare). - * - * The EOS overlay is never provisioned: nothing points the game's EOS SDK at an overlay component. - */ object EpicLaunchData { private const val TAG = "EpicLaunch" - /** The static launch arguments for [appName] in [namespace]. Blocking: one catalog call for the extra flags. */ fun arguments(context: Context, token: String?, appName: String, namespace: String, catalogItemId: String, deploymentId: String): List<String> { val creds = EpicCredentialStore.load(context) val displayName = creds?.displayName?.takeIf { it.isNotEmpty() } ?: "EpicUser" @@ -42,13 +30,11 @@ object EpicLaunchData { return args } - /** The sidecar's identifiers the launcher reads back. */ fun extras(appName: String, namespace: String, catalogItemId: String, deploymentId: String): Map<String, String> = buildMap { put("appName", appName); put("namespace", namespace); put("catalogItemId", catalogItemId) if (deploymentId.isNotEmpty()) put("deploymentId", deploymentId) } - /** True when [sidecar] is an Epic install that can take a sign-in code. */ fun wantsExchangeCode(sidecar: StoreGameSidecar): Boolean = sidecar.store == com.droiddeck.launcher.stores.Store.EPIC private fun sanitize(s: String): String = s.replace("\n", "").replace("\r", "").replace("\u0000", "") diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLibrary.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLibrary.kt index f9c0c04bd..f84fa1dec 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicLibrary.kt @@ -10,12 +10,6 @@ import org.json.JSONArray import org.json.JSONObject import java.util.concurrent.atomic.AtomicBoolean -/** - * The signed-in account's Epic library: the library service's records, each enriched from the - * catalog (title, art, DLC flag), cached so the tab paints at once and re-synced when forced or - * 15 minutes stale. DLC entries are folded away under their base game. Ported from Bannerlator's - * EpicLibraryRepo. - */ object EpicLibrary { private const val TAG = "EpicLibrary" private const val CACHE_KEY = "library_cache" @@ -53,7 +47,6 @@ object EpicLibrary { }.getOrDefault(emptyList()) } - /** Remembers a game's install size once the manifest said it, for the Install button. */ fun rememberSize(context: Context, appName: String, bytes: Long) { if (bytes <= 0) return val p = EpicPrefs.get(context) @@ -115,7 +108,6 @@ object EpicLibrary { imageUrl = g.artSquare.ifBlank { null } ?: g.artCover.ifBlank { null }, tallImageUrl = g.artCover.ifBlank { null } ?: g.artSquare.ifBlank { null }, developer = g.developer, publisher = g.publisher, description = com.droiddeck.launcher.stores.cleanStoreText(g.description), owned = true, - // A cache from before the release date was kept still has it under the older per-game key. releaseDate = g.releaseDate.ifEmpty { EpicPrefs.get(context).getString("release_${g.appName}", "").orEmpty() }, sizeBytes = EpicPrefs.get(context).getLong("size_${g.appName}", g.installSize), storeUrl = "https://store.epicgames.com/en-US/browse?q=${java.net.URLEncoder.encode(g.title, "UTF-8")}", diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt index 59849fd60..28510a8f1 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicNative.kt @@ -1,4 +1,3 @@ -// JNI symbols depend on this package path and class name (app/src/main/rust/stores/JNI.md). package com.droiddeck.launcher.stores.epic import android.util.Log @@ -8,35 +7,16 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference -/** - * The native half of an Epic install in `libdroiddeckstores.so`: [run] fills the chunk cache with - * verified, decompressed chunks for the files the manager says are pending, [assemble] writes those - * files from the cache. Everything around them (manifest, install tags, delta pass, post-install) - * stays in the manager. - * - * The cache is `<installDir>/.chunks` when `chunkCacheDir` is `""`, else the scratch folder given - - * the app's own cache when the game installs to a card, where the assembler drops each chunk right - * after its last use and the folder at the end. Both calls take the same value for one install. - * - * Both block like the Java loops they replace and poll [AtomicBoolean] `cancel` every 250 ms: on - * cancel they flip the native flag, wait up to 5 s for the run to wind down, then return. - */ object EpicNative { private const val TAG = "EpicNative" - /** Callbacks from a native run; every method is called on a native thread. One interface serves both runs. */ interface Listener { - /** Fetch only, once before any fetch: the plan the engine derived (the manager cross-checks it). */ fun onPlan(chunksTotal: Int, bytesTotal: Long, chunkDir: String) - /** Fetch: per accounted chunk (cached-skip or fetched), done/total = chunks. Assembly: per file written, done/total = files. */ fun onProgress(bytesDone: Long, bytesTotal: Long, done: Int, total: Int) - /** Engine log line (already in logcat under `EpicNative`). */ fun onLog(line: String) - /** Terminal. Fetch: bytes credited; assembly: bytes written. */ fun onComplete(success: Boolean, error: String, bytes: Long) } - /** Outcome of a run. `started == false`: nothing happened on disk, the caller runs its own loop. */ class Result( @JvmField val started: Boolean, @JvmField val success: Boolean, @JvmField val cancelled: Boolean, @JvmField val error: String, @JvmField val bytes: Long, @JvmField val done: Int, @JvmField val total: Int, @@ -44,7 +24,6 @@ object EpicNative { private class Completion(val success: Boolean, val error: String, val bytes: Long) - /** The fetch: chunks for [pendingFileIdx] into the cache. */ @JvmStatic fun run( manifest: ByteArray, installDir: String, chunkCacheDir: String, cdnPrefixes: Array<String>, pendingFileIdx: IntArray, expectedChunks: Int, expectedBytes: Long, @@ -53,7 +32,6 @@ object EpicNative { nativeStart(manifest, installDir, chunkCacheDir, cdnPrefixes, pendingFileIdx, expectedChunks, expectedBytes, caBundlePath, maxWorkers, processWorkers, inner) } - /** The assembly: the files [fileIdx] written from the cache, the cache removed on success. */ @JvmStatic fun assemble(manifest: ByteArray, installDir: String, chunkCacheDir: String, fileIdx: IntArray, cancel: AtomicBoolean?, listener: Listener): Result = drive("nativeAssemble", cancel, listener) { inner -> nativeAssemble(manifest, installDir, chunkCacheDir, fileIdx, inner) } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicPrefs.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicPrefs.kt index ae2bfadb3..1510dbef9 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicPrefs.kt @@ -3,11 +3,6 @@ package com.droiddeck.launcher.stores.epic import android.content.Context import android.content.SharedPreferences -/** - * Epic's own records that are not credentials: the library cache, each game's release date, the - * cached deployment id and extra command line a launch needs. App-private preferences; the sign-in - * lives in StoreAccounts. Public: the Java clients read it too. - */ object EpicPrefs { @JvmStatic fun get(context: Context): SharedPreferences = context.applicationContext.getSharedPreferences("stores.epic", Context.MODE_PRIVATE) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicStoreCatalog.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicStoreCatalog.kt index 4e07c78aa..b7b6ac364 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicStoreCatalog.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicStoreCatalog.kt @@ -14,14 +14,6 @@ import java.util.Locale import java.util.TimeZone import java.util.concurrent.ConcurrentHashMap -/** - * The Epic Games Store catalog through the GraphQL endpoint the store website uses - * (`store.epicgames.com/graphql`, which wants a browser User-Agent): `Catalog.searchStore` with - * the base-game category, sorted by price / release date / relevance, on sale or free. Elements - * carry the offer id, the namespace, the catalog item ids (what the library records, so ownership - * is matched on those), key images and pre-formatted prices. "Free this week" comes from the - * static `freeGamesPromotions` feed. - */ object EpicStoreCatalog { private const val TAG = "EpicStore" private const val GRAPHQL = "https://store.epicgames.com/graphql" @@ -82,7 +74,6 @@ object EpicStoreCatalog { val img = imgs.optJSONObject(k) ?: continue val url = img.optString("url", "") if (url.isBlank()) continue - // The opaque box images first; the logo variants are transparent and never used. when (img.optString("type", "")) { "DieselGameBox" -> wide = url "OfferImageWide", "DieselStoreFrontWide" -> if (wide == null) wide = url @@ -105,8 +96,6 @@ object EpicStoreCatalog { val tagLine = buildList { e.optJSONArray("tags")?.let { tags -> for (t in 0 until tags.length()) { val n = tags.optJSONObject(t)?.optString("name").orEmpty(); if (n.isNotBlank() && n != "Windows" && size < 3) add(n) } } }.joinToString(", ") - // Epic's public store GraphQL has no age rating on an offer (no ageGatings/ratings field on - // CatalogOffer or StoreConfig); only a content tag, if Epic sets one, can mark it. val allTags = buildList { e.optJSONArray("tags")?.let { t -> for (k in 0 until t.length()) t.optJSONObject(k)?.optString("name")?.let { add(it) } } } val itemIds = buildList { e.optJSONArray("items")?.let { items -> for (j in 0 until items.length()) items.optJSONObject(j)?.optString("id")?.takeIf { it.isNotBlank() }?.let { add(it) } } @@ -124,7 +113,6 @@ object EpicStoreCatalog { ) } - // An offer's custom attribute (developerName, publisherName), or "". private fun attribute(e: JSONObject, key: String): String { val attrs = e.optJSONArray("customAttributes") ?: return "" return (0 until attrs.length()).mapNotNull { attrs.optJSONObject(it) }.firstOrNull { it.optString("key") == key } @@ -140,11 +128,9 @@ object EpicStoreCatalog { return slug.removeSuffix("/home") } - /** The Store tab's shelves: this week's giveaways as Free, sales as Deals, newest releases, trending = free-to-play picks. Blocking. */ fun featured(context: Context, force: Boolean = false): StoreShelves? { val now = System.currentTimeMillis() featuredCache?.let { if (!force && now - it.at < FEATURED_TTL_MS) return it.value } - // Shelves saved on an earlier run are as good while they are young: shown, not fetched again. if (!force && now - EpicPrefs.get(context).getLong("store_featured_at", 0L) < FEATURED_TTL_MS) cachedFeatured(context)?.let { return it } val freeNow = fetchPromos() val onSale = searchStore(24, sortBy = "currentPrice", sortDir = "ASC", onSale = true).filter { it.discountPercent > 0 } @@ -174,7 +160,6 @@ object EpicStoreCatalog { return results } - /** This week's giveaway titles (100% off right now). */ private fun fetchPromos(): List<CatalogItem> { val body = StoreNet.get("$PROMOS?locale=en-US&country=${country()}&allowCountries=${country()}") ?: return emptyList() return runCatching { diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAchievements.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAchievements.kt index 8eb3d0688..cc3bd077a 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAchievements.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAchievements.kt @@ -11,7 +11,6 @@ import java.net.HttpURLConnection import java.net.URL import java.util.zip.InflaterInputStream -/** gameplay.gog.com, with a token scoped to the game's own client. */ object GogAchievements : StoreAchievements(Store.GOG.id, Store.GOG.shortLabel) { private const val TAG = "GogAchievements" @@ -44,7 +43,6 @@ object GogAchievements : StoreAchievements(Store.GOG.id, Store.GOG.shortLabel) { return Result.Found(out) } - /** The game's client id and secret: saved at install, else from the newest build's manifest. */ private fun client(context: Context, productId: String, token: String): Pair<String, String>? { val prefs = GogPrefs.get(context) val savedId = prefs.getString("client_id_$productId", null)?.ifEmpty { null } @@ -76,7 +74,6 @@ object GogAchievements : StoreAchievements(Store.GOG.id, Store.GOG.shortLabel) { } } - /** Gen2 manifests are zlib-compressed; plain JSON passes through. */ private fun inflate(data: ByteArray): String = if (data.isNotEmpty() && data[0] == '{'.code.toByte()) String(data, Charsets.UTF_8) else InflaterInputStream(data.inputStream()).use { String(it.readBytes(), Charsets.UTF_8) } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAuth.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAuth.kt index 43191105e..2de0880bd 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAuth.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogAuth.kt @@ -10,19 +10,12 @@ import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL -/** - * The GOG sign-in: the token the OAuth page hands back, kept in StoreAccounts' private file, - * refreshed through auth.gog.com when it has expired (the client id and secret are GOG Galaxy's - * own, public ones, as every third-party GOG client uses). Nothing here is ever logged: the token - * endpoint's URL itself carries the secret and the refresh token. - */ object GogAuth { private const val TAG = "GogAuth" private const val CLIENT_ID = "46899977096215655" private const val CLIENT_SECRET = "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9" private const val TOKEN_URL = "https://auth.gog.com/token?client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&grant_type=refresh_token&refresh_token=" - /** The login page, as GOG Galaxy opens it: an implicit grant that returns the token in the redirect's fragment. */ const val AUTH_URL = "https://auth.gog.com/auth?client_id=$CLIENT_ID" + "&redirect_uri=https%3A%2F%2Fembed.gog.com%2Fon_login_success%3Forigin%3Dclient&response_type=token&layout=client2" const val REDIRECT_PREFIX = "https://embed.gog.com/on_login_success" @@ -32,7 +25,6 @@ object GogAuth { fun isSignedIn(context: Context): Boolean = StoreAccounts.read(context, Store.GOG)?.optString("access_token", "")?.isNotEmpty() == true - /** Saves a fresh sign-in and looks the account's name up for the chip. Blocking. */ fun saveLogin(context: Context, accessToken: String, refreshToken: String?, userId: String?) = GogComet.replaceAccount { saveAccount(context, accessToken, refreshToken, userId) } @@ -52,10 +44,8 @@ object GogAuth { Log.i(TAG, "signed in") } - /** The raw stored token, expired or not; null when signed out. */ fun storedToken(context: Context): String? = StoreAccounts.read(context, Store.GOG)?.optString("access_token", "")?.ifEmpty { null } - /** A usable access token, refreshed when the recorded login time says it has expired; null when signed out or the refresh failed. */ @JvmStatic @Synchronized fun validToken(context: Context): String? { @@ -67,7 +57,6 @@ object GogAuth { return if (loginTime == 0L || now >= loginTime + expiresIn - 60) refresh(context) else token } - /** Refreshes the access token; the new one, or null on any failure. Blocking. */ @JvmStatic @Synchronized fun refresh(context: Context): String? { @@ -86,7 +75,6 @@ object GogAuth { StoreAccounts.write(context, Store.GOG, json) access } catch (e: Exception) { - // Not the throwable: HttpURLConnection's messages echo the URL, which embeds the secrets. Log.w(TAG, "refresh failed: ${e.javaClass.simpleName}") null } finally { @@ -94,7 +82,6 @@ object GogAuth { } } - /** A complete companion snapshot. Never exported through the guest request channel. */ @Synchronized fun cometCredentials(context: Context): JSONObject? { val access = validToken(context) ?: return null @@ -117,7 +104,6 @@ object GogAuth { class GameToken(val accessToken: String, val userId: String) - /** The Galaxy refresh token exchanged for one scoped to a game's own client. Blocking. */ fun gameToken(context: Context, clientId: String, clientSecret: String): GameToken? { val json = StoreAccounts.read(context, Store.GOG) ?: return null val refresh = json.optString("refresh_token", "").ifEmpty { return null } @@ -139,7 +125,6 @@ object GogAuth { } } - /** Hosts the GOG login page hands off to for a social sign-in; they refuse an embedded browser's identity. */ fun isIdentityProviderHost(host: String?): Boolean { val h = host?.lowercase() ?: return false return h == "google.com" || h.endsWith(".google.com") || h == "youtube.com" || h.endsWith(".youtube.com") || diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogBackend.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogBackend.kt index f167265e8..2661a99f6 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogBackend.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogBackend.kt @@ -17,7 +17,6 @@ import com.droiddeck.launcher.stores.download.DownloadStage import java.io.File import java.util.concurrent.atomic.AtomicBoolean -/** GOG in the Stores section: sign-in, the library, the public shelves, installs. */ object GogBackend : StoreBackend { override val store: Store = Store.GOG @@ -32,8 +31,6 @@ object GogBackend : StoreBackend { override fun syncLibrary(context: Context, force: Boolean) { val app = context.applicationContext Thread({ - // The cache first, so the tab has something while the sync runs; read here, not on the - // main thread. With a cache on screen a background sync keeps quiet: only Refresh shows its bar. val cached = GogLibrary.cached(app).map { GogLibrary.toCatalogItem(app, it) } val quiet = cached.isNotEmpty() && !force StoresState.post { @@ -62,7 +59,6 @@ object GogBackend : StoreBackend { }, "gog-shelves").start() } - /** The shelves with the library's titles marked owned, so their cards offer Install. */ private fun markOwned(s: com.droiddeck.launcher.stores.StoreShelves): com.droiddeck.launcher.stores.StoreShelves { val owned = StoresState.library[store]?.associateBy { it.id } ?: return s fun mark(l: List<CatalogItem>) = l.map { i -> owned[i.id]?.let { o -> i.copy(owned = true, sizeBytes = o.sizeBytes) } ?: i } @@ -79,7 +75,6 @@ object GogBackend : StoreBackend { override fun uninstall(context: Context, game: InstalledStoreGame) {} - /** One GOG install, as the queue runs it. */ private class InstallJob(val app: Context, val game: GogGame, val item: CatalogItem, val folder: File) : DownloadQueue.DownloadJob { private val cancelled = AtomicBoolean(false) @@ -111,7 +106,6 @@ object GogBackend : StoreBackend { Store.GOG, game.gameId, game.title, exe = result.exeRelative.ifEmpty { "" }, installVersion = result.buildId, installedAt = System.currentTimeMillis(), cover = item.tallImageUrl ?: item.imageUrl, hero = item.imageUrl, - // Kept even when empty: the build was read, and lists none. extra = mapOf(com.droiddeck.launcher.frontend.WinCompSources.GOG_DEPENDENCIES to result.dependencies.joinToString(",")) + (if (result.dependenciesRead) mapOf(com.droiddeck.launcher.frontend.WinCompSources.LIST_READ to "1") else emptyMap()), ) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogComet.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogComet.kt index a514b8db3..d6a4c3a60 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogComet.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogComet.kt @@ -22,7 +22,6 @@ import java.io.File import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -/** Owns one Galaxy companion; only its structured, secret-free events reach Android logs. */ object GogComet { private const val TAG = "GogComet" private const val BIN = "/usr/local/lib/droiddeck-comet/comet" @@ -52,7 +51,6 @@ object GogComet { AchievementNotifications.begin(generation) } - /** Account replacement and launch preparation use the same lock, preventing stale restarts. */ fun replaceAccount(change: () -> Unit) = synchronized(lock) { stopLocked() AchievementNotifications.begin(generation) @@ -95,7 +93,6 @@ object GogComet { return file } - /** No arbitrary paths or credentials from the guest are accepted. */ fun prepare(context: Context, id: String): JSONObject = synchronized(lock) { fun reply(ready: Boolean, reason: String): JSONObject { report(reason) @@ -134,7 +131,6 @@ object GogComet { process = child; account = user; session = SessionState.sessionId Thread({ runCatching { child.inputStream.bufferedReader().useLines { lines -> lines.forEach { line -> - // Raw upstream errors/payloads can contain tokens. Never forward them. when { line.startsWith("DROIDDECK_COMET_READY:") -> { val childPid = line.substringAfter(':').toIntOrNull() ?: -1 @@ -177,7 +173,6 @@ object GogComet { private fun stopLocked() { val child = process ?: return - // Signal the announced daemon, not proot; start time prevents signalling a reused PID. if (started != null && identity(pid) == started) android.os.Process.sendSignal(pid, 2) if (!child.waitFor(5, TimeUnit.SECONDS)) { if (started != null && identity(pid) == started) android.os.Process.sendSignal(pid, 15) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogDownloadManager.java b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogDownloadManager.java index a8bd89a80..0bbbaa238 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogDownloadManager.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogDownloadManager.java @@ -36,68 +36,36 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.zip.GZIPInputStream; import java.util.zip.Inflater; -/** - * The GOG install pipeline, ported from Bannerlator's GogDownloadManager and made blocking: the - * download queue runs {@link #install} on its own thread and reads progress through {@link Callback}. - * - * Gen 2: builds → build manifest → depot manifests (base product, matching language) → secure - * link → per file: chunks fetched, inflated, MD5-checked, assembled (the native engine when it is - * there, else the Java pool here). Gen 1: build manifest → per-file Range GET. Neither available: - * the product's Windows installer is downloaded as a file to run once. - * - * Resume and repair come from the per-file size + MD5 check: a complete file is skipped, a damaged - * one re-fetched. Nothing here writes outside the install folder; URLs are only logged redacted. - */ public final class GogDownloadManager { private static final String TAG = "GogDownload"; private static final int TIMEOUT = 30_000; private static final String MARKER = "_gog_manifest.json"; - /** Progress out of a running install. Every method is called on the install's thread. */ public interface Callback { - /** A new phase or file, as text, with the install's overall percentage. */ void onProgress(String message, int pct); - /** Bytes landed against the planned total, and the current speed (0 = unknown). */ default void onBytes(long done, long total, long speedBps) {} - /** A diagnostic line for the engine log. */ default void onLog(String line) {} - /** What this run fetches and what the game takes on disk. */ default void onSizes(long downloadBytes, long diskBytes) {} - /** - * The active stage's own count: {@code stage} is "verify" or "install", {@code done} of - * {@code total} an amount (bytes, or items when there are no bytes), {@code items} of - * {@code itemsTotal} the files, when it counts files. - */ default void onStage(String stage, long done, long total, int items, int itemsTotal) {} } - /** What an install leaves behind for the sidecar. */ public static final class Result { - /** The chosen exe, relative to the install folder (forward slashes), or "" when none was found. */ public final String exeRelative; public final String buildId; public final long bytes; - /** The build manifest's "dependencies" (GOG's redistributable ids: MSVC2017_x64, DirectX...), for Windows components. */ public List<String> dependencies = new ArrayList<>(); - /** The dependencies come from a gen2 build manifest read whole (gen1 builds and installers have none). */ public boolean dependenciesRead = false; Result(String exeRelative, String buildId, long bytes) { this.exeRelative = exeRelative; this.buildId = buildId; this.bytes = bytes; } } - /** Thrown for every way an install can fail; the message is what the Downloads page shows. */ public static final class InstallException extends Exception { InstallException(String message) { super(message); } } private GogDownloadManager() {} - /** - * Installs {@code game} into {@code installPath}. Blocks until done; {@code cancelled} set from - * another thread stops it at the next file boundary (what was written stays for a resume). - * Returns null when cancelled. - */ public static Result install(Context ctx, GogGame game, File installPath, Callback cb, AtomicBoolean cancelled) throws InstallException { cb.onProgress("Checking sign-in…", 0); String token = GogAuth.validToken(ctx); @@ -127,7 +95,6 @@ public final class GogDownloadManager { return runInstaller(ctx, game, token, installPath, cb, cancelled); } - // ---- gen2 ----------------------------------------------------------------------------------- private static final class Gen2Outcome { Result result; String error; boolean hardStop; @@ -148,14 +115,10 @@ public final class GogDownloadManager { tempExe = products.getJSONObject(0).optString("temp_executable", null); if (tempExe != null && tempExe.isEmpty()) tempExe = null; } - // The build manifest lists every product's depots (base + DLC), each tagged by productId; - // the base install takes the base product's only, under a secure link scoped to it. String baseProductId = manifest.optString("baseProductId", null); if ((baseProductId == null || baseProductId.isEmpty()) && products != null && products.length() > 0) baseProductId = products.getJSONObject(0).optString("productId", null); if (baseProductId == null || baseProductId.isEmpty()) baseProductId = game.gameId; - // The build's required redistributables (MSVC, DirectX, ...): logged, and kept in the - // sidecar, where Windows components take them as this game's list (WinCompSources). JSONArray deps = manifest.optJSONArray("dependencies"); List<String> dependencies = new ArrayList<>(); if (deps != null) for (int i = 0; i < deps.length(); i++) { String d = deps.optString(i, ""); if (!d.isEmpty()) dependencies.add(d); } @@ -197,7 +160,6 @@ public final class GogDownloadManager { return out; } } - // Largest first: the biggest archives start first and small files fill the tail. Collections.sort(files, (a, b) -> Long.compare(b.totalSize, a.totalSize)); final int total = files.size(); @@ -264,7 +226,6 @@ public final class GogDownloadManager { deleteCounted(new File(installPath, ".gog_chunks"), cb); String clientId = manifest.optString("clientId", null); if (clientId != null && !clientId.isEmpty()) GogPrefs.get(ctx).edit().putString("client_id_" + game.gameId, clientId).apply(); - // The game's own client secret: cloud saves need a token issued to the game's client. String clientSecret = manifest.optString("clientSecret", null); if (clientSecret != null && !clientSecret.isEmpty()) GogPrefs.get(ctx).edit().putString("client_secret_" + game.gameId, clientSecret).apply(); out.result = new Result(pickExe(installPath, tempExe, game.title), gm.buildId, planned); @@ -282,7 +243,6 @@ public final class GogDownloadManager { Gen2Manifest(JSONObject manifest, String buildId) { this.manifest = manifest; this.buildId = buildId; } } - /** The first Windows build's manifest, inflated and parsed; null on any failure. */ private static Gen2Manifest resolveGen2Manifest(String buildsJson, String token, Callback cb) { try { JSONArray items = new JSONObject(buildsJson).optJSONArray("items"); @@ -309,7 +269,6 @@ public final class GogDownloadManager { } } - /** English or every-language depots; GOG tags the rest by language code. */ private static boolean languageCompatible(JSONObject depot) { JSONArray languages = depot.optJSONArray("languages"); if (languages == null || languages.length() == 0) return true; @@ -317,7 +276,6 @@ public final class GogDownloadManager { return s.contains("*") || s.contains("en-US") || s.contains("\"en\"") || s.contains("english"); } - // ---- gen1 ----------------------------------------------------------------------------------- private static final class Gen1Outcome { Result result; String error; } @@ -420,9 +378,7 @@ public final class GogDownloadManager { } } - // ---- installer fallback --------------------------------------------------------------------- - /** Games with no content-system builds: the Windows installer, as one file, to run once in Proton. */ private static Result runInstaller(Context ctx, GogGame game, String token, File installPath, Callback cb, AtomicBoolean cancelled) throws InstallException { try { String productJson = httpGet("https://api.gog.com/products/" + game.gameId + "?expand=downloads", token); @@ -526,13 +482,7 @@ public final class GogDownloadManager { } } - // ---- native engine -------------------------------------------------------------------------- - /** - * Drives the native engine for one fetch loop, producing what the Java pool produces: the same - * counters, progress strings and percentages. A secure-link expiry (the run dying on - * 401/403/404/500) refreshes the link and re-runs for the files not yet done. - */ private static void runNativeEngine(Context ctx, int kind, String verifiedMsg, List<String> depotJsons, File installPath, AtomicReference<String> cdnBaseRef, String secureLinkUrl, String token, AtomicInteger cdnRefreshCount, int maxCdnRefresh, int maxWorkers, int processWorkers, @@ -542,7 +492,6 @@ public final class GogDownloadManager { StoreDownloadTier tier = StoreDownloadTier.Companion.current(ctx); int workers = Math.max(1, Math.min(128, tier.getNetworkWindow())); int process = Math.max(16, Math.max(processWorkers, tier.getProcessWorkers())); - // The tier's window goes to the engine as its ceiling (Max = 96); the per-host spread is the engine's. cb.onLog("gog: plan kind=" + kind + " files=" + total + " bytes=" + planned + " window=" + workers + " process_workers=" + process + " tier=" + tier.getId() + " engine=native"); final String[] manifests = depotJsons.toArray(new String[0]); final java.util.Set<String> donePaths = java.util.concurrent.ConcurrentHashMap.newKeySet(); @@ -554,9 +503,6 @@ public final class GogDownloadManager { final AtomicBoolean okRef = new AtomicBoolean(false); final AtomicBoolean linkExpiryRef = new AtomicBoolean(false); final AtomicReference<String> errRef = new AtomicReference<>(""); - // The bar between file completions: what earlier runs counted, plus the files this run - // skipped as already verified (the engine credits those no bytes), plus the engine's own - // byte count, which includes the files still in flight. Never shown going back. final long runStart = totalBytes.get(); final AtomicLong verifiedInRun = new AtomicLong(0); final AtomicLong shownBytes = new AtomicLong(runStart); @@ -583,7 +529,6 @@ public final class GogDownloadManager { okRef.set(success); linkExpiryRef.set(linkExpiry); errRef.set(error == null ? "" : error); latch.countDown(); } }; - // Every CDN of the set: the engine spreads its window across the hosts (JNI.md). String[] bases = (base == null || base.isEmpty()) ? new String[0] : base.split("\n"); if (run == 1) cb.onLog("gog: " + bases.length + " CDN host(s) offered"); long handle = GogNative.start(kind, manifests, bases, installPath.getAbsolutePath(), donePaths.toArray(new String[0]), "", @@ -611,7 +556,6 @@ public final class GogDownloadManager { } } - // ---- gen2 helpers (shared with the Java pool) ----------------------------------------------- private static void parseDepotManifest(String json, List<DepotFile> out) { try { @@ -675,17 +619,11 @@ public final class GogDownloadManager { return tmpFile.renameTo(outFile); } - /** - * Every CDN the secure link offers, as one value - their bases, one per line - so the set is - * refreshed (and compared on expiry) as a whole. GOG answers with several hosts in {@code urls[]}; - * taking only the first left every request on one host. - */ private static String parseCdnUrl(String json) { java.util.List<String> all = parseCdnUrls(json); return all.isEmpty() ? null : String.join("\n", all); } - /** The CDN bases of a secure_link answer, in the order GOG gives them. */ static java.util.List<String> parseCdnUrls(String json) { java.util.List<String> out = new ArrayList<>(); if (json == null) return out; @@ -712,13 +650,11 @@ public final class GogDownloadManager { private static final AtomicInteger CDN_TURN = new AtomicInteger(); - /** One of the set's CDNs, in turn, so the Java pool spreads its requests over all of them. */ static String pickCdn(String set) { String[] all = set.split("\n"); return all[Math.floorMod(CDN_TURN.getAndIncrement(), all.length)]; } - /** The first CDN of the set - what a caller that takes a single base gets. */ static String firstCdn(String set) { int nl = set.indexOf('\n'); return nl < 0 ? set : set.substring(0, nl); @@ -768,7 +704,6 @@ public final class GogDownloadManager { static byte[] fetchBytes(String url, String token) { return fetchBytesEx(url, token).body; } - /** gzip or zlib detected by magic, else the bytes as UTF-8. */ static String decompressBytes(byte[] data) { if (data == null || data.length < 2) return null; try { @@ -840,7 +775,6 @@ public final class GogDownloadManager { } } - /** The chunk path goes before the query string so the secure link's token survives. */ private static String buildChunkUrl(String base, String chunkPath) { int q = base.indexOf('?'); return q >= 0 ? base.substring(0, q) + "/" + chunkPath + base.substring(q) : base + "/" + chunkPath; @@ -946,7 +880,6 @@ public final class GogDownloadManager { private static String baseName(String path) { return path.contains("/") ? path.substring(path.lastIndexOf('/') + 1) : path; } - /** Download-pool threads for the built-in loop: cores times two, between 6 and 16. */ private static int downloadThreads() { int threads = Runtime.getRuntime().availableProcessors() * 2; return Math.max(6, Math.min(16, threads)); @@ -956,7 +889,6 @@ public final class GogDownloadManager { try (FileOutputStream fos = new FileOutputStream(f)) { fos.write(data); } } - /** Removes the chunk cache, counting its files as the Install stage's progress (a large cache takes a while). */ private static void deleteCounted(File dir, Callback cb) { File[] files = dir.listFiles(); if (files == null) { dir.delete(); return; } @@ -976,10 +908,6 @@ public final class GogDownloadManager { dir.delete(); } - /** - * The exe to run, relative to the install folder: the manifest's hint when present, else the - * candidate named most like the title, else the shallowest. "" when the folder has none. - */ static String pickExe(File installPath, String tempExe, String title) { if (tempExe != null) { File hinted = new File(installPath, tempExe); @@ -1038,12 +966,6 @@ public final class GogDownloadManager { Gen1File(String path, String url, long offset, long size) { this.path = path; this.url = url; this.offset = offset; this.size = size; } } - /** The install size of the Windows build (English / all-language depots), or -1. Blocking. */ - /** - * The "dependencies" of the build [buildId] of [gameId] (else the newest Windows build), from - * its build manifest alone: no game file is fetched. Null when the builds or the manifest - * could not be had; empty when the build lists none. Blocking. - */ public static List<String> fetchDependencies(String gameId, String buildId, String token) { try { String buildsUrl = "https://content-system.gog.com/products/" + gameId + "/os/windows/builds?generation=2"; @@ -1067,7 +989,6 @@ public final class GogDownloadManager { String manifestStr = decompressBytes(raw); if (manifestStr == null) return null; JSONObject manifest = new JSONObject(manifestStr); - // A manifest without depots is not one this reads right: no list rather than an empty one. JSONArray depots = manifest.optJSONArray("depots"); if (depots == null) { Log.w(TAG, "gog: build manifest for " + gameId + " has no depots; keys " + manifest.names()); return null; } JSONArray deps = manifest.optJSONArray("dependencies"); @@ -1124,6 +1045,5 @@ public final class GogDownloadManager { return String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024)); } - /** Keeps the import used: every URL a log line names goes through here. */ static String safe(String url) { return StoreLog.redactUrl(url); } } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogGame.java b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogGame.java index 9f74b2ead..fef9876d0 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogGame.java +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogGame.java @@ -1,6 +1,5 @@ package com.droiddeck.launcher.stores.gog; -/** One GOG game as the library cache holds it. */ public class GogGame { public final String gameId; public final String title; @@ -8,11 +7,8 @@ public class GogGame { public final String description; public final String developer; public final String category; - /** Content-system generation of the Windows build: 1 or 2 (0 = unknown). */ public final int generation; - /** gamesdb.gog.com 2:3 box art, or null when the lookup found none. */ public final String verticalCover; - /** ISO date-time of the worldwide release, or "" when GOG gave none. */ public final String releaseDate; public final String publisher; diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogLibrary.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogLibrary.kt index 79d0abf73..61fae1a40 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogLibrary.kt @@ -14,18 +14,10 @@ import java.util.concurrent.Callable import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean -/** - * The signed-in account's GOG library: the owned ids from embed.gog.com, each product's details - * from api.gog.com, and a cache so the tab paints at once. A sync fetches only what the cache - * lacks unless forced or 15 minutes stale. Ported from Bannerlator's GogLibraryRepo. - */ object GogLibrary { private const val TAG = "GogLibrary" - // v2: the card art moved from the store page's backdrop to the Galaxy library art; an older cache is re-fetched. private const val CACHE_KEY = "library_cache_v2" private const val LAST_SYNC_KEY = "library_synced_at" - // Owned products that are not Windows games (DLC, films, soundtracks, other platforms): never in - // the cache, so without this every sync would fetch each of them again. private const val SKIPPED_KEY = "library_skipped" private const val THROTTLE_MS = 6L * 60L * 60L * 1000L @@ -38,7 +30,6 @@ object GogLibrary { object Busy : SyncResult } - /** The cached library, alphabetical; empty until the first sync. */ fun cached(context: Context): List<GogGame> { val json = GogPrefs.get(context).getString(CACHE_KEY, null) ?: return emptyList() return runCatching { @@ -54,7 +45,6 @@ object GogLibrary { fun sizeOf(context: Context, gameId: String): Long = GogPrefs.get(context).getLong("size_$gameId", 0L) - /** Owned ids diffed against the cache; fetches what is new, or everything when forced / empty / stale. Blocking. */ fun sync(context: Context, force: Boolean = false, onStatus: (String) -> Unit = {}): SyncResult { if (!syncing.compareAndSet(false, true)) return SyncResult.Busy try { @@ -69,7 +59,6 @@ object GogLibrary { val owned = JSONObject(gamesJson).optJSONArray("owned") if (owned != null) for (i in 0 until owned.length()) { val id = owned.optLong(i).toString() - // 1801418160 is GOG's own "Galaxy" entry, in every library. if (id != "1801418160" && id != "0") ids.add(id) } }.isSuccess @@ -118,9 +107,7 @@ object GogLibrary { } } - /** One product's details, or null for anything that is not a Windows game (DLC, movies, other platforms). */ private fun fetchGame(prefs: SharedPreferences, id: String, token: String, skipped: MutableSet<String>): GogGame? { - // Not a game this app installs: remembered, so the next sync does not ask again. fun skip(): GogGame? { skipped.add(id); return null } try { val productJson = StoreNet.get("https://api.gog.com/products/$id?expand=downloads,description", bearer = token) ?: return null @@ -131,10 +118,6 @@ object GogLibrary { var title = prod.optJSONObject("title")?.optString("*") if (title.isNullOrEmpty()) title = prod.optString("title") if (title.isNullOrEmpty()) return skip() - // Card art from the v2 game record, the way the Galaxy client draws its library: the dark - // Galaxy background for the wide card and the box art for the tall one. The product's - // `images.background` is the store page's fade-out backdrop (it ends in white on a - // card) and is never used; the logo or icon is the last resort. val images = prod.optJSONObject("images") var imageUrl = "" var boxArt = "" @@ -145,12 +128,9 @@ object GogLibrary { } if (imageUrl.isEmpty()) imageUrl = images?.optString("logo2x", "") ?: "" if (imageUrl.isEmpty()) imageUrl = images?.optString("icon", "") ?: "" - // The account endpoint hands back template keys for some products; the v2 record's text - // is tried next, and when that is a template too the game simply has no description. val desc = cleanStoreText(prod.optJSONObject("description")?.optString("lead", "")) .ifEmpty { cleanStoreText(v2?.optString("overview", "")) } .ifEmpty { cleanStoreText(v2?.optString("description", "")) } - // The products endpoint names no developer or publisher; the v2 record does, with the worldwide release date. val embedded = v2?.optJSONObject("_embedded") fun names(key: String) = embedded?.optJSONArray(key) ?.let { arr -> (0 until arr.length()).mapNotNull { arr.optJSONObject(it)?.optString("name")?.takeIf(String::isNotBlank) } } @@ -218,13 +198,11 @@ object GogLibrary { } } - /** A cached game as the page's card. */ fun toCatalogItem(context: Context, g: GogGame): CatalogItem = CatalogItem( store = Store.GOG, id = g.gameId, title = g.title, imageUrl = GogStoreCatalog.absolutize(g.imageUrl).ifBlank { null }, tallImageUrl = GogStoreCatalog.absolutize(g.verticalCover ?: g.imageUrl).ifBlank { null }, tags = g.category, developer = g.developer, publisher = g.publisher, description = g.description, owned = true, - // A cache from before the release date was kept still has it under the older per-game key. releaseDate = g.releaseDate.ifEmpty { GogPrefs.get(context).getString("release_${g.gameId}", "").orEmpty() }, sizeBytes = sizeOf(context, g.gameId), storeUrl = "https://www.gog.com/en/game/${g.gameId}", diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogNative.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogNative.kt index 0922eda9a..f8ab86b14 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogNative.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogNative.kt @@ -1,52 +1,27 @@ -// JNI symbols depend on this package path and class name (app/src/main/rust/stores/JNI.md). package com.droiddeck.launcher.stores.gog import android.util.Log import com.droiddeck.launcher.stores.StoresNative -/** - * The native GOG download engine in `libdroiddeckstores.so`: the gen2 chunk loop (depot manifests - * → chunk fetch + inflate + MD5 → assembled files) and the gen1 range loop. One [start] is one - * download loop; the manager owns everything before and after it. Method names and signatures - * are Bannerlator's `BlGogDownload`, only the package and class changed. - */ object GogNative { private const val TAG = "GogNative" - /** gen2: depot manifests → chunk fetch + inflate + MD5 (base install, DLC, dependencies). */ const val KIND_GEN2_CHUNKS = 0 - /** gen1: build manifest → per-file HTTP Range GET streamed to disk. */ const val KIND_GEN1_RANGES = 1 - /** Callbacks from the engine; every method runs on a native thread. */ interface Listener { - /** - * One file reached its final state. [verified] = resume-skip (existing file passed size+MD5; - * no bytes credited); otherwise a freshly assembled, verified and renamed file ([fileBytes] - * = its decompressed size). [filesDone] counts both; [bytesDone] counts assembled bytes only. - */ fun onProgress(bytesDone: Long, bytesTotal: Long, filesDone: Int, filesTotal: Int, file: String, fileBytes: Long, verified: Boolean) - /** - * Byte progress between file completions, every 250 ms while fetching (and once at the end): - * assembled bytes plus what the files in flight have so far. Drives the bar and the speed. - */ fun onBytes(bytesDone: Long, bytesTotal: Long) - /** Engine diagnostics (already in logcat under `GogNative`). */ fun onLog(line: String) - /** - * Exactly once per [start] that returned a non-zero handle. [linkExpiry] = the run died on an - * HTTP 401/403/404/500, the codes an expired secure link returns: refresh it and run again. - */ fun onComplete(success: Boolean, cancelled: Boolean, linkExpiry: Boolean, error: String, bytesWritten: Long, filesDone: Int) } @Volatile private var available: Boolean? = null - /** True when the library loads and the GOG exports bind. Cached after the first call. */ @JvmStatic fun isAvailable(): Boolean { available?.let { return it } @@ -60,10 +35,6 @@ object GogNative { return ok } - /** - * Starts one download loop on a native thread; returns its handle (0 = not started, no - * callbacks). See JNI.md for the parameters; they are what the manager already holds. - */ @JvmStatic fun start( kind: Int, depotManifests: Array<String>, cdnBases: Array<String>, installDir: String, skipPaths: Array<String>, @@ -78,14 +49,12 @@ object GogNative { } } - /** Requests cancellation; `onComplete(cancelled = true)` follows. Idempotent; 0 is a no-op. */ @JvmStatic fun cancel(handle: Long) { if (handle == 0L) return runCatching { nativeCancel(handle) } } - /** Releases the handle, once, after `onComplete`. */ @JvmStatic fun release(handle: Long) { if (handle == 0L) return diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogPrefs.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogPrefs.kt index b5a3af6cb..f67e26cba 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogPrefs.kt @@ -3,12 +3,6 @@ package com.droiddeck.launcher.stores.gog import android.content.Context import android.content.SharedPreferences -/** - * GOG's own records that are not credentials: the library cache, per-game facts the sync learnt - * (generation, release date, size, cover), a build's client id. App-private preferences; the - * sign-in itself lives in StoreAccounts. Public, not internal: the Java download manager reads it - * and Kotlin mangles internal members' names for Java. - */ object GogPrefs { @JvmStatic fun get(context: Context): SharedPreferences = context.applicationContext.getSharedPreferences("stores.gog", Context.MODE_PRIVATE) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogStoreCatalog.kt b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogStoreCatalog.kt index c05f9fdf9..7ec7454cb 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/gog/GogStoreCatalog.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/gog/GogStoreCatalog.kt @@ -14,19 +14,11 @@ import java.util.Currency import java.util.Locale import java.util.concurrent.ConcurrentHashMap -/** - * GOG's public catalog - the same unauthenticated service the gog.com store page uses: - * `catalog.gog.com/v1/catalog?order=desc:trending&productType=in:game,pack&...` with `discounted`, - * `price=between:0,0` and `query=like:` filters. Products carry pre-formatted prices and a - * discount string; Windows-only titles are kept. Cached in-process (shelves 30 min, searches - * 5 min) and the last good shelves mirrored to prefs so the tab paints offline too. - */ object GogStoreCatalog { private const val TAG = "GogStore" private const val CATALOG = "https://catalog.gog.com/v1/catalog" private const val FEATURED_TTL_MS = 3 * 60 * 60 * 1000L private const val SEARCH_TTL_MS = 5 * 60 * 1000L - // v2: entries carry the mature mark; an older cache would show everything until it aged out. private const val KEY_FEATURED = "store_featured_v2" private val supportedCurrencies = setOf( @@ -48,7 +40,6 @@ object GogStoreCatalog { private fun baseQuery(limit: Int, order: String, extra: String = ""): String = "$CATALOG?limit=$limit&order=$order&productType=in:game,pack&page=1&countryCode=${country()}&locale=en-US¤cyCode=${currency()}$extra" - /** GET the catalog; on a refusal of the locale's currency, retry as US / USD. */ private fun fetchProducts(url: String): List<CatalogItem> { var body = StoreNet.get(url) if (body == null && !url.contains("countryCode=US&locale=en-US¤cyCode=USD")) { @@ -57,11 +48,9 @@ object GogStoreCatalog { return if (body == null) emptyList() else parseProducts(body) } - /** The Store tab's shelves. Null only when every feed failed and nothing is cached. Blocking. */ fun featured(context: Context, force: Boolean = false): StoreShelves? { val now = System.currentTimeMillis() featuredCache?.let { if (!force && now - it.at < FEATURED_TTL_MS) return it.value } - // Shelves saved on an earlier run are as good while they are young: shown, not fetched again. if (!force && now - GogPrefs.get(context).getLong("store_featured_at", 0L) < FEATURED_TTL_MS) cachedFeatured(context)?.let { return it } val trending = fetchProducts(baseQuery(24, "desc:trending")) val newest = fetchProducts(baseQuery(40, "desc:releaseDate")).filter { !isFuture(it.releaseDate) }.take(24) @@ -105,7 +94,6 @@ object GogStoreCatalog { val id = p.optString("id", "") val title = p.optString("title", "") if (id.isEmpty() || title.isEmpty()) return null - // Windows only: the app cannot run anything else. val os = p.optJSONArray("operatingSystems") if (os != null && os.length() > 0) { var windows = false @@ -140,11 +128,6 @@ object GogStoreCatalog { ) } - /** - * GOG's own data: `ratings[]` carries each system's age (`esrbRating` 17, `pegiRating` 18, - * `uskRating` 18, `gogRating` 18, ...) and `tags[]` its content tags (`mature`, `nsfw`, - * `sexual-content`, `nudity`). The catalog has no filter for either, so this is client-side. - */ fun isMature(p: JSONObject): Boolean { val ages = ArrayList<Int>() p.optJSONArray("ratings")?.let { r -> for (i in 0 until r.length()) r.optJSONObject(i)?.optString("ageRating")?.toIntOrNull()?.let { ages.add(it) } } @@ -153,14 +136,12 @@ object GogStoreCatalog { return com.droiddeck.launcher.stores.matureByAge(ages) || com.droiddeck.launcher.stores.matureByTags(tags) } - /** GOG returns protocol-relative `//images…` URLs in a few places. */ @JvmStatic fun absolutize(url: String?): String { if (url.isNullOrBlank()) return "" return if (url.startsWith("//")) "https:$url" else url } - /** "YYYY.MM.DD" later than today: a pre-order, kept out of What's new. */ private fun isFuture(date: String): Boolean { if (date.length < 10) return false val parts = date.substring(0, 10).split('.', '-') diff --git a/app/src/main/java/com/droiddeck/launcher/ui/AddAppDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/AddAppDialog.kt index 31b8f46d6..bc9618c96 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/AddAppDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/AddAppDialog.kt @@ -111,10 +111,8 @@ private enum class AddSource(val label: Int, val hint: Int) { private val ICON_TYPES = listOf("png", "jpg", "jpeg", "webp") private val FieldShape = RoundedCornerShape(8.dp) -/** What an icon choice shows: a picked file, or a suggestion's link. */ private fun iconModel(ref: String): Any = if (ref.startsWith("https://")) ref else File(ref) -/** The Desktop page's Add: a script, an AppImage, a GitHub release or a Flatpak, with a name and icon. */ @Composable internal fun AddAppDialog(runtimeReady: Boolean, onDismiss: () -> Unit, onAdd: (UserApps.Request, String) -> Unit) { val ctx = LocalContext.current @@ -159,7 +157,6 @@ internal fun AddAppDialog(runtimeReady: Boolean, onDismiss: () -> Unit, onAdd: ( val busy = UserAppsState.working val tabFocus = remember { AddSource.entries.map { FocusRequester() } } val inputMode = LocalInputModeManager.current.inputMode - // Once, as it opens: typing a name switches the input mode too, and must keep its field. LaunchedEffect(Unit) { if (inputMode == InputMode.Keyboard) { repeat(2) { androidx.compose.runtime.withFrameNanos { } } @@ -240,10 +237,6 @@ internal fun AddAppDialog(runtimeReady: Boolean, onDismiss: () -> Unit, onAdd: ( } } -/** - * An added app's name and icon, changed on its page. [icon]: null keeps the current one, "" goes - * back to the default, else a picked file or a suggestion. - */ @Composable internal fun EditAppDialog(app: UserApps.App, onDismiss: () -> Unit, onSave: (String, String?, String?) -> Unit) { val ctx = LocalContext.current @@ -299,7 +292,6 @@ internal fun EditAppDialog(app: UserApps.App, onDismiss: () -> Unit, onSave: (St } } -/** Open from the first frame; [onDismiss] once the closing animation has run. */ @Composable internal fun rememberShown(onDismiss: () -> Unit): MutableTransitionState<Boolean> { val shown = remember { MutableTransitionState(false).apply { targetState = true } } @@ -307,25 +299,17 @@ internal fun rememberShown(onDismiss: () -> Unit): MutableTransitionState<Boolea return shown } -/** - * The card both dialogs sit in, sized to fit a landscape handheld without scrolling: it scales in - * like the app's menus, rises above the keyboard, and closes on a tap outside or B. - */ @Composable internal fun AppDialog( shown: MutableTransitionState<Boolean>, close: () -> Unit, label: String, wide: Boolean, modifier: Modifier = Modifier, maxWidth: Dp? = null, - /** [width] fixed, else as wide as its content when [fitContent], else as wide as it may be. */ width: Dp? = null, fitContent: Boolean = false, - /** What B / Back does, when not [close] (a picker goes up a folder first). */ back: (() -> Unit)? = null, content: @Composable ColumnScope.() -> Unit, ) { val pal = LocalPalette.current - // The page's ring stays out of sight behind it. VeilRing() Dialog(onDismissRequest = back ?: close, properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false)) { - // The page's focus memory stays the page's: nothing in here is a place to come back to. CompositionLocalProvider(LocalFrontFocus provides null) { Box( contentAlignment = Alignment.Center, @@ -368,7 +352,6 @@ internal fun AppDialog( } } -/** The eyebrow and title, with [trailing] (the tabs) beside them when there is room, else under them. */ @Composable internal fun DialogHeader(eyebrow: String, title: String, trailing: (@Composable () -> Unit)? = null) { val heading = @Composable { @@ -391,7 +374,6 @@ internal fun DialogHeader(eyebrow: String, title: String, trailing: (@Composable } } -/** Side by side and of one height on a wide card, stacked on a narrow one. */ @Composable private fun Columns(first: @Composable ColumnScope.() -> Unit, second: @Composable ColumnScope.() -> Unit) { Rise(1) { @@ -407,7 +389,6 @@ private fun Columns(first: @Composable ColumnScope.() -> Unit, second: @Composab } } -/** A titled inset of the card; it fills the height its column is given. */ @Composable private fun Panel(title: String, content: @Composable ColumnScope.() -> Unit) { val colors = MaterialTheme.colorScheme @@ -421,7 +402,6 @@ private fun Panel(title: String, content: @Composable ColumnScope.() -> Unit) { } } -/** Why the main button is off (or nothing), then Cancel and the main button, at the card's foot. */ @Composable internal fun DialogFooter(note: String?, confirm: String, enabled: Boolean, onCancel: () -> Unit, onConfirm: () -> Unit) { val lineColor = LocalPalette.current.line @@ -446,7 +426,6 @@ private fun hideKeyboard(view: View) { ?.hideSoftInputFromWindow(view.windowToken, 0) } -/** The app's text field, with what is wrong with its value under it. */ @Composable private fun CheckedField(value: String, onChange: (String) -> Unit, label: String, placeholder: String, problem: String?) { val view = LocalView.current @@ -468,7 +447,6 @@ private fun NameField(name: String, onName: (String) -> Unit, placeholder: Strin ) } -/** The app as the Desktop page will show it: its icon, its name ([name] null: not known yet) and kind. */ @Composable private fun AppPreview(icon: Any?, glyph: ImageVector, name: String?, kind: String) { val colors = MaterialTheme.colorScheme @@ -493,7 +471,6 @@ private fun AppPreview(icon: Any?, glyph: ImageVector, name: String?, kind: Stri } } -/** The icon's picker and [clear] (a label and what it does). */ @Composable private fun IconActions(onChoose: () -> Unit, clear: Pair<Int, () -> Unit>?) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { @@ -504,7 +481,6 @@ private fun IconActions(onChoose: () -> Unit, clear: Pair<Int, () -> Unit>?) { } } -/** Icons found in [repo], looked up off the main thread once the name has settled; [selected] is ringed. */ @Composable private fun IconSuggestions(repo: String, selected: String?, onPick: (String) -> Unit) { val found by produceState<List<String>?>(null, repo) { @@ -537,7 +513,6 @@ private fun SuggestedIcon(url: String, selected: Boolean, onClick: () -> Unit) { .controllerConfirm(onClick = onClick), ) { AsyncImage(model = ImageRequest.Builder(LocalContext.current).data(url).crossfade(true).build(), contentDescription = stringResource(R.string.add_app_icon), contentScale = ContentScale.Fit, modifier = Modifier.size(34.dp)) - // The chosen one is ticked; the ring stays with focus. if (selected) Box( contentAlignment = Alignment.Center, modifier = Modifier.align(Alignment.TopEnd).padding(2.dp).size(14.dp).clip(androidx.compose.foundation.shape.CircleShape).background(pal.signal), @@ -545,7 +520,6 @@ private fun SuggestedIcon(url: String, selected: Boolean, onClick: () -> Unit) { } } -/** The chosen file, or none yet, laid out like the app's text fields, with the button that picks it. */ @Composable private fun FileRow(path: String?, onChoose: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -567,7 +541,6 @@ private fun FileRow(path: String?, onChoose: () -> Unit) { } } -/** Whether the script's folder will be linked or copied in, worked out off the main thread. */ @Composable private fun FolderNote(scriptPath: String) { var plan by remember(scriptPath) { mutableStateOf<UserApps.ScriptPlan?>(null) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt b/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt index 1822ee527..672e73cc9 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt @@ -62,7 +62,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File -/** Each art slot's thumbnail, in its true shape: width, height, corner. */ private fun AddedGameArt.Slot.shape(): Triple<Dp, Dp, Dp> = when (this) { AddedGameArt.Slot.COVER -> Triple(28.dp, 41.dp, 5.dp) AddedGameArt.Slot.BACKGROUND -> Triple(60.dp, 34.dp, 5.dp) @@ -92,7 +91,6 @@ private fun artSourceLabel(source: String): String = stringResource( }, ) -/** A thumbnail of [model] (a file or URL) in [slot]'s shape, scaled by [scale]. */ @Composable private fun SlotThumb(slot: AddedGameArt.Slot, model: Any?, scale: Float = 1f, selected: Boolean = false) { val pal = LocalPalette.current @@ -110,7 +108,6 @@ private fun SlotThumb(slot: AddedGameArt.Slot, model: Any?, scale: Float = 1f, s } } -/** A row of the editor: a label column, then the value. */ @Composable private fun FieldRow(label: String, focus: FocusRequester? = null, onClick: () -> Unit, value: @Composable () -> Unit) { ListRow(focus = focus, onClick = onClick) { @@ -119,20 +116,13 @@ private fun FieldRow(label: String, focus: FocusRequester? = null, onClick: () - } } -/** Where the editor opens: its game's folder, straight on Target, and whether it came from the added-games summary. */ internal class EditRequest(val folder: String, val openTarget: Boolean, val fromSummary: Boolean, val preview: EditPreview) -/** - * What the editor shows on its first frame, from what the page already has (the game's row, or - * the summary's); the rest (the ranked exes, where each piece of art came from) follows. - * [exeName] / [startIn] null = not known yet. - */ internal class EditPreview( val name: String, val exeName: String?, val startIn: String?, val launchOptions: String, val art: Map<AddedGameArt.Slot, File?>, ) { companion object { - /** From the Games tab's row for [game] and the few preferences the edits keep. */ fun of(context: android.content.Context, game: com.droiddeck.launcher.frontend.Library.SteamGame, folder: String): EditPreview { val exe = SessionPrefs.addedGameExe(context, folder).ifEmpty { com.droiddeck.launcher.frontend.AddedExes.list(context).firstOrNull { it.folder == folder && it.picked }?.exe.orEmpty() @@ -144,28 +134,20 @@ internal class EditPreview( ) } - /** From a summary row's game and its cover. */ fun of(game: AddedGames.Game, cover: File?) = EditPreview( game.name, game.exe.name, game.guestDir.substringAfterLast('/'), game.launchOptions, mapOf(AddedGameArt.Slot.COVER to cover), ) } } -/** A small spinner where a value is still being read. */ @Composable private fun Loading() { androidx.compose.material3.CircularProgressIndicator(Modifier.size(14.dp), strokeWidth = 2.dp) } -/** - * An added game's editor, Steam's non-Steam Properties in the app: Name, Target (the ranked exes, - * "?" where nothing clearly won, Other exe…), Start in, Launch options, the four pieces of art, and - * Remove (two presses; the files stay). Every change goes through the game's own Steam shortcut. - */ @Composable internal fun AddedGameEditor( request: EditRequest, onBack: () -> Unit, onClose: () -> Unit, onChanged: () -> Unit, - /** Remove pressed twice: the game's folder, its shortcut appid when known, its name. */ onRemove: (folder: String, appId: Long?, name: String) -> Unit, ) { val ctx = LocalContext.current @@ -260,7 +242,6 @@ internal fun AddedGameEditor( RemoveRow(armed) { if (!armed) armed = true else { armed = false - // At once: the page drops the game and the rest runs off this dialog's life. onRemove(folder, game?.appId, game?.name ?: preview.name) if (request.fromSummary) onBack() close() @@ -304,7 +285,6 @@ internal fun AddedGameEditor( } } -/** Start in as a path on the device: the override mapped back under the game's folder, else the exe's folder. */ private fun hostPath(context: android.content.Context, game: AddedGames.Game): String { val folderGuest = AddedGames.guestPath(context, game.folder) ?: return game.exe.parentFile?.path.orEmpty() return when { @@ -322,7 +302,6 @@ private fun ValueText(text: String, muted: Boolean = false) { ) } -/** "Remove" in red; pressed once it asks, a second press within four seconds removes. */ @Composable private fun RemoveRow(armed: Boolean, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -344,7 +323,6 @@ private fun RemoveRow(armed: Boolean, onClick: () -> Unit) { } } -/** One line of text in a compact dialog: [title] and a red X, the field, OK. */ @Composable internal fun TextEntryDialog(title: String, initial: String, onDismiss: () -> Unit, onSave: (String) -> Unit) { val shown = rememberShown(onDismiss) @@ -365,11 +343,6 @@ internal fun TextEntryDialog(title: String, initial: String, onDismiss: () -> Un } } -/** - * The art chooser for one [slot]: back, the slot's name and a red X; the images grouped by where - * they come from (the game's folder, Steam, SteamGridDB, fetched as it opens), the current one - * outlined, a tap applies it; "Pick image…" and "Reset to automatic". - */ @OptIn(ExperimentalLayoutApi::class) @Composable private fun ArtChooserDialog( @@ -456,12 +429,6 @@ private fun ArtOption(slot: AddedGameArt.Slot, option: AddedGameArt.Option, sele } } -/** - * After "Add all games in this folder": how many were added and how many were there already, then - * one row per added game (cover, name, exe, "?" where unsure); a row opens that game's editor. - * [games] are the added games as the add left them, shown at once; the rows then follow edits and - * removals, and the covers arrive. - */ @Composable internal fun AddedGamesSummaryDialog( games: List<AddedGames.Game>, already: Int, checking: String? = null, @@ -478,8 +445,6 @@ internal fun AddedGamesSummaryDialog( var gone by remember { mutableStateOf(emptySet<String>()) } var armed by remember { mutableStateOf<String?>(null) } LaunchedEffect(armed) { if (armed != null) { kotlinx.coroutines.delay(4_000); armed = null } } - // Rows show as the games arrive; each is refreshed once (current name and exe, a removed one - // dropped) and gets its cover, without redoing the ones already done when more arrive. var resolved by remember { mutableStateOf(emptyMap<String, Pair<AddedGames.Game, File?>?>()) } LaunchedEffect(games) { for (old in games) { @@ -534,7 +499,6 @@ internal fun AddedGamesSummaryDialog( } } -/** A summary row's red X: the first press arms it (filled), a second within 4 s removes the game. */ @Composable private fun RemoveX(armed: Boolean, description: String, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/AppScale.kt b/app/src/main/java/com/droiddeck/launcher/ui/AppScale.kt index 806ef22cd..bbdb62c19 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/AppScale.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/AppScale.kt @@ -10,7 +10,6 @@ internal fun scaledAppDensity(base: Density, percent: Int): Density { return if (scale == 1f) base else Density(base.density * scale, base.fontScale) } -/** Responsive layouts and popup height limits must see the same dp space as the scaled UI. */ internal fun scaledAppConfiguration(base: Configuration, percent: Int): Configuration { val scale = AppUiPrefs.normalizeScale(percent) / 100f if (scale == 1f) return base diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt index 48ac4a28d..32dcaa167 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ComponentsPage.kt @@ -97,28 +97,21 @@ import kotlinx.coroutines.withContext import java.text.DateFormat import java.util.Date -/** The Components page's layer for the GPU drivers, under every Proton's. */ const val GPU_TAB = "gpu" -/** The Components page's layer for the Proton itself, the one the indented layers belong to. */ const val PROTON_TAB = "proton" -/** The stack's layers, top to bottom: LB / RB and the d-pad walk them in this order. */ internal val LAYERS = listOf(PROTON_TAB, "dxvk", "vkd3d", "fex", GPU_TAB) -/** The layer [by] steps from [current] (LB -1, RB +1), wrapping round the stack. */ internal fun layerStep(current: String, by: Int): String { val i = LAYERS.indexOf(current).coerceAtLeast(0) return LAYERS[((i + by) % LAYERS.size + LAYERS.size) % LAYERS.size] } -/** One installed row: a Proton's original bundle (one per Proton build) or a stored package. */ private class InstalledItem( val name: String, val detail: String, val tag: String, val selected: Boolean, val removable: Boolean, - /** Stored package file, or null for an original. */ val file: String?, - /** The Proton build an original belongs to. */ val protonVersion: String?, ) @@ -134,14 +127,6 @@ private val QUEUED = Color(0xFFFFB86B) private val ROW_SHAPE = RoundedCornerShape(10.dp) private val LAYER_SHAPE = RoundedCornerShape(12.dp) -/** - * Everything a game runs on, as one stack: the Proton, its DXVK, VKD3D-Proton and FEX indented - * under it because they belong to it, then the GPU drivers that every Proton shares. One selection - * glides along the stack (LB / RB, or the d-pad in it); beside it a detail frame that never moves - - * what is in use, what is installed and what is available, and what else can be done - only its - * rows trade places. A swap is asked for by stepping out of its row, and the row flies home to its - * layer. On a narrow page the stack is the page, and a layer's detail floods out of its card. - */ @Composable fun ComponentsPage( snapshot: Snapshot?, @@ -167,13 +152,10 @@ fun ComponentsPage( generalFexPreset: String = "", onGeneralFexPreset: (String) -> Unit = {}, requestInitialFocus: Boolean = true, - /** The GPU drivers layer ([GPU_TAB]): what it shows and does. */ gpu: GpuDriversState = GpuDriversState(), gpuActions: GpuDriversActions = GpuDriversActions(), - /** The Proton layer ([PROTON_TAB]): the Protons DroidDeck can install, and installing them. */ protons: ProtonsState = ProtonsState(), protonActions: ProtonsActions = ProtonsActions(), - /** The Proton layer's options for every Proton (sync, FEX preset, SSBS, environment). */ protonOptions: @Composable () -> Unit = {}, ) { val colors = MaterialTheme.colorScheme @@ -391,12 +373,10 @@ fun ComponentsPage( val views = snapshot?.protons ?: emptyList() val view = views.firstOrNull { it.proton.id == protonId } ?: views.firstOrNull() - // Where things sit, root px: each layer card, each row (for a swap's dot), the page itself. val cardsAt = remember { mutableStateMapOf<String, Rect>() } val rowsAt = remember { HashMap<String, Rect>() } var stackAt by remember { mutableStateOf(Offset.Zero) } var pageAt by remember { mutableStateOf(Offset.Zero) } - // A layer card pops and rings as a swap's dot lands on it, and rings as a hop arrives on it. val landed = remember { mutableStateMapOf<String, Int>() } val rings = remember { mutableStateMapOf<String, Int>() } fun land(l: String) { landed[l] = (landed[l] ?: 0) + 1; rings[l] = (rings[l] ?: 0) + 1 } @@ -404,24 +384,20 @@ fun ComponentsPage( LaunchedEffect(Hops.arrived) { if (Hops.arrived == arrivedSeen[0]) return@LaunchedEffect arrivedSeen[0] = Hops.arrived - // Once the page has flooded in from the rail. delay(Motion.ms(700).toLong()) rings[layer] = (rings[layer] ?: 0) + 1 } - /** The row at [key] squashes into a dot and flies home to [to]'s card, which pops on landing. */ fun flyHome(key: String, to: String) { val from = rowsAt[key] ?: return land(to) Flights.fly(Flight(from, to = { cardsAt[to]?.center }, onLand = { land(to) })) } - // A confirm stepping out of the row that asked: right of a left-column row, left of a right one. fun ask(key: String, right: Boolean, title: String, note: String, verb: String, danger: Boolean, face: String = "", then: () -> Unit) { val at = rowsAt[key] ?: return then() val cancel = ctx.getString(R.string.common_cancel) Steps.ask(StepAsk( anchor = at, side = if (right) StepSide.Left else StepSide.Right, accent = if (danger) colors.error else pal.signal, pillCorner = 10.dp, items = 3, maxWidth = 340.dp, - // The row's own name rides on top, as the pull's handle. handle = { Box(Modifier.fillMaxSize().padding(start = 46.dp, end = 14.dp), contentAlignment = Alignment.CenterStart) { Text(face, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = if (danger) colors.error else pal.signal, maxLines = 2, overflow = TextOverflow.Ellipsis) @@ -437,7 +413,6 @@ fun ComponentsPage( }) } - // The runtime and display drivers on their own pages, flooding out of the link that opens them. var driverPage by remember { mutableStateOf<String?>(null) } var driverShown by remember { mutableStateOf<String?>(null) } var driverLeaving by remember { mutableStateOf(false) } @@ -450,7 +425,6 @@ fun ComponentsPage( driverShown = null; driverLeaving = false; driverFrom = null } } - // Narrow: a layer's detail floods out of its card, and B draws it back in. var narrowOpen by rememberSaveable { mutableStateOf<String?>(null) } var narrowShown by remember { mutableStateOf<String?>(null) } var narrowLeaving by remember { mutableStateOf(false) } @@ -466,21 +440,17 @@ fun ComponentsPage( BackHandler(enabled = driverPage != null || narrowOpen != null) { if (driverPage != null) driverPage = null else narrowOpen = null } - // Leaving the page: nothing stays stepped out of a row that is no longer on screen. androidx.compose.runtime.DisposableEffect(Unit) { onDispose { Steps.drop() } } val checked = if (catalogAt > 0) DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(catalogAt * 1000)) else stringResource(R.string.comp_never) val firstRow = remember { FocusRequester() } val layerFocus = remember { LAYERS.associateWith { FocusRequester() } } - // Arrived (from the rail, a hop, a game's card): a pad starts on the chosen layer. LaunchedEffect(requestInitialFocus) { if (!requestInitialFocus || selectedGame != null || inputMode.inputMode != InputMode.Keyboard) return@LaunchedEffect delay(Motion.ms(200).toLong()) focusSoon { layerFocus.getValue(layer) } } var travel by remember { mutableIntStateOf(1) } - // LB / RB (taken by the activity before the page sees them): a pad anywhere on the page goes to - // the new layer's card, so one on a row - which leaves with its layer - is never left with nothing. var pageFocused by remember { mutableStateOf(false) } val seenLayer = remember { arrayOf(layer) } LaunchedEffect(layer) { @@ -494,7 +464,6 @@ fun ComponentsPage( if (l != layer) { travel = if (LAYERS.indexOf(l) > LAYERS.indexOf(layer)) 1 else -1; onComp(l) } } - // ---- one layer's detail, a slot at a time ------------------------------------------------------ val compState = { l: String -> view?.components?.get(l) } val bar: @Composable (String) -> Unit = { l -> when (l) { @@ -531,7 +500,6 @@ fun ComponentsPage( } } - // Installed and Available, as (title, rows) for a layer; [right] says which column it is in. var showAllPairs by rememberSaveable { mutableStateOf(false) } val installedTitle: @Composable (String) -> String = { l -> when (l) { @@ -643,8 +611,6 @@ fun ComponentsPage( } } PROTON_TAB -> { - // Not installed, and installed with a newer release out: an update goes in beside the - // build it replaces, which stays until it is removed (games are mapped to it by name). val rows = protons.rows.filter { it.installed == null || it.update != null } if (rows.isEmpty()) EmptyLine(stringResource(R.string.comp_nothing_new)) for (row in rows) GetLine( @@ -731,17 +697,13 @@ fun ComponentsPage( } } - // ---- the frame ------------------------------------------------------------------------------ val detail: @Composable (String, Boolean) -> Unit = { l, narrowPage -> androidx.compose.foundation.layout.BoxWithConstraints(Modifier.fillMaxSize()) { - // Two columns while each gets room for a row's name and its Get; one under the other below that. val single = narrowPage || maxWidth < 600.dp Column(Modifier.fillMaxSize()) { - // The in-use bar keeps its place; only its words change. AnimatedContent(l, transitionSpec = { fadeIn(Motion.tw(240)) togetherWith fadeOut(Motion.tw(160)) }, label = "inUse") { k -> bar(k) } val detailScroll = androidx.compose.runtime.key(l) { rememberScrollState() } Column(Modifier.weight(1f).fillMaxWidth().verticalScroll(detailScroll).padding(top = 12.dp, bottom = 16.dp)) { - // Shared performance settings stay visible before the downloadable catalog. if (l == PROTON_TAB) { if (!protons.runtimeReady) EmptyLine(stringResource(R.string.content_needs_runtime)) if (protons.sessionRunning) EmptyLine(stringResource(R.string.proton_stop_first)) @@ -801,7 +763,6 @@ fun ComponentsPage( onNext = { pick(layerStep(layer, 1)) }, ) else Modifier), ) { - // ---- header: the title, the device, when the lists were checked and the one refresh ---- Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) { Text(stringResource(R.string.comp_title), fontSize = if (narrow) 22.sp else 26.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1) if (!narrow) DeviceChip(gpu) @@ -894,7 +855,6 @@ fun ComponentsPage( ) } else { Row(Modifier.weight(1f).fillMaxWidth()) { - // ---- the stack ---- Box( (if (narrow) Modifier.fillMaxWidth() else Modifier.width(250.dp)).fillMaxHeight() .onGloballyPositioned { stackAt = it.positionInRoot() }, @@ -911,7 +871,6 @@ fun ComponentsPage( else runCatching { firstRow.requestFocus() } }, ) - // Over the cards: under them, their rest fill hid it as it crossed them. ChipGlide(LAYERS.indexOf(layer), local, tint, GlideAxis.Vertical, corner = 12.dp) } if (!narrow) { @@ -921,7 +880,6 @@ fun ComponentsPage( } } } - // Narrow: the layer's detail over the stack, out of its card. narrowShown?.let { l -> val from = cardsAt[l]?.let { Origin(it.translate(-pageAt), 12f * density.density) } val body: @Composable () -> Unit = { @@ -936,7 +894,6 @@ fun ComponentsPage( } if (from == null) body() else PageFlood(from, narrowLeaving) { body() } } - // The runtime or display driver on its own page. driverShown?.let { d -> val body: @Composable () -> Unit = { Box(Modifier.fillMaxSize().background(colors.background).pointerInput(Unit) { detectTapGestures { } }) { @@ -967,7 +924,6 @@ fun ComponentsPage( onDismissRequest = { confirm = null }, title = { Text(confirmTitle) }, text = { Text(body, fontSize = 14.sp) }, - // Opens on Cancel, so a stray A press on a controller never swaps or deletes anything. confirmButton = { val del = confirmVerb == R.string.common_delete || confirmVerb == R.string.comp_game_reset FocusText(stringResource(confirmVerb), if (del) colors.error else pal.signal) { confirm = null; action() } @@ -1008,7 +964,6 @@ fun ComponentsPage( } } -/** The Installed rows of one component layer for the Proton the page shows. */ @Composable private fun installedFor(l: String, view: ComponentsManager.ProtonView?, snapshot: Snapshot?): List<InstalledItem> { view ?: return emptyList() @@ -1027,7 +982,6 @@ private fun installedFor(l: String, view: ComponentsManager.ProtonView?, snapsho } } -/** The Nightlies of one component layer that are not stored yet. */ private fun availableFor(l: String, catalog: List<CatalogItem>, snapshot: Snapshot?): List<CatalogItem> { val stored = (snapshot?.packages ?: emptyList()).map { it.file }.toSet() return catalog.filter { it.comp == l && ComponentsManager.safeName(it.file) !in stored } @@ -1040,14 +994,8 @@ private fun LayerTitle(l: String): String = when (l) { else -> ComponentsManager.LABEL[l] ?: l } -/** Records where a row sits, for the confirm that steps out of it and the dot that flies from it. */ private fun Modifier.trackRow(key: String, into: HashMap<String, Rect>): Modifier = onGloballyPositioned { into[key] = it.boundsInRoot() } -/** - * The stack: a heading over the Proton and the three layers that belong to it (indented, on a - * line), then a heading over the GPU drivers that every Proton shares. Each card is a layer's name - * and what it is set to; a dot marks a layer swapped away from its original. - */ @Composable private fun Stack( layer: String, view: ComponentsManager.ProtonView?, gpu: GpuDriversState, @@ -1096,7 +1044,6 @@ private fun StackHeading(text: String) { ) } -/** One layer: 40dp to see, 44 to touch; its name, what it is set to, and a dot when it is swapped. */ @Composable private fun LayerCard( title: String, value: String, swapped: Boolean, selected: Boolean, landed: Int, ring: Int, @@ -1106,7 +1053,6 @@ private fun LayerCard( val pal = LocalPalette.current val src = remember { MutableInteractionSource() } val hot = rememberHot(src) - // The stack's glide draws the picked card's look by touch only; with a pad, its rest look and the bar. val under = selected && androidx.compose.ui.platform.LocalInputModeManager.current.inputMode != androidx.compose.ui.input.InputMode.Keyboard Box(Modifier.fillMaxWidth().heightIn(min = 44.dp), contentAlignment = Alignment.Center) { Row( @@ -1125,7 +1071,6 @@ private fun LayerCard( if (swapped) Box(Modifier.size(7.dp).clip(CircleShape).background(pal.signal)) Text(title, fontSize = 13.5.sp, fontWeight = FontWeight.Bold, color = if (selected || hot) colors.onBackground else colors.onSurfaceVariant, maxLines = 1) Spacer(Modifier.weight(1f)) - // The value ticks over as a swap lands. AnimatedContent( value, transitionSpec = { (slideInVertically(Motion.tw(260)) { it / 2 } + fadeIn(Motion.tw(260))) togetherWith (slideOutVertically(Motion.tw(160)) { -it / 2 } + fadeOut(Motion.tw(160))) }, @@ -1135,7 +1080,6 @@ private fun LayerCard( } } -/** What is in use for the layer: a dot (green, or amber while a change waits), a sentence, and a note or Cancel. */ @Composable private fun InUseBar(queued: Boolean, sentence: String, note: String, onCancel: (() -> Unit)? = null) { val colors = MaterialTheme.colorScheme @@ -1152,7 +1096,6 @@ private fun InUseBar(queued: Boolean, sentence: String, note: String, onCancel: } } -/** A titled column of rows. */ @Composable private fun RowGroup(title: String, content: @Composable ColumnScope.() -> Unit) { val colors = MaterialTheme.colorScheme @@ -1437,10 +1380,6 @@ internal fun ToolIcon(icon: ImageVector, description: String, busy: Boolean = fa } } -/** - * A small button. Resting: grey outline (blue text when [accent]). Focused or hovered: a solid blue - * fill with an outline in the text colour - unmistakable in a column of identical buttons. - */ @Composable private fun SmallButton(text: String, enabled: Boolean = true, accent: Boolean = false, modifier: Modifier = Modifier, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -1485,10 +1424,6 @@ private fun Tag(text: String) { ) } -/** - * One installed choice: a radio for the one in use, its name and detail, and a trash at the right - * of the ones that can go. [progress] shows a download for it in place of the tag. - */ @Composable private fun ChoiceLine( name: String, detail: String, selected: Boolean, removable: Boolean, @@ -1525,10 +1460,8 @@ private fun ChoiceLine( } DetailLine(detail, badge) } - // The radio says which is in use; a download in flight shows where a tag would. if (progress != null) ProgressBit(progress) } - // The trash column is always there (empty when the row can't be deleted) so rows line up. if (!removable) Spacer(Modifier.padding(end = 6.dp).size(44.dp)) else { val delSrc = remember { MutableInteractionSource() } @@ -1546,7 +1479,6 @@ private fun ChoiceLine( Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line)) } -/** One thing not installed yet: its name and detail, and Get - its download shown in the row once started. */ @Composable private fun GetLine( name: String, detail: String, progress: Int?, enabled: Boolean, @@ -1571,7 +1503,6 @@ private fun GetLine( Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line)) } -/** A row's second line, led by [badge] (Recommended) in green when there is one. */ @Composable private fun DetailLine(detail: String, badge: String?) { val colors = MaterialTheme.colorScheme @@ -1599,10 +1530,6 @@ private fun ProgressBit(progress: Int) { } } -/** - * The GPU and how far it is supported, as a chip: a green dot and "<GPU> · tested", amber when it - * is not. A steps the device's full details out of it, with the copy for a bug report. - */ @Composable private fun DeviceChip(s: GpuDriversState) { val colors = MaterialTheme.colorScheme @@ -1643,7 +1570,6 @@ private fun DeviceChip(s: GpuDriversState) { ) { face() } } -/** What the Proton layer installs: the Protons DroidDeck can download, and one being installed. */ class ProtonsState( val rows: List<ProtonRow> = emptyList(), val busyId: String? = null, @@ -1656,16 +1582,10 @@ class ProtonsState( class ProtonsActions( val onInstall: (String) -> Unit = {}, val onCancel: (String) -> Unit = {}, - /** The source's id and the installed folder to delete. */ val onRemove: (String, String) -> Unit = { _, _ -> }, - /** Deletes every build of the source but the newest. */ val onRemoveOlder: (String) -> Unit = {}, ) -/** - * A Proton DroidDeck can install: the folder its newest build is installed as (null when it is - * not), every installed build oldest first, and a newer release's tag when one is out. - */ class ProtonRow( val id: String, val name: String, val installed: String?, val queued: Boolean, val installedAll: List<String> = listOfNotNull(installed), val update: String? = null, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/CursorOverlay.kt b/app/src/main/java/com/droiddeck/launcher/ui/CursorOverlay.kt index 6b527c97c..8f883c24c 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/CursorOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/CursorOverlay.kt @@ -11,11 +11,6 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Stroke -/** - * The session's pointer. [image]: the program's own cursor (wl_pointer.set_cursor - labwc's resize - * arrows, a text field's I-beam), in session pixels, drawn [imageScale] times larger with its hotspot - * ([hotX], [hotY]) on [position]; null = the program has not set one, and the built-in arrow is drawn. - */ @Composable fun CursorOverlay( position: Offset, visible: Boolean, scale: Float, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/DeckControlsPanel.kt b/app/src/main/java/com/droiddeck/launcher/ui/DeckControlsPanel.kt index 4bee3b6a8..b030632fc 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/DeckControlsPanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/DeckControlsPanel.kt @@ -27,16 +27,6 @@ import kotlin.math.atan2 import kotlin.math.hypot import kotlin.math.min -/** - * The Steam Deck controller's back grips and trackpads, on the second screen. - * - * Four tabs: the grips as a 2x2 grid (left grips on the left, upper above lower, as they sit on a - * Deck); either trackpad on its own with a bar to click it; and both trackpads with the grips in a - * row beneath. Given the [pad] (the unfolded screen's panel), a fifth: a whole gamepad, each side - * an analog stick or a trackpad as the player picks. A trackpad clicks from its bar or from a second finger while the first is down. - * Drawn for OLED: true black, with outlines rather than filled surfaces, and the Steam blue only - * where something is being touched. - */ @SuppressLint("ViewConstructor") class DeckControlsPanel( context: Context, @@ -61,8 +51,6 @@ class DeckControlsPanel( override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) - // Held flat the gamepad has no Panels button, so another tab would have no way back: - // turning the phone from upright onto its side returns to the gamepad. if (pad != null && selectedWithPad != Tab.GAMEPAD && !GamepadGeometry.tall(w.toFloat(), h.toFloat())) { post { if (selectedWithPad != Tab.GAMEPAD) select(Tab.GAMEPAD) } } @@ -115,7 +103,6 @@ class DeckControlsPanel( view.setTextColor(if (on) ON_ACCENT else TEXT_DIM) } content.removeAllViews() - // The gamepad needs every row of the panel; held upright its Panels button brings the tabs back. topBar.visibility = if (tab == Tab.GAMEPAD) View.GONE else View.VISIBLE content.addView( when (tab) { @@ -159,7 +146,6 @@ class DeckControlsPanel( addView(TrackpadView(context, right = true), LayoutParams(0, LayoutParams.MATCH_PARENT, 1f).apply { leftMargin = dp(6) }) } addView(pads, LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f).apply { bottomMargin = dp(12) }) - // Mirrored, as hands hold them: the upper grips outside, the lower ones in the middle. val grips = LinearLayout(context).apply { orientation = HORIZONTAL val row = listOf("L4" to DeckControls.L4, "L5" to DeckControls.L5, "R5" to DeckControls.R5, "R4" to DeckControls.R4) @@ -173,15 +159,6 @@ class DeckControlsPanel( addView(grips, LayoutParams(LayoutParams.MATCH_PARENT, dp(84))) } - /** - * A whole pad, placed for two thumbs rather than in columns (see [GamepadGeometry]): on each side - * a surface where the thumb rests - most of play is on the sticks or trackpads - and a short - * slide inward along its arc, the d-pad on the left and the face buttons on the right; the bumpers and triggers above, at the top of the arc; View, Steam, QAM and Menu in the - * middle, which a thumb only reaches on purpose. Each surface is an analog stick or a Steam - * trackpad, switched from its corner and remembered (SessionPrefs.gamepadSurface). Its tab bar - * is hidden; held upright, a Panels button brings the other tabs back, and held flat there is no - * room for one. - */ private fun gamepad(pad: PadBridge): View = GamepadLayout(context).apply { fun pill(label: String, write: (PadState, Boolean) -> Unit) = PadButton(context, pad, label, write) place(surfaceSlot(pad, right = false), GamepadGeometry.Slot.LEFT_MAIN) @@ -215,13 +192,11 @@ class DeckControlsPanel( } } - /** One side's stick or trackpad, with a chip in its outer corner that swaps the two. */ private fun surfaceSlot(pad: PadBridge, right: Boolean): View = FrameLayout(context).apply { val slot = this fun show() { slot.removeAllViews() val stick = SessionPrefs.gamepadSurface(context, right) == SessionPrefs.SURFACE_STICK - // No caption: the switch in its corner says which it is. slot.addView(if (stick) StickView(context, pad, right) else TrackpadView(context, right, captioned = false), FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) val chip = TextView(context).apply { @@ -239,7 +214,6 @@ class DeckControlsPanel( show() } } - // In the corner away from the thumb, so play does not swap it by accident. slot.addView(chip, FrameLayout.LayoutParams(dp(30), dp(30), (if (right) Gravity.START else Gravity.END) or Gravity.TOP).apply { setMargins(dp(6), dp(6), dp(6), dp(6)) }) } @@ -280,7 +254,6 @@ class DeckControlsPanel( private fun dp(value: Int): Int = (value * resources.displayMetrics.density + 0.5f).toInt() - /** A surface drawn the same way everywhere on the panel; pressed, it lights up in the accent. */ private abstract class Surface(context: Context) : View(context) { protected val density = resources.displayMetrics.density protected val rect = RectF() @@ -307,7 +280,6 @@ class DeckControlsPanel( } } - /** A held button: down while a finger is on it. */ private open class HoldButton( context: Context, private val label: String, @@ -353,10 +325,6 @@ class DeckControlsPanel( private class ClickBar(context: Context, right: Boolean) : HoldButton(context, context.getString(R.string.deck_click), if (right) context.getString(R.string.deck_right_trackpad) else context.getString(R.string.deck_left_trackpad), { DeckControls.setClick(right, it) }) - /** - * One trackpad: where the first finger is, as the Deck reports it (-1..1, y up), and a click - * while a second finger is down. - */ private class TrackpadView(context: Context, private val right: Boolean, private val captioned: Boolean = true) : Surface(context) { private var touching = false private var clicked = false @@ -396,7 +364,6 @@ class DeckControlsPanel( } MotionEvent.ACTION_POINTER_DOWN -> click(true) MotionEvent.ACTION_POINTER_UP -> { - // The finger that lifted may be the first; the pad follows whichever stays. val stays = if (event.actionIndex == 0) 1 else 0 touch(event.getX(stays), event.getY(stays)) click(event.pointerCount - 1 > 1) @@ -428,7 +395,6 @@ class DeckControlsPanel( } } - /** A pad button held while a finger is on it, written into the shared pad as the on-screen pad does. */ private class PadButton( context: Context, private val pad: PadBridge, @@ -441,11 +407,6 @@ class DeckControlsPanel( } } - /** - * An analog stick that centres where the thumb lands, so it is never missed: the knob follows - * the finger from there out to the ring. Tapping it again and holding (within [CLICK_MS]) is - * the stick's click - L3 or R3 - for as long as the thumb stays down. - */ private class StickView(context: Context, private val pad: PadBridge, private val right: Boolean) : Surface(context) { private var touching = false private var clicking = false @@ -475,7 +436,6 @@ class DeckControlsPanel( val r = reach().coerceAtLeast(1f) when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { - // Centred where the thumb lands, but kept far enough in for the ring to fit. originX = event.x.coerceIn(r, (width - r).coerceAtLeast(r)) originY = event.y.coerceIn(r, (height - r).coerceAtLeast(r)) touching = true @@ -525,10 +485,9 @@ class DeckControlsPanel( } } - /** The d-pad: the direction is the finger's angle from the centre, diagonals included. */ private class DirectionPad(context: Context, private val pad: PadBridge) : View(context) { private val density = resources.displayMetrics.density - private var held = 0 // bits: 1 up, 2 right, 4 down, 8 left + private var held = 0 private val arm = Paint(Paint.ANTI_ALIAS_FLAG) private val rect = RectF() @@ -558,7 +517,6 @@ class DeckControlsPanel( val dx = event.getX(0) - width / 2f val dy = event.getY(0) - height / 2f if (hypot(dx, dy) < min(width, height) * 0.08f) 0 else { - // Eight sectors of 45 degrees, starting with right and going clockwise. val sector = ((Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())) + 360 + 22.5) % 360 / 45).toInt() intArrayOf(2, 2 or 4, 4, 4 or 8, 8, 8 or 1, 1, 1 or 2)[sector] } @@ -585,7 +543,6 @@ class DeckControlsPanel( } } - /** A, B, X and Y in a diamond, as on a Deck; several fingers can hold several at once. */ private class FaceButtons(context: Context, private val pad: PadBridge) : View(context) { private val buttons = listOf(PadState.Y to "Y", PadState.B to "B", PadState.A to "A", PadState.X to "X") private var held = emptySet<Int>() @@ -595,7 +552,6 @@ class DeckControlsPanel( typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } - /** Top, right, bottom, left - the centre of each, and the radius of all. */ private fun layout(): Pair<List<Pair<Float, Float>>, Float> { val cx = width / 2f val cy = height / 2f @@ -626,7 +582,6 @@ class DeckControlsPanel( if (event.actionMasked == MotionEvent.ACTION_POINTER_UP && p == event.actionIndex) continue val x = event.getX(p) val y = event.getY(p) - // The nearest button within reach, so a thumb between two still lands on one. val nearest = centres.indices.minByOrNull { hypot(centres[it].first - x, centres[it].second - y) } ?: continue if (hypot(centres[nearest].first - x, centres[nearest].second - y) <= radius * 1.6f) now.add(buttons[nearest].first) } @@ -649,7 +604,6 @@ class DeckControlsPanel( } } - /** Lays its controls out where [GamepadGeometry] puts them for the panel's size. */ private class GamepadLayout(context: Context) : ViewGroup(context) { private val slots = LinkedHashMap<View, GamepadGeometry.Slot>() @@ -686,12 +640,9 @@ class DeckControlsPanel( } companion object { - /** A second press on a stick this soon after the last is its click (L3, R3). */ private const val CLICK_MS = 300L - /** The tab last shown, kept for the session so reopening the panel lands where it was. */ private var selected = Tab.BOTH - /** The same for the unfolded screen's panel, which opens on the gamepad. */ private var selectedWithPad = Tab.GAMEPAD private const val CORNER = 20f diff --git a/app/src/main/java/com/droiddeck/launcher/ui/DesktopPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/DesktopPage.kt index 9d52bdf50..e1068a53c 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/DesktopPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/DesktopPage.kt @@ -93,12 +93,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -// The Desktop tab in the Steam tab's shape: a tilted wall of little windows behind the word and -// one big Open desktop, with Play's lip and lifted sheet under it (Display, Controls, Session and -// Files). Under the hero the page scrolls to two shelves - the apps that run in the desktop, and -// what can be added - and the hero folds into a slim header as it goes. -/** The desktop's own chips: its session settings, then Files. */ private val DESKTOP_TABS = modeSheetTabs(steam = false) + SheetTab.FILES @Composable @@ -108,7 +103,6 @@ internal fun DesktopPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie val narrow = LocalNarrowPane.current val colors = MaterialTheme.colorScheme LaunchedEffect(Unit) { UserAppsState.refresh(ctx) } - // Flathub's installed apps sit with the others on the dock. LaunchedEffect(s.ready) { com.droiddeck.launcher.store.StoreState.refresh(ctx) } val installed = s.emulators.filter { it.installed } val apps = UserAppsState.items @@ -116,14 +110,11 @@ internal fun DesktopPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie val mode = SessionService.MODE_DESKTOP val tabs = DESKTOP_TABS.map { it to sheetTabLabel(it) } val open = if (s.desktopInstalled) stringResource(R.string.content_open_desktop) else stringResource(R.string.content_install_open_desktop) - // Opened from a card: the page floods out of it, its picture on the tile. fun openFrom(at: Rect?, art: Any?, page: String) { at?.let { PageOrigin.mark(Origin(it, 16f * density.density, art = art)) } onSelect(page) } - // The Steam tab's shape, the whole pane: the dock of apps along the top of the wall, the word - // and Open desktop at the foot, and nothing under the settings sheet but the hero itself. LiftHero( key = "desktop", tabs = tabs, onOpen = a.onDesktopSettings, onPrimary = a.onDesktop, @@ -134,15 +125,12 @@ internal fun DesktopPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie }, scrim = { lifted -> WallFade() - // Over the fades, so the cards read at full strength; gone as the sheet lifts, so - // nothing but the wall is behind it. Box(Modifier.graphicsLayer { val v = lifted(); alpha = 1f - v; translationY = -12.dp.toPx() * v }) { DesktopDock(s, a, installed, apps, openFrom = ::openFrom) } }, words = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - // The word at the wordmark's size. Text( stringResource(R.string.rail_desktop), fontSize = if (narrow) 30.sp else 42.sp, lineHeight = if (narrow) 34.sp else 46.sp, fontWeight = FontWeight.Black, color = colors.onBackground, @@ -151,13 +139,10 @@ internal fun DesktopPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie } }, button = { click -> - // Enabled without a runtime or the desktop: the session's loading screen installs them first. PrimaryButton(open, enabled = !s.busy, main = true, large = true, icon = Icons.Outlined.DesktopWindows, onClick = click) }, beside = { - // Flathub, whose apps run in the desktop: its storefront floods out of this button. HeroIconButton(Icons.Outlined.Storefront, stringResource(R.string.flathub_name), FlathubTeal, "desktop:flathub") { at -> openFrom(at, null, "store") } - // Add your own app or game. HeroIconButton(Icons.Filled.Add, stringResource(R.string.user_apps_add), null, "desktop:add") { if (!s.busy) adding = true } }, rows = { tab -> @@ -168,11 +153,6 @@ internal fun DesktopPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie if (adding && !s.busy) AddAppDialog(s.ready, onDismiss = { adding = false }) { request, label -> UserAppsState.add(ctx, request, label) } } -/** - * The dock along the top of the Desktop hero: what runs in the desktop (emulators installed, apps - * added, Flathub's installed apps), then the emulators that can be added - one - * row of cards that scrolls sideways, over the wall. It sinks back with the wall as the sheet lifts. - */ @Composable private fun DesktopDock( s: FrontEndState, a: FrontEndActions, installed: List<Library.Emulator>, apps: List<UserApps.App>, @@ -184,14 +164,12 @@ private fun DesktopDock( val available = s.emulators.filter { !it.installed } val flatpaks = com.droiddeck.launcher.store.StoreState.installed val keys = installed.map { "emu:${it.id}" } + apps.map { "user:${it.key}" } + flatpaks.map { "flatpak:${it.id}" } - // Where the last of your apps sits, for a new emulator's dot to land after it. var lastAt by remember { mutableStateOf<Rect?>(null) } var dockAt by remember { mutableStateOf<Rect?>(null) } val seen = remember { mutableSetOf<String>() } val mounted = remember { booleanArrayOf(false) } LaunchedEffect(Unit) { mounted[0] = true } val scroll = rememberScrollState() - // Nothing yet (the emulators' catalog still on its way): no heading over an empty row. if (keys.isEmpty() && available.isEmpty() && UserAppsState.working == null && UserAppsState.lastError == null) return Column(Modifier.fillMaxWidth().padding(top = if (narrow) 14.dp else 22.dp)) { Text( @@ -205,7 +183,6 @@ private fun DesktopDock( ) { keys.forEachIndexed { i, k -> key(k) { - // A card that arrives after the dock is up (an emulator just installed) opens in. val state = remember { MutableTransitionState(!mounted[0] || k in seen) }.apply { targetState = true } LaunchedEffect(Unit) { seen += k } AnimatedVisibility(state, enter = expandHorizontally(Motion.sp(0.75f)) + fadeIn(Motion.tw(300, 80))) { @@ -224,7 +201,6 @@ private fun DesktopDock( k, flatpak.name, flatpak.summary ?: stringResource(R.string.flathub_name), art = flatpak.icon ?: com.droiddeck.launcher.store.FlathubApi.iconUrl(flatpak.id), first = i == 0, ) { at -> - // Its page on Flathub: open, update or remove it there. com.droiddeck.launcher.store.StoreState.openRequest = flatpak.id openFrom(at, flatpak.icon, "store") } else if (app != null) DesktopCard(k, app.name, stringResource(app.kind.label()), art = app.icon, fallback = if (app.kind == UserApps.Kind.SCRIPT) Icons.Outlined.Terminal else Icons.Outlined.Apps, first = i == 0) { at -> @@ -234,7 +210,6 @@ private fun DesktopDock( } } } - // Between yours and what can be added: a thin rule. if (keys.isNotEmpty()) Box(Modifier.padding(horizontal = 4.dp).width(1.dp).height(120.dp).background(pal.line2)) available.forEach { e -> key(e.id) { @@ -247,8 +222,6 @@ private fun DesktopDock( art = e.iconRes, badge = if (busyHere) null else stringResource(R.string.comp_get), progress = if (busyHere) s.packagePercent else null, first = keys.isEmpty() && e == available.first(), ) { at -> - // Get installs it there and then, and a dot flies to where its card will be; - // when it cannot (no runtime, a session running), its page says why. if (canGet && pkg != null && at != null) { a.onInstallPackage(pkg.id) val end = lastAt @@ -268,10 +241,6 @@ private fun DesktopDock( } } -/** - * A square button the big one's height beside it on the hero: an icon in [tint] (the palette's - * signal when null). [onClick] gets its bounds, for a page to flood out of. - */ @Composable private fun HeroIconButton(icon: androidx.compose.ui.graphics.vector.ImageVector, description: String, tint: Color?, id: String, onClick: (Rect?) -> Unit) { val colors = MaterialTheme.colorScheme @@ -295,10 +264,6 @@ private fun HeroIconButton(icon: androidx.compose.ui.graphics.vector.ImageVector ) { Icon(icon, null, tint = accent, modifier = Modifier.size(26.dp)) } } -/** - * The hero's wall: little windows tilted and drifting as Steam's capsules do, each a window with a - * bar along its foot and an emulator's or app's icon inside; blank windows when there are none. - */ @Composable private fun DesktopWall(icons: List<Any>) { val pal = LocalPalette.current @@ -308,11 +273,6 @@ private fun DesktopWall(icons: List<Any>) { } } -/** - * One thing that runs in the desktop: its icon over a blurred, darkened copy of itself, a status on - * the art (Get, or how far an install is), and under it its name on up to two lines and what it - * is. [onClick] gets the art's bounds, for a page to flood out of or a dot to fly from. - */ @Composable private fun DesktopCard( key: String, name: String, detail: String, art: Any?, @@ -384,10 +344,6 @@ private fun DesktopCard( } } -/** - * The desktop sheet's Files chip: the file manager and the ROMs folder (both from Setup's old - * tools), how much the desktop takes, and Uninstall - its only home - which asks in red first. - */ @Composable private fun DesktopFiles(s: FrontEndState, a: FrontEndActions) { val ctx = LocalContext.current @@ -408,7 +364,6 @@ private fun DesktopFiles(s: FrontEndState, a: FrontEndActions) { } } -/** Uninstall the desktop: are you sure steps out of it in red, Keep first and focused. */ @Composable private fun UninstallDesktop(enabled: Boolean, onUninstall: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -435,7 +390,6 @@ private fun UninstallDesktop(enabled: Boolean, onUninstall: () -> Unit) { } } -/** The add or remove under way, or why the last one failed. */ @Composable private fun UserAppsProgressLine() { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/DriverPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/DriverPage.kt index 1e62c42e3..3d3994384 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/DriverPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/DriverPage.kt @@ -55,13 +55,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -/** - * One driver list as a full page - the Runtime driver or the Display driver - opened from the - * session's settings page. Installed drivers are picked by tapping the row and each carries a trash - * can (the "Auto"/"Runtime default" row is a setting, not a driver, so it has none); what the last - * check of the release repos found is offered below; the refresh button beside the title is the - * only thing that goes online. - */ @Composable fun DriverPage( title: String, @@ -85,16 +78,13 @@ fun DriverPage( val colors = MaterialTheme.colorScheme val pal = LocalPalette.current var confirm by remember { mutableStateOf<DriverRow?>(null) } - // Back (the controller's B too) closes this page only, back to the session's settings. BackHandler(onBack = onBack) - // D-pad: up from the first driver lands on refresh; the page opens with the driver in use focused. val refreshFocus = remember { FocusRequester() } val selectedFocus = remember { FocusRequester() } val inputMode = LocalInputModeManager.current var pageFocused by remember { mutableStateOf(false) } LaunchedEffect(inputMode.inputMode) { if (inputMode.inputMode != InputMode.Keyboard) return@LaunchedEffect - // The chooser holds controller focus until its closing animation finishes. snapshotFlow { Steps.step }.first { it == null } focusWithinFrames({ pageFocused }) { selectedFocus } } @@ -162,7 +152,6 @@ fun DriverPage( fontSize = 13.sp, ) }, - // Opens on Cancel, so a stray A press on a controller never deletes anything. confirmButton = { FocusText(stringResource(R.string.common_delete), colors.error) { confirm = null; onDelete(row.id) } }, dismissButton = { val cancelFocus = remember { FocusRequester() } @@ -180,10 +169,6 @@ private fun InstalledRow(row: DriverRow, selected: Boolean, onSelect: () -> Unit val src = remember { MutableInteractionSource() } val hot = src.collectIsFocusedAsState().value || src.collectIsHoveredAsState().value val shape = RoundedCornerShape(10.dp) - // Two focus targets side by side - the driver itself, and its trash can - so a controller's - // d-pad right reaches the trash can and left comes back. (Inside one clickable row the trash - // can was a child of the focused area and "right" had nowhere to go.) The row's outline still - // follows the driver part, so it looks the same. val mainFocus = remember { FocusRequester() } val delFocus = remember { FocusRequester() } Row( @@ -236,7 +221,6 @@ private fun InstalledRow(row: DriverRow, selected: Boolean, onSelect: () -> Unit Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line)) } -/** A text button that shows where controller focus is: an outline in its own colour. */ @Composable internal fun FocusText(text: String, color: Color, modifier: Modifier = Modifier, onClick: () -> Unit) { val src = remember { MutableInteractionSource() } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/EmptyLibrary.kt b/app/src/main/java/com/droiddeck/launcher/ui/EmptyLibrary.kt index a605c38e9..c29d3abd4 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/EmptyLibrary.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/EmptyLibrary.kt @@ -45,14 +45,6 @@ import kotlin.math.max import kotlin.math.min import kotlin.math.sin -/** - * The Steam tab with nothing installed. The blank wall drifts as the full one does, and the - * DroidDeck mark wanders over it, bouncing off the page's edges and off [avoid] (the words and - * buttons) like an idle DVD logo, carrying a clearing in the wall with it. On arrival its outline - * traces in, the D fills, and the ball pops toward the viewer, flips twice and slams back into its - * socket; the ring the slam sends out fills each blank capsule it reaches. Poking the ball plays - * the pop again, without the mark stopping. [fade] is drawn over the wall and under the mark. - */ @Composable internal fun EmptyLibrary(avoid: Rect?, fade: @Composable () -> Unit) { val clock = rememberMarkClock(avoid) @@ -63,23 +55,16 @@ internal fun EmptyLibrary(avoid: Rect?, fade: @Composable () -> Unit) { } } -// The trace and fill, in ms from arrival. private const val TRACE_MS = 1250f private const val FILL_FROM = 450f -// The ball's timeline: its fade-in, pop, flip and slam, the ripple and the capsules it lights. private const val POP_FROM = 900f private const val IMPACT = 2200f private const val RIPPLE_MS = 1250f private const val HIT_MS = 900f private const val END = 3600f -// Where a poke restarts the ball's timeline: just before the wind-up. private const val POKE = 880f private const val DRIFT_MS = 75_000f -/** - * Time into the mark's arrival, into the ball's timeline (a poke restarts only that), into the - * wall's drift, and where the mark is on the page and where its last slam landed. - */ private class MarkClock { var intro by mutableFloatStateOf(0f) var ball by mutableFloatStateOf(0f) @@ -107,7 +92,6 @@ private fun rememberMarkClock(avoid: Rect?): MarkClock { if (clock.page == Size.Zero) { withFrameMillis { }; continue } val f = MarkFrame(clock.page, Offset.Zero) if (clock.at == Offset.Unspecified) clock.at = Offset(clock.page.width * 0.64f, clock.page.height * 0.42f) - // Animations off in the system settings: the finished mark, still, on a still wall. if (Motion.scale == 0f) { clock.intro = END clock.ball = END @@ -123,7 +107,6 @@ private fun rememberMarkClock(avoid: Rect?): MarkClock { val before = clock.ball clock.ball += dt - // Drift, bouncing off the page edges and off whatever it should keep clear of. var x = clock.at.x + vx * dt / 1000f var y = clock.at.y + vy * dt / 1000f val hw = f.halfWidth @@ -152,10 +135,6 @@ private fun rememberMarkClock(avoid: Rect?): MarkClock { return clock } -/** - * The mark on a page of [size], centred on [centre]: about a third of the page tall. [k] scales - * the timeline's distances, which were drawn on a 460px mark. - */ private class MarkFrame(size: Size, val centre: Offset) { val height = min(size.height * 0.3f, size.width * 0.28f * LogoShape.HEIGHT / LogoShape.WIDTH) val s = height / LogoShape.HEIGHT @@ -180,11 +159,9 @@ private val HitEase = CubicBezierEasing(0.2f, 0.7f, 0.3f, 1f) private fun clamp01(v: Float) = v.coerceIn(0f, 1f) private fun mix(a: Float, b: Float, t: Float) = a + (b - a) * t -/** One stretch of a keyframed value: [from] to [to] while [p] runs from [a] to [b]. */ private fun span(p: Float, a: Float, b: Float, from: Float, to: Float, ease: Easing) = mix(from, to, ease.transform(clamp01((p - a) / (b - a)))) -/** When the ripple's edge reaches [progress] of its way out, as a share of the ripple's time. */ private val rippleTime: FloatArray = FloatArray(257) { RippleEase.transform(it / 256f) } private fun rippleReaches(progress: Float): Float { var lo = 0 @@ -196,14 +173,12 @@ private fun rippleReaches(progress: Float): Float { return lo / 256f } -/** How lit a capsule the ripple reached [since] ms ago is: up fast, then fading back. */ private fun hitAmount(since: Float): Float { if (since < 0f || since > HIT_MS) return 0f val p = since / HIT_MS return if (p < 0.14f) HitEase.transform(p / 0.14f) else 1f - HitEase.transform((p - 0.14f) / 0.86f) } -/** The blank wall: the same tilted, drifting columns as [CapsuleWall], each capsule lit as the ripple passes. */ @Composable private fun EmptyWall(clock: MarkClock) { val pal = LocalPalette.current @@ -239,7 +214,6 @@ private fun EmptyWall(clock: MarkClock) { val dy = if (c % 2 == 0) -shift else shift - run for (i in 0 until perRun * 2) { val y = top + i * (capH + gap) + dy - // The capsule's centre on the page, out of the wall's tilt. val lx = x + capW / 2 - pivot.x val ly = y + capH / 2 - pivot.y val sx = pivot.x + lx * cs + ly * sn @@ -266,7 +240,6 @@ private fun EmptyWall(clock: MarkClock) { } } } - // The clearing the mark carries with it. drawRect( Brush.radialGradient( 0f to bg, 255f / 420f to bg, 330f / 420f to bg.copy(alpha = 0.7f), 1f to Color.Transparent, @@ -276,7 +249,6 @@ private fun EmptyWall(clock: MarkClock) { } } -/** The mark, its ball's pop and flip, and the ring the slam sends out. */ @Composable private fun Mark(clock: MarkClock) { val pal = LocalPalette.current @@ -303,7 +275,6 @@ private fun Mark(clock: MarkClock) { val t = clock.ball val intro = clock.intro - // The ripple, from where the slam landed, under the mark. if (clock.slam != Offset.Unspecified && t in IMPACT..IMPACT + RIPPLE_MS) { val p = (t - IMPACT) / RIPPLE_MS val alpha = if (p < 0.7f) mix(0.9f, 0.35f, p / 0.7f) else mix(0.35f, 0f, (p - 0.7f) / 0.3f) @@ -323,7 +294,6 @@ private fun Mark(clock: MarkClock) { } else 0f translate(0f, shake) { - // The D, as flat raised slabs with a solid edge underneath. val fill = Out.transform(clamp01((intro - FILL_FROM) / 450f)) if (fill > 0f) { val lift = 8f * k * (1f - fill) @@ -338,7 +308,6 @@ private fun Mark(clock: MarkClock) { drawPath(right, ink.copy(alpha = fill)) } - // The trace around both halves on arrival, the GameCube nod. if (intro < TRACE_MS) { val drawn = TraceEase.transform(clamp01(intro / 775f)) val alpha = if (intro < 775f) 1f else 1f - (intro - 775f) / 475f @@ -352,7 +321,6 @@ private fun Mark(clock: MarkClock) { } } - // The ball. val shown = Out.transform(clamp01((intro - 650f) / 300f)) if (shown > 0f) { val p = (t - POP_FROM) / (IMPACT - POP_FROM) @@ -366,7 +334,6 @@ private fun Mark(clock: MarkClock) { val q = clamp01((pop - 1f) / 0.6f) val bob = if (t > END) -4f * k * (0.5f - 0.5f * cos(2f * PI.toFloat() * (t - END) / 5200f)) else 0f - // Its shadow stays on the slabs, spreading and softening as the ball rises. val blur = mix(10f, 28f, q) * k val sr = f.ball * 0.93f * mix(1f, 1.4f, q) val dark = Color.Black.copy(alpha = mix(0.6f, 0.25f, q) * shown) @@ -396,7 +363,6 @@ private fun Mark(clock: MarkClock) { } } - // The flash where it lands. if (t in IMPACT..IMPACT + 420f) { val p = Out.transform((t - IMPACT) / 420f) drawCircle(pal.signal.copy(alpha = 1f - p), (f.ball + 6f * k) * mix(1f, 1.4f, p), f.centre, style = Stroke(max(4f * k, 2.dp.toPx()))) @@ -405,10 +371,6 @@ private fun Mark(clock: MarkClock) { } } -/** - * The ball as a coin of [thick]ness turned [deg] about its vertical axis: flat faces in the logo's - * blue, and between them a solid rim that shows as it turns edge-on. - */ private fun DrawScope.coin(c: Offset, r: Float, thick: Float, deg: Float, face: Color, rim: Color) { val a = Math.toRadians(deg.toDouble()) val cs = cos(a).toFloat() diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ExePickerDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/ExePickerDialog.kt index cda22fc69..901bf6485 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ExePickerDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ExePickerDialog.kt @@ -62,13 +62,11 @@ import com.droiddeck.launcher.session.GameStorage import com.droiddeck.launcher.stores.formatBytes import java.io.File -/** What a picker lists besides folders. */ internal enum class PickKind(val extensions: Set<String>) { EXE(setOf("exe")), IMAGE(setOf("png", "jpg", "jpeg", "webp", "ico")), } -/** A place a file can be picked from: internal storage or a card, at its root. */ internal class PickVolume(val root: File, val label: String, val removable: Boolean) { val free: Long get() = runCatching { StatFs(root.path).availableBytes }.getOrDefault(0L) } @@ -79,15 +77,10 @@ internal fun pickVolumes(context: Context): List<PickVolume> = File(o.path.substringBefore("/Android/")).takeIf { it.isDirectory }?.let { PickVolume(it, o.name, removable = true) } }.distinctBy { it.root.absolutePath } -/** A folder in a listing; [readable] false = shown greyed, not opened. */ internal class PickFolder(val dir: File, val readable: Boolean) internal class PickListing(val folders: List<PickFolder>, val files: List<File>) -/** - * Every folder of [dir] (dot-folders left out, the unreadable ones marked), then the files [kind] - * takes; each A-Z. - */ internal fun pickListing(dir: File, kind: PickKind): PickListing { val all = dir.listFiles()?.filter { !it.name.startsWith(".") }.orEmpty().sortedBy { it.name.lowercase() } return PickListing( @@ -96,13 +89,6 @@ internal fun pickListing(dir: File, kind: PickKind): PickListing { ) } -/** - * The compact in-app file picker: [title] and a red X on one line; the storage rows (internal - * storage, each card, free space); inside one, its root first, a breadcrumb, "..", every folder - * and the [kind] files, scrolling in the dialog. Its width is what the first step needed and stays; - * long names are cut short. B / Back goes up a folder, and closes from the storage rows. [start] - * opens in a folder. [onAddAll] adds a row on top of a folder with subfolders. - */ @Composable internal fun ExePickerDialog( title: String, kind: PickKind = PickKind.EXE, start: File? = null, @@ -165,7 +151,6 @@ internal fun ExePickerDialog( } } -/** A compact dialog's title line: [leading] (a back arrow), the title, a small red X. */ @Composable internal fun DialogTitleRow(title: String, onClose: () -> Unit, leading: (@Composable () -> Unit)? = null) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) { @@ -175,7 +160,6 @@ internal fun DialogTitleRow(title: String, onClose: () -> Unit, leading: (@Compo } } -/** A 32 dp icon button for a dialog's title line. */ @Composable internal fun SmallIconButton(icon: ImageVector, description: String, tint: androidx.compose.ui.graphics.Color, onClick: () -> Unit) { val src = remember { MutableInteractionSource() } @@ -193,7 +177,6 @@ internal fun SmallIconButton(icon: ImageVector, description: String, tint: andro } } -/** One row of a compact dialog's list, the Install dialog's style: [content] laid out in a row. */ @Composable internal fun ListRow( enabled: Boolean = true, accent: Boolean = false, tall: Boolean = false, focus: FocusRequester? = null, @@ -217,7 +200,6 @@ internal fun ListRow( ) { content(hot) } } -/** A list row with an icon, a name (cut short when long) and a detail on the right. */ @Composable internal fun PickerRow( icon: ImageVector?, name: String, detail: String?, tall: Boolean = false, file: Boolean = false, accent: Boolean = false, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FitText.kt b/app/src/main/java/com/droiddeck/launcher/ui/FitText.kt index 77bdbcada..5935931da 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FitText.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FitText.kt @@ -16,12 +16,6 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp -/** - * One line of text in a slot of fixed width - a rail label, a key cap - set as large as - * [maxSize] when it fits and smaller, down to [minSize], when it does not. Labels are as long as - * their language makes them ("Components" is "コンポーネント", "Componentes", "구성 요소"), so a - * fixed size cuts some of them off; below [minSize] the end is ellipsized rather than clipped. - */ @Composable fun FitText( text: String, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FocusGlide.kt b/app/src/main/java/com/droiddeck/launcher/ui/FocusGlide.kt index 9f8f0b0fe..0bc1ccc47 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FocusGlide.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FocusGlide.kt @@ -48,27 +48,19 @@ import kotlin.math.abs import kotlin.math.hypot import kotlinx.coroutines.coroutineScope -// One focus ring for a whole screen that slides from control to control. When focus moves, the -// edge on the side it is heading springs ahead and the trailing edge catches up a beat later, so -// the ring stretches like a drop and snaps into the new control's shape. -/** A control that can show the ring: where it is and what its ring looks like there. */ internal class GlideSource { var coords: LayoutCoordinates? = null var shape: Shape by mutableStateOf<Shape>(androidx.compose.ui.graphics.RectangleShape) var color by mutableStateOf(Color.Transparent) var width by mutableStateOf(2.dp) - /** Focuses the control again: a step-out hands focus back to the control it came out of. */ val requester = androidx.compose.ui.focus.FocusRequester() } internal class FocusGlide(val view: View) { var host: LayoutCoordinates? = null - /** Controls that are hot, the latest last: focus and a hovering pointer can each hold one. */ val hot = mutableStateListOf<GlideSource>() - /** Bumped when the current control moves (a scroll, a relayout). */ var moves by mutableIntStateOf(0) - /** Off while a page floods in or out (PageFlood); back on, it appears on whatever has focus. */ var hidden by mutableStateOf(false) val current: GlideSource? get() = hot.lastOrNull() @@ -77,7 +69,6 @@ internal class FocusGlide(val view: View) { fun release(s: GlideSource) { hot.remove(s) } fun moved(s: GlideSource) { if (hot.lastOrNull() === s) moves++ } - /** [s]'s bounds in the host, cut to what is on screen; null when it is not laid out or is scrolled away. */ fun boundsOf(s: GlideSource): Rect? { val h = host ?: return null val c = s.coords ?: return null @@ -89,12 +80,10 @@ internal class FocusGlide(val view: View) { internal val LocalFocusGlide = staticCompositionLocalOf<FocusGlide?> { null } -/** Floods on screen (a launch, a stop, the return from one): every ring keeps out of the way of them. */ internal object RingVeil { var count by mutableIntStateOf(0) } -/** Keeps the focus ring off while this is in the composition. */ @Composable internal fun VeilRing() { DisposableEffect(Unit) { @@ -110,11 +99,6 @@ private fun cornerOf(shape: Shape, size: Size, dir: LayoutDirection, density: De is Outline.Generic -> minOf(size.width, size.height) / 2f } -/** - * A control's outline: [restColor] at [restWidth] always, and [hotColor] at [hotWidth] while [hot]. - * Under a [FocusGlideHost] the hot outline is the host's one sliding ring; elsewhere (a dialog, a - * popup, a screen with no host) it is drawn here, as a plain border. - */ @Composable internal fun Modifier.glideBorder( hot: Boolean, @@ -140,54 +124,34 @@ internal fun Modifier.glideBorder( return base.focusRequester(src.requester).onGloballyPositioned { src.coords = it; glide.moved(src) } } -/** Leading edge on a focus move: quick and a little loose. */ -/** A spring an edge chases its target on: damping ratio and stiffness, as Compose's. */ private class Edge(val damping: Float, val stiffness: Float) private fun lead() = Edge(0.62f, 700f) -/** Trailing edge: slower, settles without a bounce to speak of. */ private fun trail() = Edge(0.78f, 360f) -/** Following a control that moves under the ring (a scroll): tight, no wobble. */ private fun follow() = Edge(1f, 2400f) -/** How long after a focus move the ring keeps its stretchy springs and keeps checking where it is. */ private const val SETTLE_MS = 450L -/** How long the control must stand still before the ring stops checking where it is. */ private const val STILL_MS = 120L -/** The longest the ring keeps checking after a move, for a control that never stands still. */ private const val TRACK_MAX_MS = 2_000L -/** How strongly the ring tints what it holds. */ private const val RING_FILL = 0.14f -/** Moves closer together than this are a held direction: the ring runs along as one, no stretch. */ private const val REPEAT_MS = 180L -/** A move further than this goes as a droplet: pinched to a drop, carried across, opened onto the control. */ private val FAR = 360.dp -/** - * Hosts the sliding focus ring for everything inside it. The ring is drawn over [content], in the - * host's space, so put the host inside anything that moves as a whole (a sheet that slides in). - */ @Composable internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable () -> Unit) { val view = LocalView.current val density = LocalDensity.current val dir = LocalLayoutDirection.current val glide = remember(view) { FocusGlide(view) } - val edges = remember { List(4) { Animatable(0f) } } // left, top, right, bottom + val edges = remember { List(4) { Animatable(0f) } } val corner = remember { Animatable(0f) } - /** How solid the ring is: 1 while it travels as a drop, 0 (an outline) on a control. */ val solid = remember { Animatable(0f) } val alpha = remember { Animatable(0f) } val scope = rememberCoroutineScope() - // An edge each, the corner, a droplet carrying all of them, its solid fill, and the chase. val jobs = remember { arrayOfNulls<Job>(8) } - // The chase: each edge on a spring of its own toward where focus is now. A new target only - // moves where it is heading - restarting an animation on every focus move started it from - // its first frame (which does not move), so a held direction over a busy page (each rail item - // swaps the page) left the ring standing while focus ran on ahead. val target = remember { FloatArray(4) } val vel = remember { FloatArray(4) } val springs = remember { Array(4) { follow() } } @@ -206,15 +170,12 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable var delays = LongArray(4) var lastMove = 0L fun go(i: Int, v: Float, spec: Edge, wait: Long) { - // A drop cut short by another move turns back into an outline on the way. if (jobs[5]?.isActive == true || solid.targetValue > 0f) { jobs[5]?.cancel() jobs[6]?.cancel() jobs[6] = scope.launch { solid.animateTo(0f, Motion.tw(120)) } } val chasing = jobs[7]?.isActive == true - // An edge some other animation was moving (the appear, a drop's opening) is taken over - // at the speed it was going. jobs[i]?.let { if (it.isActive) { it.cancel(); vel[i] = edges[i].velocity } } if (!chasing) for (j in 0..3) if (jobs[j]?.isActive != true) vel[j] = edges[j].velocity target[i] = v @@ -225,7 +186,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable var last = withFrameNanos { it } while (true) { val frame = withFrameNanos { it } - // Real time, however long the frame took, in small steps so a stiff spring holds. val dt = ((frame - last) / 1e9f).coerceAtMost(0.1f) / Motion.scale.coerceAtLeast(0.01f) last = frame val now = System.nanoTime() @@ -251,7 +211,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable } } } - /** Pinch into a solid dot where it is, carry it over, and open it into [b] as it arrives. */ fun droplet(s: GlideSource, b: Rect) { jobs.forEach { it?.cancel() } val size = s.coords?.size?.let { Size(it.width.toFloat(), it.height.toFloat()) } ?: b.size @@ -260,19 +219,15 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable val r = with(density) { 6.dp.toPx() } fun dot(c: Offset) = listOf(c.x - r, c.y - r, c.x + r, c.y + r) val here = Offset((edges[0].value + edges[2].value) / 2f, (edges[1].value + edges[3].value) / 2f) - // Pinch into a solid drop where it is. coroutineScope { launch { solid.animateTo(1f, Motion.tw(80)) } launch { corner.animateTo(r, Motion.tw(90)) } dot(here).forEachIndexed { i, v -> launch { edges[i].animateTo(v, Motion.tw(90)) } } } - // Across as a dot, every edge together, on a timed ease, steering each frame for - // where the control is now: one still rising into place is met where it ends up. val distance = hypot(b.center.x - here.x, b.center.y - here.y) / density.density val travel = (180 + distance * 0.12f).toInt().coerceIn(220, 380) val p = Animatable(0f) val flight = launch { p.animateTo(1f, Motion.tw(travel, easing = FastOutSlowInEasing)) } - // Takes the control's shape as it arrives, not after: no resting as a dot. while (flight.isActive && p.value < 0.78f) { val to = (glide.boundsOf(s) ?: b).center dot(Offset(here.x + (to.x - here.x) * p.value, here.y + (to.y - here.y) * p.value)).forEachIndexed { i, v -> edges[i].snapTo(v) } @@ -316,8 +271,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable return@collectLatest } if (src == null) { - // Focus passing between two controls can let go of one a moment before the next - // takes it: wait a beat, so that reads as a move and not a fade out and back in. delay(60) jobs.forEach { it?.cancel() } solid.snapTo(0f) @@ -342,7 +295,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable shown = src first = false if (!wasShowing) { - // Appearing: a hair bigger, pulled in onto the control as it fades up. val out = with(density) { 3.dp.toPx() } val grown = listOf(b.left - out, b.top - out, b.right + out, b.bottom + out) jobs.forEach { it?.cancel() } @@ -352,7 +304,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable cornerTo(src, b, snap = true) scope.launch { alpha.animateTo(1f, Motion.tw(140)) } } else { - // Moving: the edges on the side it is heading lead. val dx = b.center.x - (edges[0].value + edges[2].value) / 2f val dy = b.center.y - (edges[1].value + edges[3].value) / 2f val horizontal = abs(dx) >= abs(dy) @@ -365,7 +316,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable dropped = !held && hypot(dx, dy) > with(density) { FAR.toPx() } if (dropped) droplet(src, b) else { - // A held direction runs along as one piece; a single press stretches. dirSpecs = if (held) List(4) { Edge(0.9f, 900f) } else List(4) { i -> when (i) { leadIdx -> lead(); trailIdx -> trail(); else -> Edge(0.7f, 520f) } } delays = LongArray(4) { i -> if (i == trailIdx && !held) 30L else 0L } @@ -376,15 +326,11 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable if (!dropped) listOf(b.left, b.top, b.right, b.bottom).forEachIndexed { i, v -> go(i, v, dirSpecs[i], delays[i]) } last = b } else if (b != last && jobs[5]?.isActive != true) { - // It moved under the ring: keep the stretch while the move is fresh, else follow. val fresh = (System.nanoTime() - movedAt) / 1_000_000 < SETTLE_MS listOf(b.left, b.top, b.right, b.bottom).forEachIndexed { i, v -> go(i, v, if (fresh) dirSpecs[i] else follow(), 0L) } if (last == null) scope.launch { alpha.animateTo(1f, Motion.tw(120)) } last = b } - // Layer animations (a tile growing, a row sliding, a page rising in) move it without - // a relayout, so look again each frame until it has stood still a moment: a fixed - // window let the ring park mid-rise and jump over when something next laid out. val now = System.nanoTime() val settled = (now - start) / 1_000_000 > SETTLE_MS && (now - stillSince) / 1_000_000 > STILL_MS if (settled || (now - start) / 1_000_000 > TRACK_MAX_MS) break @@ -411,8 +357,6 @@ internal fun FocusGlideHost(modifier: Modifier = Modifier, content: @Composable cornerRadius = CornerRadius(corner.value), alpha = a * f, ) if (f < 0.99f) { - // The ring's own infill: the only fill a pad sees, so it is always exactly what - // the ring holds and moves with it (a row's own sliding fill trailed it). drawRoundRect( color, topLeft = Offset(l + w / 2f, t + w / 2f), size = Size(sw, sh), cornerRadius = CornerRadius(rad), alpha = a * (1f - f) * RING_FILL, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrameGenMenu.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrameGenMenu.kt index 1ec0fbfad..816177bc1 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrameGenMenu.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrameGenMenu.kt @@ -8,11 +8,6 @@ import com.droiddeck.launcher.R import com.droiddeck.launcher.gpu.FrameGen import com.droiddeck.launcher.gpu.Lossless -/** - * The frame generation picker, anchored to whatever opened it (host key "fg"). One copy for Setup, - * a game's launch settings and the session drawer, so they say the same thing. Importing a - * Lossless.dll is offered once the account is known to own Lossless Scaling. - */ @Composable internal fun FrameGenMenu( host: MenuHost, mode: FrameGen.Mode, lossless: Lossless.State, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndArt.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndArt.kt index 992e337a6..85386ca93 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndArt.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndArt.kt @@ -59,13 +59,10 @@ import java.io.File import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.onGloballyPositioned -// Cover art: posters, the art grid and its tiles. -/** Where each art tile sits, root px, by its key: a page opened from one floods out of it. */ internal object TileBounds { val at = HashMap<String, androidx.compose.ui.geometry.Rect>() - /** The page about to open came from tile [key] (its art or icon for the flood), if it is on screen. */ fun mark(key: String, art: Any?, corner: Float) { at[key]?.let { PageOrigin.mark(Origin(it, corner, art = art)) } } @@ -97,12 +94,8 @@ internal fun Poster(art: File?, name: String, modifier: Modifier) { @Composable internal fun ArtGrid(tiles: List<Tile>, wide: Boolean = false) { val square = tiles.isNotEmpty() && tiles.all { it.art == null && (it.iconRes != null || it.iconBitmap != null) } - // Big enough to recognise a game by its art and read its name under it; icon tiles are squares. val minSize = if (square) 76.dp else if (wide) 120.dp else 96.dp val gap = 12.dp - // Laid out whole, not lazily: the pad's focus search only finds tiles that exist, and a lazy - // grid composes only the rows on screen, so a press towards the next row bounced back among - // the visible tiles. A few hundred tiles lay out fine; the page's scroll follows the focused one. BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { val avail = maxWidth - 8.dp val cols = ((avail + gap) / (minSize + gap)).toInt().coerceAtLeast(1) @@ -152,10 +145,6 @@ private fun GameTile(t: Tile, wide: Boolean, square: Boolean, src: MutableIntera } } -/** - * Art for a portrait (2:3) tile. Box art fills it; wide art - a PS3 disc's ICON0, a game's header - - * is shown whole over a blurred, darkened copy of itself instead of losing its sides to the crop. - */ @Composable private fun CoverImage(art: File, modifier: Modifier) { var wideArt by remember(art) { mutableStateOf(false) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt index 764967860..db6850226 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndContent.kt @@ -99,7 +99,6 @@ import com.droiddeck.launcher.runtime.UserApps import com.droiddeck.launcher.store.UserAppsState import java.io.File -// The front end's right-hand pane and the page it shows: backdrop, desktop card and emulators. @OptIn(androidx.compose.animation.ExperimentalAnimationApi::class) @Composable @@ -119,35 +118,25 @@ internal fun Pane( } Backdrop(backdropArt) val pal = LocalPalette.current - // Pages keep drawing while they leave, so one can fold away instead of blinking out. val pages = remember { HashMap<String, @Composable () -> Unit>() } - // Pages opened from a control (the cog), with where that control sat, in this pane's coordinates. val origins = remember { HashMap<String, Origin>() } val paneAt = remember { arrayOf(Offset.Zero) } val livePage by rememberUpdatedState(s.pageKey) - // Picking another game changes the detail beside the list, not the whole page. val railContent = if (selected.startsWith("app:")) "games" else selected val target = if (page != null && s.pageKey != null) s.pageKey else railContent - // The game the Games page shows. It must outlive the selection: while the page animates out, - // drawing it with what is selected now (another rail item) turned it into a copy of that page. val gameShown = remember { arrayOf("games") } if (selected == "games" || selected.startsWith("app:")) gameShown[0] = selected - // What the pane showed before this change: the content a page opened over belongs to it. val before = remember { arrayOf(target) } if (page != null && s.pageKey != null) { pages[s.pageKey] = page if (s.pageKey !in origins) PageOrigin.take()?.let { o -> - // A hop's page came out of its own rail item, and is that item's content. origins[s.pageKey] = inPane(o.translate(-paneAt[0])).let { if (it.owner == null) it.ownedBy(railContent) else it } } } else if (target != before[0] && target !in origins) { - // A section's own page opened from a control in it (an emulator's card, a ROM's) floods - // out of that control and belongs to what was showing; a hop's comes out of its rail item. PageOrigin.take()?.let { o -> origins[target] = inPane(o.translate(-paneAt[0])).let { if (it.owner == null) it.ownedBy(before[0]) else it } } } before[0] = target val targetNow by rememberUpdatedState(target) - // A page leaving through a hop's link is not coming back to its control. val leave = if (Hops.linking) PageLeave.Link else PageLeave.Ordinary LaunchedEffect(target) { Hops.linking = false } AnimatedContent( @@ -155,16 +144,13 @@ internal fun Pane( modifier = Modifier.onGloballyPositioned { paneAt[0] = it.positionInRoot() }, transitionSpec = { when { - // Into a page from its control: the control floods the pane (PageFlood) as it sinks back. targetState in origins -> (fadeIn(Motion.tw(120))) .togetherWith(fadeOut(Motion.tw(320, 80)) + scaleOut(Motion.tw(480), targetScale = 0.95f)) .apply { targetContentZIndex = 1f } - // Back out of it: the flood draws back into the control as the pane comes forward again. initialState in origins && pageExit(origins[initialState]?.owner, targetState, leave) == PageExit.Drain -> (fadeIn(Motion.tw(300, 100)) + scaleIn(Motion.tw(460, 40), initialScale = 0.95f)) .togetherWith(ExitTransition.None) .apply { targetContentZIndex = -1f } - // Away to something else: the control is not coming back, so the page sinks with the pane. initialState in origins -> (fadeIn(Motion.tw(300, 80)) + slideInVertically(Motion.tw(420, 80)) { it / 24 }) .togetherWith(fadeOut(Motion.tw(170)) + scaleOut(Motion.tw(170), targetScale = 0.95f)) .apply { targetContentZIndex = 1f } @@ -177,8 +163,6 @@ internal fun Pane( ) { key -> val drawn = (if (page != null && key == s.pageKey) page else pages[key]) ?: { Content(s, if (key == "games") gameShown[0] else key, a, Modifier.fillMaxSize(), onSelect, onAndroidAppClick, onOpenDeveloperOptions, onRequestWirelessAdb) } - // A hop leaving this content: it sinks back as the dot leaves it (and comes forward - // again, freshly drawn, when the hop is undone). val hopSink by androidx.compose.animation.core.animateFloatAsState( if (Hops.sinking == key) 1f else 0f, Motion.tw(260), label = "hopSink", ) @@ -190,16 +174,13 @@ internal fun Pane( val from = origins[key] if (from == null) shown() else { - // The control's own picture (a card's art, an emulator's icon) on the tile as it grows. val art = when (val m = from.art) { null -> null is Int -> androidx.compose.ui.res.painterResource(m) else -> coil.compose.rememberAsyncImagePainter(m) } val leaving = transition.targetState == EnterExitState.PostExit - // Settled as the page starts to leave, from what is coming in, not from where it came in. val drains = remember(leaving) { !leaving || pageExit(from.owner, target, leave) == PageExit.Drain } - // Keeps a leaving page on screen while its flood draws back into the control. transition.animateFloat( transitionSpec = { if (targetState == EnterExitState.PostExit && drains) Motion.tw(PAGE_RETURN_MS) else snap() }, label = "pageReturn", @@ -212,11 +193,6 @@ internal fun Pane( } } -/** - * An origin left of the pane (a rail item a hop flew to) starts half inside it, at the item's - * height: the page swells out of the pane's edge beside the item. Wholly outside, its tile was a - * bar that raced across from the far edge. - */ internal fun inPane(o: Origin): Origin = if (o.bounds.right > 0f) o else o.translate(Offset(-o.bounds.right + o.bounds.width / 2f, 0f)) @@ -225,8 +201,6 @@ private fun Backdrop(art: File?) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current Crossfade(targetState = art, animationSpec = Motion.tw(700), label = "backdrop") { a -> - // A game's own art, blurred, where it has some; otherwise the plain ground with one quiet - // glow of the theme's signal colour - a colour per title hash read as noise. if (a != null && Build.VERSION.SDK_INT >= 31) { Box(modifier = Modifier.fillMaxSize().alpha(0.26f).blur(70.dp)) { AsyncImage(model = a, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize().graphicsLayer { scaleX = 1.5f; scaleY = 1.5f }) @@ -257,7 +231,6 @@ private fun Content( val density = androidx.compose.ui.platform.LocalDensity.current.density val padH = if (narrow) 16.dp else 22.dp val padV = if (narrow) 12.dp else 18.dp - // Setup scrolls inside itself, under its tabs. if (selected == "setup") { Column(modifier = modifier.padding(horizontal = padH, vertical = padV)) { SetupPanel(s, a, onOpenDeveloperOptions, onRequestWirelessAdb) @@ -268,33 +241,26 @@ private fun Content( UpdatesPage(s, a, modifier.padding(horizontal = padH, vertical = padV)) return } - // Steam is a full-bleed wall of the library with Play over it. if (selected == "steam") { SteamHome(s, a, modifier) return } - // The Store scrolls and navigates inside itself (its tabs and an app's page). if (selected == "store") { StorePage(s, a, modifier) return } - // Stores (GOG, Epic, Amazon) takes the whole pane: chips, shelves and a game's page inside it. if (selected == "stores") { StoresPage(s, a, modifier) return } - // The Games tab lays out its own list and detail. if (selected == "games" || selected.startsWith("app:")) { GamesPage(s, a, selected, onSelect, modifier) return } - // Desktop is the Steam tab's shape: a hero with its lifted sheet, and shelves under it. if (selected == "desktop") { DesktopPage(s, a, modifier, onSelect) return } - // Every other page scrolls as one - header, hero and grid - so a short screen reaches the grid - // instead of showing a sliver of it under a fixed hero. A focused tile scrolls itself into view. Column(modifier = modifier.verticalScroll(rememberScrollState()).padding(horizontal = padH, vertical = padV)) { when { selected == "android-apps" && s.isHomeApp -> { @@ -322,7 +288,6 @@ private fun Content( if (e == null) Note(stringResource(R.string.content_not_installed)) else { val pkgId = Library.packageId(e.id) val pkg = pkgId?.let { id -> s.packages?.firstOrNull { it.id == id } } - // Out of its card on Desktop, with the card's icon; B draws it back in. Rise(1) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -367,8 +332,6 @@ private fun Content( } else Rise(5, Modifier.fillMaxWidth()) { Column { - // RetroArch's games come from many systems: chips filter them, one - // selection gliding between them. val systems = remember(e.games) { e.games.map { romSystem(it) }.distinct() } var system by rememberSaveable(e.id) { mutableStateOf<String?>(null) } if (systems.size > 1) Box(Modifier.padding(top = 10.dp)) { @@ -467,7 +430,6 @@ internal fun UserApps.Kind.label() = when (this) { UserApps.Kind.FLATPAK -> R.string.user_apps_kind_flatpak } -/** An added app's icon: its own, or the kind's when it has none. */ @Composable private fun UserAppIcon(app: UserApps.App, size: Int) { if (app.icon != null) AppIcon(app.icon, app.name, size) @@ -482,7 +444,6 @@ private fun UserAppIcon(app: UserApps.App, size: Int) { } } -/** The add or remove under way, or why the last one failed. */ @Composable private fun UserAppsProgress() { val colors = MaterialTheme.colorScheme @@ -501,7 +462,6 @@ private fun UserAppsProgress() { } } -/** An added app's page: open it, change its name and icon, update one added from GitHub, or remove it (asked first). */ @Composable private fun UserAppPage(app: UserApps.App, s: FrontEndState, a: FrontEndActions, onSelect: (String) -> Unit) { val colors = MaterialTheme.colorScheme @@ -556,7 +516,6 @@ private fun UserAppPage(app: UserApps.App, s: FrontEndState, a: FrontEndActions, } } -/** Remove, which asks first: are you sure steps out of it in red, Keep first and focused. */ @Composable private fun RemoveAppButton(app: UserApps.App, enabled: Boolean, onRemove: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -581,7 +540,6 @@ private fun RemoveAppButton(app: UserApps.App, enabled: Boolean, onRemove: () -> } } -/** Looks up a newer release of an app added from GitHub, then installs it. */ @Composable private fun UpdateButton(app: UserApps.App, s: FrontEndState) { val ctx = LocalContext.current @@ -604,13 +562,8 @@ private fun UpdateButton(app: UserApps.App, s: FrontEndState) { } } -/** - * One emulator: its icon, system and games. One not yet installed says so in words and keeps full - * contrast - dimming it read as disabled rather than one press from installing. - */ @Composable private fun systemLabel(e: Library.Emulator): String = if (e.systemRes != 0) stringResource(e.systemRes) else e.system.replaceFirstChar { it.uppercase() } -/** The system a ROM belongs to: the folder it sits in under the ROMs folder. */ private fun romSystem(rom: Library.Rom): String = rom.hostPath.parentFile?.name.orEmpty() diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt index 0acb72b5d..907f9d5f9 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndGames.kt @@ -61,12 +61,7 @@ import coil.compose.AsyncImage import com.droiddeck.launcher.frontend.Library import kotlinx.coroutines.launch -// The Games page: the list, the hero for the selected game and its labels. -/** - * The Games tab: installed Steam games down the left, most recently played first, and the one - * picked beside them - its banner, Launch and the launch settings. One game skips the list. - */ @Composable internal fun GamesPage(s: FrontEndState, a: FrontEndActions, selected: String, onSelect: (String) -> Unit, modifier: Modifier) { val games = remember(s.steamGames) { s.steamGames.sortedByDescending { it.lastPlayed } } @@ -155,11 +150,9 @@ private fun GameActions(g: Library.SteamGame, s: FrontEndState, a: FrontEndActio } g.protonPrefix?.takeIf { it.isDirectory }?.let { dir -> SecondaryButton(stringResource(R.string.games_prefix), compact = true) { a.onBrowseFiles(dir) } - // Only games added to the library; Steam titles keep their saves with Steam Cloud. if (g.library == Library.ADDED) ManageSaves(g, dir, a) } if (!s.shortcutPicker) GameShortcutMenu(g, a) - // A game added in DroidDeck (Custom): its editor, Steam's Properties for a non-Steam game. if (!s.shortcutPicker && g.library == Library.ADDED && g.source == Library.ADDED && g.gameFiles != null) EditGameButton { onEdit(g) } BusyChip(s) } @@ -184,10 +177,6 @@ private fun GameShortcutMenu(g: Library.SteamGame, a: FrontEndActions) { } } -/** - * Manage saves: import a GameHub or Winlator save zip into this game's prefix, export its saves as - * either, or open the save folder found for it. The saves are looked for when the menu opens. - */ @Composable private fun ManageSaves(g: Library.SteamGame, prefix: java.io.File, a: FrontEndActions) { var open by remember(g.gameId) { androidx.compose.runtime.mutableStateOf(false) } @@ -211,7 +200,6 @@ private fun ManageSaves(g: Library.SteamGame, prefix: java.io.File, a: FrontEndA } } Box { - // A GOG or Epic game's saves are its cloud saves: the one place for them, named so. val cloudGame = com.droiddeck.launcher.stores.Store.byId(g.source).let { it == com.droiddeck.launcher.stores.Store.GOG || it == com.droiddeck.launcher.stores.Store.EPIC } val label = stringResource(if (cloudGame) R.string.store_cloud_saves else R.string.games_manage_saves) SecondaryButton(label, compact = true) { open = !open } @@ -235,12 +223,6 @@ private fun ManageSaves(g: Library.SteamGame, prefix: java.io.File, a: FrontEndA } } -/** - * A GOG or Epic game's cloud saves: the Cloud saves switch, Upload and Download - three rows from - * the first frame, so nothing moves under the pad while the cloud is checked. Until the check is - * back the two actions are disabled and read "…"; then only their subtitles change: the last sync, - * "No cloud saves", or a conflict, when Upload and Download become Keep local and Keep cloud. - */ @Composable private fun CloudRows(g: Library.SteamGame, open: Boolean, first: androidx.compose.ui.focus.FocusRequester) { val store = com.droiddeck.launcher.stores.Store.byId(g.source)?.takeIf { it != com.droiddeck.launcher.stores.Store.AMAZON } ?: return @@ -253,7 +235,6 @@ private fun CloudRows(g: Library.SteamGame, open: Boolean, first: androidx.compo runCatching { com.droiddeck.launcher.stores.CloudSaves.status(context, store, id) }.getOrNull() } } - // The game's own switch (sidecar "cloud"), known locally: right from the first frame. val folder = g.gameFiles var enabled by remember(g.gameId, open) { androidx.compose.runtime.mutableStateOf(folder?.let { com.droiddeck.launcher.stores.StoreGameSidecar.read(it)?.cloud } ?: true) } val st = status @@ -285,17 +266,14 @@ private fun CloudRows(g: Library.SteamGame, open: Boolean, first: androidx.compo st.lastSync > 0 -> stringResource(R.string.cloud_last_sync, android.text.format.DateUtils.getRelativeTimeSpanString(st.lastSync).toString()) else -> stringResource(R.string.cloud_never) } - // A conflict turns the two actions into its two answers; the rows stay where they are. MenuItem(stringResource(if (conflict) R.string.cloud_keep_local else R.string.cloud_upload), checked = false, enabled = ready, detail = summary) { sync(up = true, force = conflict) } MenuItem(stringResource(if (conflict) R.string.cloud_keep_cloud else R.string.cloud_download), checked = false, enabled = ready, detail = if (st == null || busy != null) pending else null) { sync(up = false, force = conflict) } } -/** When it was last played (or where it is, if never) over its name, then room for Launch. */ @Composable private fun ColumnScope.GameHeroCopy(g: Library.SteamGame, titleSize: androidx.compose.ui.unit.TextUnit) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { SourceChip(g.source) - // A Custom game's chip already says what its eyebrow would; the line then stays empty. val eyebrow = lastPlayedText(g.lastPlayed) ?: libraryLabel(g) if (eyebrow.isNotEmpty()) Text( eyebrow.uppercase(), @@ -316,14 +294,11 @@ private fun GameList( onSelect: (Library.SteamGame) -> Unit, onLaunch: (Library.SteamGame) -> Unit, onAdd: (() -> Unit)?, modifier: Modifier, ) { val colors = MaterialTheme.colorScheme - // Laid out whole, as the art grid is: the pad's focus search only finds rows that exist. Column( verticalArrangement = Arrangement.spacedBy(2.dp), modifier = modifier.verticalScroll(rememberScrollState()).padding(start = 12.dp, end = 10.dp, top = 16.dp, bottom = 16.dp), ) { GamesHeader(games.size, onAdd, Modifier.padding(start = 6.dp, bottom = 10.dp)) - // A game that arrives while the list is up (Steam installed it in the session that just - // ended, or it was added) eases in; the ones there when the page opened are simply there. val seen = remember { HashSet<Int>() } var settled by remember { androidx.compose.runtime.mutableStateOf(false) } for (g in games) key(g.appId) { @@ -338,7 +313,6 @@ private fun GameList( } } -/** "Games N", and the + that adds a game from its .exe beside it ([onAdd] null: no +). */ @Composable private fun GamesHeader(count: Int, onAdd: (() -> Unit)?, modifier: Modifier) { val colors = MaterialTheme.colorScheme @@ -349,17 +323,11 @@ private fun GamesHeader(count: Int, onAdd: (() -> Unit)?, modifier: Modifier) { } } -/** - * The Games tab's dialogs, one at a time: the + picker, the summary after "Add all games in this - * folder", and a game's editor (from the page's ✎ or a summary row, with back to the summary). - */ internal object GamesDialogState { var adding by androidx.compose.runtime.mutableStateOf(false) var summary by androidx.compose.runtime.mutableStateOf<Pair<List<com.droiddeck.launcher.frontend.AddedGames.Game>, Int>?>(null) var editing by androidx.compose.runtime.mutableStateOf<EditRequest?>(null) - /** The subfolder "Add all games in this folder" is looking at; null when it is done. */ var checking by androidx.compose.runtime.mutableStateOf<String?>(null) - /** The summary an editor opened from it goes back to. */ var behindEditor: Pair<List<com.droiddeck.launcher.frontend.AddedGames.Game>, Int>? = null } @@ -409,7 +377,6 @@ private fun GamesDialogs(a: FrontEndActions) { } } -/** The ✎ on a Custom game's page: icon only. */ @Composable private fun EditGameButton(onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -430,7 +397,6 @@ private fun EditGameButton(onClick: () -> Unit) { } } -/** A round + that opens the .exe picker. */ @Composable private fun AddGameButton(onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -452,7 +418,6 @@ private fun AddGameButton(onClick: () -> Unit) { } } -/** Fades and slides a row in once when [arriving]; nothing for one already there (or with animations off). */ @Composable internal fun Modifier.arriveIn(arriving: Boolean): Modifier { val t = remember { androidx.compose.animation.core.Animatable(if (arriving) 0f else 1f) } @@ -460,7 +425,6 @@ internal fun Modifier.arriveIn(arriving: Boolean): Modifier { return graphicsLayer { alpha = t.value; translationY = (1f - t.value) * 12.dp.toPx() } } -/** One game in the list. Moving onto it with the pad shows it; A launches it, a tap only shows it. */ @Composable private fun GameRow(g: Library.SteamGame, selected: Boolean, onSelect: () -> Unit, onLaunch: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -484,7 +448,6 @@ private fun GameRow(g: Library.SteamGame, selected: Boolean, onSelect: () -> Uni } Column(modifier = Modifier.weight(1f)) { Text(g.name, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis) - // Where it came from beside when it was played: a store's games sit among Steam's own. Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { SourceChip(g.source, small = true) Text( @@ -496,7 +459,6 @@ private fun GameRow(g: Library.SteamGame, selected: Boolean, onSelect: () -> Uni } } -/** A page's title, with room at its right for controls or status. */ @Composable internal fun PageHeader(title: String, trailing: @Composable RowScope.() -> Unit = {}) { Row( @@ -511,13 +473,11 @@ internal fun PageHeader(title: String, trailing: @Composable RowScope.() -> Unit } } -/** Why a launch button is greyed out: the runtime is being worked on. Nothing when it is not. */ @Composable internal fun BusyChip(s: FrontEndState) { if (s.busy) ActionChip(if (s.percent >= 0) stringResource(R.string.games_runtime_busy_percent, s.percent) else s.stage, ok = false) } -/** Whether Steam can start, said where Play is rather than only in Setup. */ @Composable internal fun RuntimeChip(s: FrontEndState) = when { s.busy -> Chip(if (s.percent >= 0) stringResource(R.string.setup_runtime_progress, s.stage, s.percent) else s.stage, ok = false) @@ -527,12 +487,10 @@ internal fun RuntimeChip(s: FrontEndState) = when { else -> Chip(stringResource(R.string.games_runtime_ready), ok = true) } -/** "Last played 3 days ago" from Steam's unix seconds; null for a game never played. */ @Composable private fun lastPlayedText(lastPlayed: Long): String? = playedSpan(lastPlayed)?.let { stringResource(R.string.games_last_played, it.replaceFirstChar { c -> c.lowercase() }) } -/** "3 days ago" from Steam's unix seconds; null for a game never played. */ private fun playedSpan(lastPlayed: Long): String? { if (lastPlayed <= 0L) return null return android.text.format.DateUtils.getRelativeTimeSpanString( @@ -540,10 +498,6 @@ private fun playedSpan(lastPlayed: Long): String? { ).toString() } -/** - * Where a never-played game lives, beside its source chip: Steam's library, the Games storage for - * a store install (the chip names the store), "Custom game" for a folder the user added. - */ @Composable private fun libraryLabel(g: Library.SteamGame): String = when { g.library != Library.ADDED -> if (g.library == "internal") stringResource(R.string.games_internal) else g.library @@ -551,11 +505,6 @@ private fun libraryLabel(g: Library.SteamGame): String = when { else -> "" } -/** - * A game's wide banner: Steam's hero art where the client cached one, else its capsule blurred to - * fill the width. The copy sits bottom-left over a scrim of the ground colour so it always reads; - * the art takes the banner's size, so a banner given a minimum height grows to fit its copy. - */ @Composable private fun GameHero(g: Library.SteamGame, modifier: Modifier, content: @Composable ColumnScope.() -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndRail.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndRail.kt index c95ae71ae..c5a0c6866 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndRail.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndRail.kt @@ -82,14 +82,9 @@ import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.onGloballyPositioned import com.droiddeck.launcher.core.PhantomProcessLimit -// The front end's side rail: the page list and the resume entry under it. internal val AttentionAmber = Color(0xFFFFB547) -/** - * The launcher's sections, always on screen down the left edge: the app is landscape-only, so the - * width a hamburger menu would save is better spent keeping every section one press away. - */ @Composable internal fun SideRail( s: FrontEndState, selected: String, @@ -97,7 +92,6 @@ internal fun SideRail( ) { val colors = MaterialTheme.colorScheme val compact = LocalConfiguration.current.screenHeightDp < 420 - // Under 600dp wide (4:3 and square screens) the labels would cost a fifth of the width. val iconOnly = isNarrowScreen() val setupNeedsAttention = !s.ready || (s.available != null && s.available != s.installed) || PhantomProcessLimit.blocksSteam(s.phantomProcessStatus) @@ -105,11 +99,9 @@ internal fun SideRail( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.width(if (iconOnly) 64.dp else 92.dp).background(colors.surface).padding(vertical = if (compact) 8.dp else 12.dp), ) { - // Every section on screen at once: items shrink (to 44dp, still a touch target) before the rail scrolls. val count = 6 + (if (s.gameStoresEnabled) 1 else 0) + (if (s.isHomeApp) 1 else 0) BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) { val fit = (maxHeight / count - 4.dp).coerceIn(44.dp, if (iconOnly) 48.dp else if (compact) 52.dp else 60.dp) - // A hop in flight moves the selection ahead of the page it is bringing. val shown = Hops.railPreview ?: selected val keys = buildList { add("steam"); add("games") @@ -119,13 +111,10 @@ internal fun SideRail( if (s.isHomeApp) add("android-apps") add("setup"); add("updates") } - // Where each item sits in the column, for the selection that glides between them. val bounds = remember { androidx.compose.runtime.mutableStateMapOf<String, androidx.compose.ui.geometry.Rect>() } val tint = androidx.compose.runtime.rememberUpdatedState(LocalPalette.current.signal) var railAt by remember { mutableStateOf(androidx.compose.ui.geometry.Offset.Zero) } Box(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).onGloballyPositioned { railAt = it.boundsInRoot().topLeft }) { - // One selection for the whole rail, under the items: it glides from item to item as the - // chips' does, its leading edge first, and a jump of more than one item goes as a drop. if (shown in keys) ChipGlide(keys.indexOf(shown), bounds[shown]?.translate(-railAt), tint, GlideAxis.Vertical, corner = 14.dp, fill = 0.14f, outline = 0f) Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -133,22 +122,16 @@ internal fun SideRail( modifier = Modifier.fillMaxWidth(), ) { val place: (String) -> Modifier = { key -> - // In root px: an item that comes and goes sits in its own wrapper, not the column. Modifier.onGloballyPositioned { bounds[key] = it.boundsInRoot() Hops.railBounds[key] = it.boundsInRoot() } } RailItem(stringResource(R.string.rail_steam), Icons.Outlined.SportsEsports, "steam", shown == "steam", compact, iconOnly, fit, place = place, onFocus = { onFocusSelect("steam") }) { onSelect("steam") } - // Always there, so the items below it never move; an empty library says how to fill it. RailItem(stringResource(R.string.rail_games), Icons.Outlined.VideoLibrary, "games", shown == "games", compact, iconOnly, fit, place = place, onFocus = { onFocusSelect("games") }) { onSelect("games") } - // The game stores, right under Games: what they install lands there. Its badge counts - // the downloads in flight, so a download started here is never out of sight. - // A section switched on in Setup comes in as the rail's own entrance; off, it folds away. if (s.gameStoresEnabled) RailItem(stringResource(R.string.rail_stores), Icons.Outlined.LocalMall, "stores", shown == "stores", compact, iconOnly, fit, count = s.storeDownloadsActive, place = place, onFocus = { onFocusSelect("stores") }) { onSelect("stores") } RailItem(stringResource(R.string.rail_desktop), Icons.Outlined.DesktopWindows, "desktop", shown == "desktop", compact, iconOnly, fit, place = place, onFocus = { onFocusSelect("desktop") }) { onSelect("desktop") } RailItem(stringResource(R.string.rail_components), Icons.Outlined.Layers, "components", shown == "components", compact, iconOnly, fit, place = place, onFocus = { onFocusSelect("components") }) { onSelect("components") } - // Home mode's extra section goes last, so it shifts nothing above it. if (s.isHomeApp) RailItem(stringResource(R.string.rail_apps), Icons.Outlined.Apps, "android-apps", shown == "android-apps", compact, iconOnly, fit, place = place, onFocus = { onFocusSelect("android-apps") }) { onSelect("android-apps") } RailItem(stringResource(R.string.rail_setup), Icons.Outlined.Tune, "setup", shown == "setup", compact, iconOnly, fit, badge = setupNeedsAttention, place = place, onFocus = { onFocusSelect("setup") }) { onSelect("setup") } RailItem(stringResource(R.string.rail_updates), Icons.Outlined.SystemUpdate, "updates", shown == "updates", compact, iconOnly, fit, badge = s.updates.hasUpdate, place = place, onFocus = { onFocusSelect("updates") }) { onSelect("updates") } @@ -169,9 +152,7 @@ internal fun SideRail( private fun RailItem( label: String, icon: ImageVector, key: String, current: Boolean, compact: Boolean, iconOnly: Boolean, height: Dp, badge: Boolean = false, - /** A number on the item (downloads in flight); 0 shows nothing. */ count: Int = 0, - /** Reports where the item sits, for the rail's gliding selection and a hop's dot. */ place: (String) -> Modifier = { Modifier }, onFocus: () -> Unit = {}, onClick: () -> Unit, ) { @@ -193,7 +174,6 @@ private fun RailItem( if (current) pal.signal else if (focused || hovered) colors.onBackground else colors.onSurfaceVariant, Motion.tw(220), label = "railFg", ) - // The selected look is the rail's glide under the item, so the item draws only hover here. val fill by animateColorAsState( if (current) Color.Transparent else if (focused || hovered) Color.White.copy(alpha = 0.05f) else Color.Transparent, Motion.tw(220), label = "railFill", @@ -205,7 +185,6 @@ private fun RailItem( .then(if (frontFocus != null) Modifier.focusRequester(frontFocus.railFor(key)) else Modifier) .testTag("rail-$key") .then(place(key)) - // Pops as a hop's dot lands on it. .landingPop(Hops.railLanded[key] ?: 0) .graphicsLayer { scaleX = scale; scaleY = scale } .size(width = if (iconOnly) 52.dp else 80.dp, height = height) @@ -228,14 +207,12 @@ private fun RailItem( if (count > 0) Text( if (count > 9) "9+" else count.toString(), fontSize = 10.sp, fontWeight = FontWeight.Bold, color = pal.onSignal, maxLines = 1, - // Pops with the Downloads chip's count as an install's dot lands there. modifier = Modifier.align(Alignment.TopEnd).padding(top = if (iconOnly) 4.dp else 6.dp, end = if (iconOnly) 5.dp else 12.dp) .landingPop().clip(RoundedCornerShape(8.dp)).background(pal.signal).padding(horizontal = 5.dp, vertical = 1.dp), ) } } -/** The running session, one press from anywhere in the launcher. */ @Composable private fun ResumeRailItem(name: String, compact: Boolean, iconOnly: Boolean, onResume: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -246,7 +223,6 @@ private fun ResumeRailItem(name: String, compact: Boolean, iconOnly: Boolean, on val resumeDescription = stringResource(R.string.rail_resume_named, name) val pulse = rememberInfiniteTransition(label = "pulse") val ringScale by pulse.animateFloat(0.4f, 1.6f, infiniteRepeatable(tween(1600, easing = Motion.Ease), RepeatMode.Restart), label = "ring") - // Animations off: the live dot alone, no pulsing ring. val still = Motion.scale == 0f Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -266,7 +242,6 @@ private fun ResumeRailItem(name: String, compact: Boolean, iconOnly: Boolean, on if (!still) Box(modifier = Modifier.size(14.dp).graphicsLayer { scaleX = ringScale; scaleY = ringScale; alpha = (1.6f - ringScale) / 1.2f }.border(1.5.dp, pal.good, CircleShape)) Box(modifier = Modifier.size(7.dp).clip(CircleShape).background(pal.good)) } - // Icons only: the live dot alone says something is running; its description says what. if (!iconOnly) { Text(stringResource(R.string.resume_session), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, softWrap = false) Text(name, fontSize = 12.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt index 79486e5d8..73212d3f8 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -97,7 +97,6 @@ class FrontEndState( val frameGenLabel: String, val romsDir: String?, val logsEnabled: Boolean, - /** Setup's Debugging tools toggle (AgentAccess): off unless the user turns it on. */ val agentCommands: Boolean = false, val steamGames: List<Library.SteamGame>, val emulators: List<Library.Emulator>, @@ -109,7 +108,6 @@ class FrontEndState( val gameSyncFolder: String? = null, val pageKey: String? = null, val theme: String = Themes.GRAPHITE, - /** The language picked in Setup, or AppLanguage.SYSTEM to follow the system. */ val language: String = "", val appScale: Int = com.droiddeck.launcher.core.AppUiPrefs.DEFAULT_SCALE, val isHomeApp: Boolean = false, @@ -135,19 +133,13 @@ class FrontEndState( val launcherFullscreen: Boolean = true, val animationsEnabled: Boolean = true, val gameStoresEnabled: Boolean = true, - /** Setup › Stores: the download engine's speed tier (stores/download/StoreDownloadTier ids). */ val gameStoresSpeedTier: String = "fast", - /** Setup › Stores: the tab a signed-in store opens on (SessionPrefs.STORES_OPEN_LIBRARY / STORE). */ val storesOpenTab: String = SessionPrefs.STORES_OPEN_LIBRARY, - /** A page to open once: "stores:downloads", or a selection such as "app:<id>" (a notification's tap). */ val navigate: String? = null, - /** Setup: art for added games is fetched; the user has a SteamGridDB key of their own. */ val addedGamesArt: Boolean = true, val sgdbUserKey: Boolean = false, val storesShowMature: Boolean = false, - /** Downloads queued, running or paused across the three stores; the rail item's badge. */ val storeDownloadsActive: Int = 0, - /** The Updates page: DroidDeck's own builds and the channel followed. */ val updates: UpdatesState = UpdatesState(), ) @@ -158,30 +150,21 @@ class FrontEndActions( val onGameShortcut: (Library.SteamGame) -> Unit = {}, val onExportGameFile: (Library.SteamGame) -> Unit = {}, val onSyncGameFiles: () -> Unit = {}, - /** The Games tab's +: the .exe picked in its dialog, to add as a game. */ val onAddGameExe: (path: String) -> Unit = {}, - /** These added games (by folder) were added or edited: the Games tab updates just them. */ val onAddedGamesChanged: (folders: List<String>) -> Unit = {}, - /** An added game removed in its editor: off the Games tab at once, out of Steam after. */ val onAddedGameRemoved: (folder: String, appId: Long?, name: String) -> Unit = { _, _, _ -> }, - /** Setup's Artwork switch and the user's SteamGridDB API key ("" removes it). */ val onAddedGamesArt: (Boolean) -> Unit = {}, val onSgdbKey: (String) -> Unit = {}, val onStopGameFileSync: () -> Unit = {}, val onCopyGameLink: (Library.SteamGame) -> Unit = {}, val onDesktop: () -> Unit, val onEmulator: (Library.Emulator) -> Unit, - /** A Flatpak app by id and name, from the Store. */ val onFlatpakApp: (String, String) -> Unit = { _, _ -> }, - /** An app added on the Desktop page. */ val onUserApp: (com.droiddeck.launcher.runtime.UserApps.App) -> Unit = {}, val onRom: (Library.Rom) -> Unit, val onResume: () -> Unit, - /** Reads Steam's session settings for its lifted sheet (under Play). */ val onSteamSettings: () -> Unit, - /** Reads the desktop's session settings for its lifted sheet (under Open desktop). */ val onDesktopSettings: () -> Unit, - /** A lifted sheet's rows for one tab of [mode]'s settings, as last read. */ val modeSheet: @Composable (mode: String, tab: String, extras: SheetExtras) -> Unit = { _, _, _ -> }, val onRemoveDesktop: () -> Unit = {}, val onInstallPackage: (String) -> Unit, @@ -190,11 +173,8 @@ class FrontEndActions( val onFrameGenPick: (FrameGen.Mode) -> Unit, val onImportLossless: () -> Unit, val onProtons: () -> Unit, - /** The Components page: FEX / DXVK / VKD3D-Proton per Proton. */ val onComponents: (focusContent: Boolean) -> Unit, - /** The Components page on one layer ([GPU_TAB], a component id): a hop's destination. */ val onComponentsTab: (String) -> Unit = {}, - /** A game page's Manage saves: import a save zip into this game, or export its saves in a layout. */ val onSaveImport: (Library.SteamGame) -> Unit = {}, val onSaveExport: (Library.SteamGame, com.droiddeck.launcher.session.GameSaves.Layout) -> Unit = { _, _ -> }, val onPerformance: () -> Unit, @@ -205,7 +185,6 @@ class FrontEndActions( val onAgentCommands: () -> Unit = {}, val onShareLogs: () -> Unit = {}, val onClearLogs: () -> Unit = {}, - /** Setup's Reset all settings: asks first, then puts every setting back to its default. */ val onResetSettings: () -> Unit = {}, val onOffline: () -> Unit, val onPageBack: () -> Unit = {}, @@ -220,7 +199,6 @@ class FrontEndActions( val onStoresOpenTab: (String) -> Unit = {}, val onNavigated: () -> Unit = {}, val onStoresShowMature: (Boolean) -> Unit = {}, - /** The Games list rebuilt now: a store install or removal changed what is on disk. */ val onLibraryChanged: () -> Unit = {}, val onHomeApp: () -> Unit = {}, val onHomeScreen: (Boolean) -> Unit = {}, @@ -243,15 +221,12 @@ class FrontEndActions( internal object Motion { var scale by mutableStateOf(1f) private set - /** App animations can be disabled independently; Android's "Remove animations" always wins. */ fun refresh(context: android.content.Context) { scale = if (SessionPrefs.animationsEnabled(context)) Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) else 0f } val Ease = CubicBezierEasing(0.2f, 0.8f, 0.2f, 1f) - /** A wall-clock wait (a coroutine delay) scaled as the animations around it are. Compose already - * scales its own animation clock by the same setting, so animation specs take unscaled times. */ fun ms(base: Int) = (base * scale).roundToInt() fun <T> tw(base: Int, delay: Int = 0, easing: Easing = Ease): FiniteAnimationSpec<T> = if (scale == 0f) snap() else tween(base, delay, easing) fun <T> sp(damping: Float = 0.7f, stiffness: Float = Spring.StiffnessMediumLow): FiniteAnimationSpec<T> = @@ -306,26 +281,15 @@ internal fun Modifier.shine(trigger: Boolean, strength: Float = 0.22f): Modifier } } -/** - * Controller focus on the front end: each rail item's requester and the page's main button (Play, - * Open desktop, Open <emulator>, Launch...). The pane leaves, to the left, for the selected rail - * item - whatever tile or button it leaves from - and remembers that control, so coming back in - * lands on it again; a page not yet visited enters on its main button. - */ internal class FrontFocus { val rail = HashMap<String, FocusRequester>() val primary = FocusRequester() var primaryAttached by mutableStateOf(0) var focusedRail by mutableStateOf<String?>(null) fun railFor(key: String): FocusRequester = rail.getOrPut(key) { FocusRequester() } - // The pane's controls by id (a tile's key, a button's label), how many of each are on screen, - // and the last one focused. val items = HashMap<String, FocusRequester>() val attached = HashMap<String, Int>() - // State, so the controller hints follow it: A launches from a game row but selects elsewhere. var last by mutableStateOf<String?>(null) - // The first tile of the page's grid: Down from the page's buttons goes to it, not to whichever - // tile happens to sit under the button. val firstTile = FocusRequester() var firstTileAttached = 0 fun paneEntry(): FocusRequester { @@ -340,16 +304,13 @@ internal class FrontFocus { companion object { const val PRIMARY = "\u0000primary" } } -/** Down from this button goes to the first tile of the page's grid, when there is one. */ @Composable internal fun Modifier.downToFirstTile(): Modifier { - // Sheet rows walk their settings; Desktop's dock stays composed behind the sheet. if (LocalSheetBeat.current != null) return this val ff = LocalFrontFocus.current ?: return this return this.focusProperties { down = if (ff.firstTileAttached > 0) ff.firstTile else FocusRequester.Default } } -/** Marks the first tile of the page's grid. */ @Composable internal fun Modifier.firstTile(): Modifier { val ff = LocalFrontFocus.current ?: return this @@ -360,7 +321,6 @@ internal fun Modifier.firstTile(): Modifier { return this.focusRequester(ff.firstTile) } -/** Lets the pane come back to this control: it is remembered when focused. */ @Composable internal fun Modifier.paneItem(id: String): Modifier { val ff = LocalFrontFocus.current ?: return this @@ -374,14 +334,8 @@ internal fun Modifier.paneItem(id: String): Modifier { internal val LocalFrontFocus = staticCompositionLocalOf<FrontFocus?> { null } -/** The pane's exit animation (170ms) and a frame's margin: a new page is in after this. */ private const val PAGE_EXIT_MS = 200 -/** - * Asks [target] for focus once a frame until [done] (or a focus request lands), for at most half a - * second: a control can take focus only once it is laid out, and that is a frame or two, not a - * fixed number of milliseconds. - */ internal suspend fun focusWithinFrames(done: () -> Boolean, target: () -> FocusRequester) { repeat(30) { androidx.compose.runtime.withFrameNanos { } @@ -390,11 +344,6 @@ internal suspend fun focusWithinFrames(done: () -> Boolean, target: () -> FocusR } } -/** - * Asks [target] for focus on the next frames until a request lands (a control can take focus only - * once it is laid out), for at most half a second; then leaves focus alone, so a press right after - * is never pulled back. - */ internal suspend fun focusSoon(target: () -> FocusRequester) { repeat(30) { androidx.compose.runtime.withFrameNanos { } @@ -443,20 +392,15 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com } } } - // A page reached by a hop goes back the way it came; any other page just closes. val pageBack = { if (!Hops.back(a.onPageBack)) a.onPageBack() } BackHandler(enabled = !processSettingsPageVisible && s.pageKey != null && page != null) { pageBack() } - // Left any other way, the hop's page has no way back to keep. LaunchedEffect(s.pageKey, selected) { Hops.forget(s.pageKey ?: selected) } - // A section reached by a hop (Desktop's Flathub card to the Store) goes back the way it came. BackHandler(enabled = !processSettingsPageVisible && s.pageKey == null && Hops.origin?.page == selected) { val rail = Hops.origin?.rail ?: return@BackHandler Hops.back { selected = rail } } val density = androidx.compose.ui.platform.LocalDensity.current.density remember(density) { Hops.density = density; true } - // Back (and B) from a ROM, an emulator, an added app or Flathub steps out one level instead - // of leaving the app: a ROM -> its emulator, the others -> Desktop. BackHandler( enabled = !processSettingsPageVisible && (s.pageKey == null || page == null) && (selected.startsWith("emu:") || selected.startsWith("rom:") || selected.startsWith("user:") || selected == "store"), @@ -464,32 +408,24 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com selected = if (selected.startsWith("rom:")) "emu:" + selected.removePrefix("rom:").substringBefore(':') else "desktop" } LaunchedEffect(s.isHomeApp) { if (!s.isHomeApp && selected == "android-apps") selected = "steam" } - // The last game uninstalled leaves the Games tab on its empty state. LaunchedEffect(s.steamGames.isEmpty()) { if (s.steamGames.isEmpty() && selected.startsWith("app:")) selected = "games" } remember { Motion.refresh(ctx); true } val railSelection = when { - // A hop's page belongs to the rail item it flew to. Hops.origin != null && s.pageKey == Hops.origin?.page -> Hops.origin!!.dest - // Performance opens from Play's sheet, and is Steam's. s.pageKey == "performance" -> "steam" s.pageKey == "controller-mapping" -> "setup" selected.startsWith("app:") -> "games" - // Flathub is a page of Desktop's, as an emulator's is. selected.startsWith("emu:") || selected.startsWith("rom:") || selected.startsWith("user:") || selected == "store" -> "desktop" else -> s.pageKey ?: selected } - // Bumped each time a rail item is picked, so a controller moves on into the new page. var railPicks by remember { mutableStateOf(0) } val showRailPage: (String, Boolean) -> Unit = { key, focusContent -> - // Components is a full page like Protons or Performance, opened over the current rail - // selection rather than replacing it. if (key == "components") a.onComponents(focusContent) else { if (s.pageKey != null) a.onPageBack() - // Another section: no way back to keep, and no sheet to put back. Hops.origin = null Hops.restore = null Hops.sinking = null @@ -512,19 +448,12 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com .windowInsetsPadding(if (s.launcherFullscreen) WindowInsets.displayCutout else WindowInsets.systemBars) .onFocusChanged { anyFocused = it.hasFocus }, ) { - // Start controllers on the current page's main action, else on the rail. LaunchedEffect(processSettingsPageVisible) { if (processSettingsPageVisible) return@LaunchedEffect snapshotFlow { window.isWindowFocused }.first { it } if (inputModeManager.inputMode != InputMode.Keyboard) inputModeManager.requestInputMode(InputMode.Keyboard) focusWithinFrames({ anyFocused }) { if (frontFocus.primaryAttached > 0) frontFocus.primary else frontFocus.railFor(railSelection) } } - // A tile or button that opens a page goes away with the page it was on, and focus with it; - // the pad then had nothing to move from (a press landed back on the rail's first item). So - // once the new page is in, a controller lands on its main button. - // Backing out of a page: it stays on screen while it leaves (and holds focus there), so hand - // focus back now to the control it was opened from. Not when the page closed because - // another rail item was picked: focus is on the rail then, and stays there. var openPage by remember { mutableStateOf<String?>(null) } LaunchedEffect(s.pageKey) { val closed = openPage != null && s.pageKey == null @@ -535,7 +464,6 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com } LaunchedEffect(selected, s.pageKey, processSettingsPageVisible) { if (processSettingsPageVisible) return@LaunchedEffect - // Past the old page's exit (170ms), then the first frame the new page takes focus. kotlinx.coroutines.delay(Motion.ms(PAGE_EXIT_MS).toLong()) if (inputModeManager.inputMode != InputMode.Keyboard) return@LaunchedEffect if (s.pageKey == "performance") { @@ -548,12 +476,10 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com if (anyFocused) return@LaunchedEffect focusWithinFrames({ anyFocused }) { if (frontFocus.primaryAttached > 0) frontFocus.primary else frontFocus.railFor(railSelection) } } - // A rail item picked with a controller moves on into its page, once the page is in. LaunchedEffect(railPicks) { if (railPicks == 0 || processSettingsPageVisible) return@LaunchedEffect kotlinx.coroutines.delay(Motion.ms(PAGE_EXIT_MS).toLong()) if (inputModeManager.inputMode != InputMode.Keyboard) return@LaunchedEffect - // Waits for the new page's main button rather than a fixed guess at how long it takes. withTimeoutOrNull(1_000) { snapshotFlow { frontFocus.primaryAttached > 0 }.first { it } } ?: return@LaunchedEffect focusWithinFrames({ frontFocus.focusedRail == null && anyFocused }) { frontFocus.paneEntry() } } @@ -563,9 +489,6 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com val railFocus = Modifier .focusProperties { enter = { frontFocus.railFor(railSelection) } } .focusGroup() - // Back from a hop onto a surface that is not a sheet (a game's Components card, Desktop's - // Flathub card): focus - // goes back to the control that asked, once the page has gone. LaunchedEffect(Hops.restore) { val r = Hops.restore ?: return@LaunchedEffect if (r.surface == HOP_SURFACE_SHEET) return@LaunchedEffect @@ -595,9 +518,7 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com } } - // Dots on their way somewhere: a hop to another section and back, a swap flying home. FlightsLayer(screenAt) - // A confirm stepping out of the control that asked for it. StepsLayer(screenAt) appToChooseDisplay?.let { app -> @@ -662,11 +583,9 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com } } val hasBackTarget = (s.pageKey != null && page != null) || - // A section reached by a hop goes back to where it came from, not to the rail. (s.pageKey == null && Hops.origin?.page == selected) || ((s.pageKey == null || page == null) && (selected.startsWith("emu:") || selected.startsWith("rom:") || selected.startsWith("user:") || selected == "store")) - // At the top of a section, Back goes to the rail - the launcher itself is never backed out of. BackHandler(enabled = !processSettingsPageVisible && !hasBackTarget) { if (inputModeManager.inputMode != InputMode.Keyboard) inputModeManager.requestInputMode(InputMode.Keyboard) runCatching { frontFocus.railFor(railSelection).requestFocus() } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt index 27e045019..c79bb2790 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt @@ -81,11 +81,9 @@ import com.droiddeck.launcher.session.SessionPrefs import com.droiddeck.launcher.stores.download.StoreDownloadTier import com.droiddeck.launcher.R -// The Setup page: runtime and device checks, tools, frame generation and launch settings. private enum class CheckState { OK, WARN, BUSY } -/** One requirement in Setup's system check: a status mark, what it is, and at most one action. */ @Composable private fun CheckRow(state: CheckState, title: String, detail: String?, divider: Boolean = true, action: (@Composable () -> Unit)? = null) { val colors = MaterialTheme.colorScheme @@ -159,7 +157,6 @@ internal fun SetupPanel( } val colors = MaterialTheme.colorScheme val pal = LocalPalette.current - // Tested hardware passes; an Adreno below it (a 610, say) warns rather than claiming support. val gpu = remember { com.droiddeck.launcher.gpu.GpuInfo.detect() } val gpuOk = gpu.support == com.droiddeck.launcher.gpu.GpuInfo.Support.TESTED val gpuName = remember { DeviceSupport.gpuName(ctx) } @@ -168,15 +165,12 @@ internal fun SetupPanel( var showLimitDetails by rememberSaveable { mutableStateOf(false) } var editSgdbKey by remember { mutableStateOf(false) } if (editSgdbKey) SgdbKeyDialog(s.sgdbUserKey, onSave = { a.onSgdbKey(it); editSgdbKey = false }, onDismiss = { editSgdbKey = false }) - // Four tabs by scope, a chip row with one gliding selection; LB and RB turn them from - // anywhere on the page. Build and credits are on the Updates page. val tabs = listOf(stringResource(R.string.setup_tab_overview), stringResource(R.string.setup_tab_controller), stringResource(R.string.setup_tab_launcher), stringResource(R.string.setup_tab_diagnostics)) var tab by rememberSaveable { mutableStateOf(0) } val chipFocus = remember { List(4) { FocusRequester() } } var tabTurned by remember { mutableStateOf(false) } val pick: (Int) -> Unit = { i -> tab = i; tabTurned = true } val inputModeManager = LocalInputModeManager.current - // The control a controller was on went with the old tab: it lands on the new tab's chip. LaunchedEffect(tab) { if (tabTurned && inputModeManager.inputMode == InputMode.Keyboard) { androidx.compose.runtime.withFrameNanos { } @@ -260,8 +254,6 @@ internal fun SetupPanel( } } AnimatedVisibility(showLimitDetails, enter = expandVertically(Motion.sp(1f)) + fadeIn(Motion.sp(1f)), exit = shrinkVertically(Motion.sp(1f)) + fadeOut(Motion.sp(1f))) { - // One sentence and at most three buttons. The computer route and its - // raw command live on the full page Wireless debugging opens. Column(modifier = Modifier.fillMaxWidth().padding(start = 56.dp, end = 14.dp, top = 4.dp, bottom = 10.dp)) { Text( if (limitBlocks) PhantomProcessLimit.gateInstructions(ctx, s.phantomProcessStatus) @@ -298,7 +290,6 @@ internal fun SetupPanel( ) } val all = listOf(gpuCheck, runtimeCheck, limitCheck, accountCheck) - // Everything passes: one line says so, and Details opens the checks in place. ReadinessCard( passing, checks.size, listOfNotNull( @@ -312,7 +303,6 @@ internal fun SetupPanel( val shownChecks = all.filterIndexed { i, _ -> checks[i] } shownChecks.forEachIndexed { i, check -> check(i < shownChecks.lastIndex) } } - // Each check that warns is its own card, its Fix it as it always was. if (passing < checks.size) { SectionTitle(stringResource(R.string.setup_needs_you), null) Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { @@ -392,7 +382,6 @@ internal fun SetupPanel( ) } } - // Two columns on a wide page: how it looks; this device and the library's art. if (wide) Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.Top) { Column(Modifier.weight(1f)) { look() } Column(Modifier.weight(1f)) { device(); art() } @@ -415,10 +404,6 @@ internal fun SetupPanel( } } -/** - * The checks as one card while they all pass: a ring with how many pass, "Ready to play" and one - * line of what that means, and a Details chip that opens the checks themselves in place ([checks]). - */ @Composable private fun ReadinessCard(passing: Int, total: Int, line: String, open: Boolean, onDetails: () -> Unit, checks: @Composable () -> Unit) { val colors = MaterialTheme.colorScheme @@ -460,26 +445,18 @@ private fun FrameGenMenu(s: FrontEndState, a: FrontEndActions, host: MenuHost) { FrameGenMenu(host, s.frameGen, s.lossless, a.onFrameGenPick, a.onImportLossless) } -/** - * What shapes a launch, beside the game rather than three screens away in Setup. They are the - * app-wide settings - each card opens the same page or menu Setup does. - */ @Composable internal fun LaunchSettings(s: FrontEndState, a: FrontEndActions, host: MenuHost, game: com.droiddeck.launcher.frontend.Library.SteamGame? = null) { - // Three across, two on a narrow page; each row's cards share one height. val columns = if (LocalNarrowPane.current) 2 else 3 val controller = a.controller val context = androidx.compose.ui.platform.LocalContext.current val inputMode = androidx.compose.ui.platform.LocalInputModeManager.current - // The game's prefix is compatdata/<id>, as droiddeck-game-env reads it: a Steam title's appid, an - // added game's shortcut appid (unsigned). Recommended scans the game's own folder either way. val wincompKey = game?.takeIf { it.library == com.droiddeck.launcher.frontend.Library.ADDED || it.appId > 0 }?.profileKey var wincompOpen by remember(wincompKey) { mutableStateOf<Boolean?>(null) } val wincompPicks = remember(wincompKey, wincompOpen) { wincompKey?.let { com.droiddeck.launcher.session.WinComponents.picks(context, it) }.orEmpty() } val cards = buildList<@Composable (Modifier) -> Unit> { add { m -> val back = stringResource(R.string.hop_from_games) - // Components is its own section: the card hops there, and B on it comes back here. SettingCard(stringResource(R.string.setup_card_components), stringResource(R.string.setup_card_components_hint), "card:components", m.hopSource("card:components")) { val at = Hops.sources["card:components"] if (at == null) a.onComponents(true) @@ -495,9 +472,6 @@ internal fun LaunchSettings(s: FrontEndState, a: FrontEndActions, host: MenuHost } } if (controller != null) add { m -> SettingCard(stringResource(R.string.setup_card_controls), stringResource(R.string.setup_card_controls_hint), "card:controls", m, controller.onMapping) } - // An Epic game's own launch choices, kept in its sidecar so a launch from the Steam client - // honours them too (droiddeck-store-launch reads the same file). - // Store launch choices; cloud saves have their own view. val cardStore = com.droiddeck.launcher.stores.Store.byId(game?.source.orEmpty())?.takeIf { it == com.droiddeck.launcher.stores.Store.EPIC || it == com.droiddeck.launcher.stores.Store.GOG } if (cardStore != null && game?.gameFiles != null) add { m -> Box(m) { @@ -528,10 +502,8 @@ internal fun LaunchSettings(s: FrontEndState, a: FrontEndActions, host: MenuHost } } -/** Epic's account page behind its sign-in: signing in there presents any step the account still owes. */ private const val EPIC_RESOLVE_URL = "https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Faccount%2Fpersonal" -/** The Epic card: the sign-in and offline switches, read from and written to the game's sidecar, and Resolve Epic sign-in. */ @Composable private fun StoreLaunchCard(host: MenuHost, folder: java.io.File, store: com.droiddeck.launcher.stores.Store) { val appContext = androidx.compose.ui.platform.LocalContext.current.applicationContext @@ -550,8 +522,6 @@ private fun StoreLaunchCard(host: MenuHost, folder: java.io.File, store: com.dro if (epic && options.offline) add(stringResource(R.string.epic_card_offline)) else if (epic && options.eos) add(stringResource(R.string.epic_card_eos)) }.ifEmpty { listOf(stringResource(R.string.epic_card_none)) }.joinToString(" · ") - // Each switch goes straight to the sidecar on disk - both launch paths read it there - and the - // card shows what was read back, so a write that did not take is never shown as done. fun set(change: (com.droiddeck.launcher.stores.EpicOptions) -> com.droiddeck.launcher.stores.EpicOptions) { Thread({ val written = runCatching { com.droiddeck.launcher.stores.StoreGameSidecar.updateEpic(folder, change) } @@ -580,8 +550,6 @@ private fun StoreLaunchCard(host: MenuHost, folder: java.io.File, store: com.dro MenuItem(stringResource(R.string.epic_offline), checked = options.offline) { set { it.copy(offline = !it.offline) } } } if (epic) { - // Epic asks some accounts to accept something once (privacy policy, EULA) before a game may - // sign in - EOS's "corrective action". Signing in on Epic's site shows it. MenuItem(stringResource(R.string.epic_resolve), checked = false) { host.open = null runCatching { @@ -616,10 +584,6 @@ private fun SettingCard(label: String, value: String, id: String, modifier: Modi } } -/** - * The app's language: "System default" (what Android uses, named in brackets) or one of the - * languages the app ships, each in its own script. Steam is set to the same language when it starts. - */ @Composable private fun LanguageRow(host: MenuHost, chosen: String, onPick: (String) -> Unit) { val context = androidx.compose.ui.platform.LocalContext.current diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndWidgets.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndWidgets.kt index 20c6871ee..0c3c0904b 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndWidgets.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndWidgets.kt @@ -62,7 +62,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -// Small building blocks shared by the front end's pages: text styles, chips and buttons. internal val Shape14 = RoundedCornerShape(14.dp) @@ -93,7 +92,6 @@ internal fun SectionTitle(t: String, detail: String?) { @Composable internal fun Note(t: String) = Text(t, fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.fillMaxWidth().clip(Shape12).background(MaterialTheme.colorScheme.surface).border(1.dp, LocalPalette.current.line2, Shape12).padding(12.dp)) -/** A page's buttons: they wrap onto a second line rather than run off a narrow page. */ @OptIn(ExperimentalLayoutApi::class) @Composable internal fun Actions(content: @Composable () -> Unit) = FlowRow( @@ -101,13 +99,11 @@ internal fun Actions(content: @Composable () -> Unit) = FlowRow( verticalArrangement = Arrangement.spacedBy(10.dp), ) { content() } -/** A status chip in a row of buttons, centred on their height. */ @Composable internal fun ActionChip(t: String, ok: Boolean) = Box(contentAlignment = Alignment.Center, modifier = Modifier.heightIn(min = 44.dp)) { Chip(t, ok) } internal val Shape16 = RoundedCornerShape(16.dp) -/** One level up, as Back and B do: a real button, big enough to hit ([compact]: for the tightest layouts). */ @Composable internal fun BackLink(label: String, compact: Boolean = false, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -150,14 +146,12 @@ internal fun PrimaryButton( main: Boolean = false, compact: Boolean = false, modifier: Modifier = Modifier, - /** The Steam tab's own Play: bigger than any other button on a page. */ large: Boolean = false, icon: ImageVector? = null, onClick: () -> Unit, ) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current - // The page's main button is where the pane is entered from the rail. val frontFocus = if (main) LocalFrontFocus.current else null if (frontFocus != null) DisposableEffect(Unit) { frontFocus.primaryAttached++ @@ -169,23 +163,18 @@ internal fun PrimaryButton( val pressed by src.collectIsPressedAsState() val scale by animateFloatAsState(if (pressed) 0.955f else if (hot) 1.02f else 1f, Motion.sp(0.5f, Spring.StiffnessMedium), label = "btnScale") val lift by animateFloatAsState(if (hot) 14f else 6f, Motion.tw(300), label = "btnLift") - // Where it sits, so a session it starts can grow out of it (LaunchFlood). val placed = remember { arrayOfNulls<androidx.compose.ui.layout.LayoutCoordinates>(1) } Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier.then(track) .onGloballyPositioned { placed[0] = it } - // 48dp to touch on a handheld; compact rows keep 44. .heightIn(min = if (compact) 44.dp else 48.dp) .then(if (frontFocus != null) Modifier.focusRequester(frontFocus.primary).onFocusChanged { if (it.isFocused) frontFocus.last = FrontFocus.PRIMARY } else Modifier) .graphicsLayer { alpha = if (LaunchOrigin.flooding === placed) 0f else 1f; scaleX = scale; scaleY = scale; shadowElevation = if (enabled) lift.dp.toPx() else 0f; shape = Shape12; clip = false; ambientShadowColor = pal.signal; spotShadowColor = pal.signal } .clip(Shape12) .background(if (enabled) Brush.linearGradient(listOf(colors.primary, pal.primary2)) else Brush.linearGradient(listOf(colors.surfaceVariant, colors.surfaceVariant))) .shine(hot, 0.45f) - // Off, its fill can match the surface it sits on: the outline keeps it a button. .then(if (enabled) Modifier else Modifier.border(1.dp, pal.line2, Shape12)) - // The grow and shine alone barely show on the light fill: outline it when a - // controller is on it, as the other controls are. .glideBorder(hot, Shape12, pal.signal) .hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, enabled = enabled) { placed[0]?.takeIf { it.isAttached }?.let { LaunchOrigin.mark(it.boundsInRoot(), placed) } @@ -207,11 +196,6 @@ internal fun PrimaryButton( } } -/** - * [label] while idle; while [progress] is set, a spinner and how far along it is in its place. - * The label stays laid out underneath, invisible, so the button keeps its width. The spinner - * turns until there is a first figure to show. - */ @Composable internal fun ProgressLabel(label: @Composable (Modifier) -> Unit, progress: Float?, fontSize: androidx.compose.ui.unit.TextUnit = 15.sp) { val pal = LocalPalette.current @@ -253,7 +237,6 @@ internal fun Cog(size: androidx.compose.ui.unit.Dp = 48.dp, onClick: () -> Unit) val colors = MaterialTheme.colorScheme val src = remember { MutableInteractionSource() } val hot = rememberHot(src) - // A press spins it on a half turn as the page it opens blooms out of it (PageOrigin). var spins by remember { mutableIntStateOf(0) } val rot by animateFloatAsState((if (hot) 90f else 0f) + spins * 180f, Motion.sp(0.55f), label = "cog") val pal = LocalPalette.current diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GameEnvironmentEditor.kt b/app/src/main/java/com/droiddeck/launcher/ui/GameEnvironmentEditor.kt index 51c8fdf5a..7557eb249 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GameEnvironmentEditor.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GameEnvironmentEditor.kt @@ -48,8 +48,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -// Environment variables for Proton games: one shared profile and one per game, in the app's own -// dialog, with each variable's actions in a menu off its value as everywhere else. @Composable fun GameEnvironmentRow( @@ -58,8 +56,6 @@ fun GameEnvironmentRow( initialScope: String = "", ) { var open by remember { mutableStateOf(false) } - // Opened with a pad, the editor starts with focus in it; the dialog is a window of its own and - // would otherwise give the pad nothing to move from. var byPad by remember { mutableStateOf(false) } val inputMode = LocalInputModeManager.current SettingsRow(stringResource(R.string.game_env_title), if (showHint) stringResource(R.string.game_env_hint) else null) { @@ -71,7 +67,6 @@ fun GameEnvironmentRow( if (open) GameEnvironmentEditor(byPad, initialScope) { open = false } } -/** With a pad driving, focus [target] once the dialog is laid out, and keep the dialog in pad mode. */ @OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class) @Composable internal fun PadFocus(byPad: Boolean, target: FocusRequester) { @@ -128,7 +123,6 @@ internal fun GameEnvironmentEditor(byPad: Boolean, initialScope: String = "", on PadFocus(byPad, firstFocus) DialogHeader(stringResource(R.string.game_env_eyebrow), stringResource(R.string.game_env_title)) Small(stringResource(R.string.game_env_applies)) - // Which profile: everything shared, or one game's. Panel { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -173,7 +167,6 @@ internal fun GameEnvironmentEditor(byPad: Boolean, initialScope: String = "", on name, value, inherited = !own.containsKey(name), open = menu == "var:$name", enabled = !busy, onToggle = { menu = if (menu == "var:$name") null else "var:$name" }, onDismiss = { if (menu == "var:$name") menu = null }, onEdit = { menu = null; editByPad = dialogInput.inputMode == InputMode.Keyboard; editing = EnvironmentVariableEdit(scope, profileName, name, value ?: "") }, - // A value set here or inherited can be switched off for this profile; one set here can go back to inheriting. onUnset = if (value != null) ({ menu = null; save(current.withEntries(scope, own + (name to null))) }) else null, onRestore = if (own.containsKey(name)) ({ menu = null; save(current.withEntries(scope, own - name)) }) else null, shared = scope.isEmpty(), @@ -182,9 +175,7 @@ internal fun GameEnvironmentEditor(byPad: Boolean, initialScope: String = "", on } } if (busy) LinearProgressIndicator(Modifier.fillMaxWidth()) - // Footer: add a variable, reset the profile, done. Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.padding(top = 4.dp)) { - // Only once the settings are in: a button drawn disabled first comes up without its box. if (current != null) Box { SecondaryButton(stringResource(R.string.game_env_add), enabled = !busy) { menu = if (menu == "add") null else "add" } AnchoredMenu(menu == "add", onDismiss = { if (menu == "add") menu = null }, title = stringResource(R.string.game_env_add)) { first -> @@ -216,12 +207,10 @@ internal fun GameEnvironmentEditor(byPad: Boolean, initialScope: String = "", on private data class EnvironmentVariableEdit(val scope: String, val profileName: String, val name: String, val value: String) -/** The panel the profile and the variables sit in, as a settings group does. */ @Composable private fun Panel(content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit) = Column(Modifier.fillMaxWidth().clip(Shape14).background(MaterialTheme.colorScheme.surface).border(1.dp, LocalPalette.current.line, Shape14), content = content) -/** One variable: its name, what it is set to (and whether that comes from the shared profile), and a menu of what to do with it. */ @Composable private fun VariableRow( name: String, value: String?, inherited: Boolean, open: Boolean, enabled: Boolean, shared: Boolean, @@ -299,7 +288,6 @@ private fun VariableValueEditor(name: String, value: String, onChange: (String) var open by remember(name) { mutableStateOf(false) } var custom by remember(name) { mutableStateOf(option == null || value !in option.choices && option.type != GameEnvironmentOptions.Type.MULTIPLE) } when { - // On/off variables pick from their two values like any other choice: one way to set a value. option != null && option.choices.isNotEmpty() -> { SettingsRow(stringResource(R.string.game_env_value), null, highlighted = open) { Box { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GamepadGeometry.kt b/app/src/main/java/com/droiddeck/launcher/ui/GamepadGeometry.kt index 242f90937..466d57aec 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GamepadGeometry.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GamepadGeometry.kt @@ -2,32 +2,14 @@ package com.droiddeck.launcher.ui import kotlin.math.min -/** - * Where the unfolded screen's gamepad puts each control, for a panel of a given size: plain - * numbers, so the layout is tested without a device (GamepadGeometryTest). - * - * Each thumb pivots at its bottom corner; its main control (a stick or trackpad) sits where it - * rests, diagonally in from the corner, and the right side mirrors the left. - * - * A wide strip (a foldable held flat as a tablet): the second control - the d-pad, the face - * buttons - a slide inward along the arc, the shoulders above, and View, Steam, QAM and Menu - * between the arcs. The reach R is the strip's height, or a third of its width. - * - * A tall panel (held upright): the second control a slide up the arc instead, the shoulders above - * that, View, Steam, QAM and Menu between the two sticks, and Panels - the Deck controls' other - * tabs - between the shoulders. Each side's column S is half the width, or as much as the height - * holds. A wide strip has no room for Panels and leaves it out. - */ object GamepadGeometry { enum class Slot { LEFT_MAIN, LEFT_SECOND, RIGHT_MAIN, RIGHT_SECOND, LEFT_OUTER_SHOULDER, LEFT_INNER_SHOULDER, RIGHT_INNER_SHOULDER, RIGHT_OUTER_SHOULDER, VIEW, STEAM, QAM, MENU, - /** The Deck controls' other tabs: on a tall panel only. */ PANELS, } - /** A control's box: its centre and size, in pixels. */ data class Box(val cx: Float, val cy: Float, val width: Float, val height: Float) { val left get() = cx - width / 2f val top get() = cy - height / 2f @@ -38,19 +20,15 @@ object GamepadGeometry { left < other.right && other.left < right && top < other.bottom && other.top < bottom } - /** The middle buttons, in a row in this order; two by two as on a Deck, View and Menu above Steam and QAM. */ private val middle = listOf(Slot.VIEW, Slot.STEAM, Slot.QAM, Slot.MENU) private val twoByTwo = mapOf(Slot.VIEW to 0, Slot.MENU to 1, Slot.STEAM to 2, Slot.QAM to 3) - /** A panel this shape is laid out as a tall panel (held upright), which has room for Panels; - * anything flatter is a wide strip. */ fun tall(w: Float, h: Float): Boolean = h >= w * 0.45f fun layout(w: Float, h: Float, density: Float): Map<Slot, Box> { if (w <= 0f || h <= 0f) return emptyMap() val pad = 8 * density val buttonH = 40 * density - // A row needs its buttons comfortable; two by two will take them a little narrower. val rowButton = 64 * density val gridButton = 48 * density val gutter = 8 * density @@ -75,14 +53,12 @@ object GamepadGeometry { val mainTop = h - reach * 0.40f - main / 2f val shoulderW = reach * 0.36f val shoulderH = (reach * 0.16f).coerceAtLeast(buttonH) - // Above the main controls, never over them, and inside the panel. val shoulderY = min(h - reach * 0.94f + shoulderH / 2f, mainTop - gutter - shoulderH / 2f) .coerceAtLeast(pad + shoulderH / 2f) pair(Slot.LEFT_MAIN, Slot.RIGHT_MAIN, reach * 0.40f, h - reach * 0.40f, main, main) pair(Slot.LEFT_SECOND, Slot.RIGHT_SECOND, reach * 1.03f, h - reach * 0.30f, second, second) pair(Slot.LEFT_OUTER_SHOULDER, Slot.RIGHT_OUTER_SHOULDER, reach * 0.22f, shoulderY, shoulderW, shoulderH) pair(Slot.LEFT_INNER_SHOULDER, Slot.RIGHT_INNER_SHOULDER, reach * 0.62f, shoulderY, shoulderW, shoulderH) - // Between the arcs: in a row if they fit, else two by two, else one above another. val gap = w - 2 * reach * 1.32f val columns = when { gap >= middle.size * rowButton -> middle.size @@ -102,12 +78,10 @@ object GamepadGeometry { pair(Slot.LEFT_SECOND, Slot.RIGHT_SECOND, side * 0.74f, secondCy, second, second) pair(Slot.LEFT_OUTER_SHOULDER, Slot.RIGHT_OUTER_SHOULDER, side * 0.20f, shoulderY, shoulderW, shoulderH) pair(Slot.LEFT_INNER_SHOULDER, Slot.RIGHT_INNER_SHOULDER, side * 0.57f, shoulderY, shoulderW, shoulderH) - // Between the two sticks at the bottom: two by two, or one above another. val gap = w - 2 * (side * 0.40f + main / 2f) val columns = if (gap >= 2 * gridButton) 2 else 1 val rows = (middle.size + columns - 1) / columns middleGrid(columns, gap - 2 * gutter, h - pad - rows * buttonH - (rows - 1) * 10 * density) - // Between the inner shoulders, where it fits. val panelsW = min(112 * density, w - 2 * (side * 0.57f + shoulderW / 2f) - 2 * gutter) if (panelsW >= buttonH) boxes[Slot.PANELS] = Box(w / 2f, shoulderY, panelsW, buttonH) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt b/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt index eec0f2087..bd850bd6a 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt @@ -62,14 +62,12 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester -/** One matched driver pair in the list (DriverPairs), and what this device makes of it. */ class PairRow( val key: String, val name: String, val version: String, val detail: String, val recommended: Boolean, - /** Built for this GPU's family; the others only show when asked. */ val suits: Boolean, val complete: Boolean, val installed: Boolean, @@ -85,13 +83,11 @@ class GpuDriversState( val unsupported: Boolean = false, val auto: Boolean = true, val pairs: List<PairRow> = emptyList(), - /** The pair key being installed, and how far along (-1 = unknown). */ val busy: String? = null, val percent: Int = -1, val autoStatus: String = "", val releaseStatus: String = "", val checking: Boolean = false, - // Advanced: each driver on its own. val linuxRows: List<DriverRow> = emptyList(), val linuxSelected: String = "", val androidRows: List<DriverRow> = emptyList(), @@ -99,10 +95,8 @@ class GpuDriversState( val linuxDownloads: List<DownloadRow> = emptyList(), val androidDownloads: List<DownloadRow> = emptyList(), val canRestoreBundled: Boolean = false, - /** The Android + Linux bundle both drivers are set to ("DD-Turnip 0.1.0"), or null. */ val activeBundle: String? = null, ) { - /** The pair both drivers are set to, if they make one. */ val activePair: PairRow? get() = pairs.firstOrNull { it.active } } @@ -117,16 +111,10 @@ class GpuDriversActions( val onDownloadDriver: (String) -> Unit = {}, val onImportLinux: () -> Unit = {}, val onImportAndroid: () -> Unit = {}, - /** The tab's own import: a bundle, or a single driver of either kind. */ val onImportZip: () -> Unit = {}, val onRestoreBundled: () -> Unit = {}, ) -/** - * Everything about the chip - SoC, GPU, CPU, memory, system, the phone's own Vulkan driver, the - * drivers in use - gathered once, off the main thread, with a button that copies it for a bug - * report. Stepped out of the Components page's device chip; [first] is where a pad starts. - */ @Composable internal fun DeviceDetails(first: FocusRequester) { val colors = MaterialTheme.colorScheme @@ -159,7 +147,6 @@ internal fun DeviceDetails(first: FocusRequester) { } } -/** One part of the device details: its title, then label/value rows. Focusable, so the d-pad walks the list. */ @Composable private fun DeviceInfoSection(section: DeviceInfo.Section) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/Hop.kt b/app/src/main/java/com/droiddeck/launcher/ui/Hop.kt index a5ee4c8f2..8260bf40e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/Hop.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/Hop.kt @@ -29,17 +29,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.droiddeck.launcher.R -// A link from a control in one section to a page in another (the Play sheet's Graphics drivers, -// a game's Components card): the control squashes into a dot that flies to the destination's rail -// item, the rail's selection glides there as it passes the top of its arc, and the page floods -// out of the rail item once it lands. B on that page flies the dot back and puts the source -// surface back as it was, focus on the control that asked. - -/** - * Where a hop came from: the rail item that was showing ([rail]), the surface on it (a sheet, the - * Games page) and its tab, the control's key and its root bounds, and the words for the way back. - * [page] is the page key the hop opened, so leaving it any other way forgets the way back. - */ internal class HopOrigin( val rail: String, val surface: String, @@ -51,46 +40,29 @@ internal class HopOrigin( val page: String, ) -/** The surfaces a hop can start from and come back to. */ internal const val HOP_SURFACE_GAMES = "games" internal const val HOP_SURFACE_DESKTOP = "desktop" internal const val HOP_SURFACE_SHEET = "sheet" internal object Hops { - /** The hop the page on screen was reached by, while B can still undo it. */ var origin by mutableStateOf<HopOrigin?>(null) - /** The pane content a hop is leaving (a rail key): it sinks back as the dot leaves it. */ var sinking by mutableStateOf<String?>(null) - /** Set while a hop's dot is flying: the rail item its selection has already glided to. */ var railPreview by mutableStateOf<String?>(null) - /** - * Set as a page leaves through a hop (either way), so the pane sinks it rather than draining - * it; the pane clears it once the change has started. - */ var linking by mutableStateOf(false) - /** A surface to put back as it was (a sheet re-lifted on its tab), once, and the control to focus. */ var restore by mutableStateOf<HopOrigin?>(null) - /** Per rail item, bumped when a dot lands on it: the item pops. */ val railLanded = mutableStateMapOf<String, Int>() - /** Bumped when a hop's page has flooded in: its target (a layer card) sends out one ring. */ var arrived by mutableIntStateOf(0) - /** Where each rail item sits, root px. */ val railBounds = HashMap<String, Rect>() - /** Where each control that can start a hop sits, root px. */ val sources = HashMap<String, Rect>() - /** - * Hops from the control at [origin].bounds to [origin].dest: [go] opens the page there once the dot lands. - * With animations off there is no dot and no flood, only the page. - */ fun go(origin: HopOrigin, go: () -> Unit) { val railAt = railBounds[origin.dest] val finish: () -> Unit = { @@ -103,7 +75,6 @@ internal object Hops { arrived++ } if (railAt == null || Motion.scale == 0f) { finish(); return } - // The surface the control is on sinks as its dot leaves, and stays down as it goes. sinking = origin.rail Flights.fly(Flight( origin.bounds, to = { railAt.center }, @@ -112,10 +83,6 @@ internal object Hops { )) } - /** - * B on a page reached by a hop: the page sinks, the source surface comes back as it was, and - * the dot flies home from the rail item to the control. False when there is no hop to undo. - */ fun back(close: () -> Unit): Boolean { val o = origin ?: return false origin = null @@ -133,26 +100,18 @@ internal object Hops { return true } - /** The page the hop opened has gone some other way: there is no way back to keep. */ fun forget(pageKey: String?) { val o = origin ?: return if (pageKey != o.page) { origin = null; sinking = null } } - /** Screen density, for the rail item's corner; set by the front end. */ var density = 1f } -/** Shows a rail section directly (no page closing, nothing forgotten): what a hop's way back calls. */ internal val LocalShowSection = androidx.compose.runtime.staticCompositionLocalOf<(String) -> Unit> { {} } -/** Remembers where this control sits, so a hop from it can fly from it and back to it. */ internal fun Modifier.hopSource(key: String): Modifier = onGloballyPositioned { Hops.sources[key] = it.boundsInRoot() } -/** - * "B · back to <source>" at the right of a page's header while it was reached by a hop; a tap is B. - * Nothing when it was not. - */ @Composable internal fun HopBackChip(onBack: () -> Unit) { val o = Hops.origin ?: return diff --git a/app/src/main/java/com/droiddeck/launcher/ui/LaunchFlood.kt b/app/src/main/java/com/droiddeck/launcher/ui/LaunchFlood.kt index 1ede5c008..0f912aa7a 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/LaunchFlood.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/LaunchFlood.kt @@ -44,25 +44,15 @@ import java.lang.ref.WeakReference import kotlin.math.PI import kotlin.math.sin -// Starting a session from a blue button: the button grows until the page is its blue, and the -// loading screen opens on that blue and gathers it into the ball of the throbber. -// Stopping one from the drawer runs it the other way: Stop grows until the page is blue, and the -// front end opens on that blue and draws it back down into the button the session came from. -// Any other way out (backing out of the loading screen, closing the ended screen, the session -// ending) spreads the blue back out of the throbber's ball instead (FloodSpread). -/** Carries the flood's colour to the session, which opens on it. */ const val EXTRA_FLOOD = "com.droiddeck.launcher.FLOOD" -/** Where the last pressed blue button sat, so a session started by it can grow out of it. */ internal object LaunchOrigin { private var bounds: Rect? = null private var button: Any? = null private var at = 0L - /** The last button a session was started from, for a stop to return into. */ private var last: WeakReference<Any>? = null - /** The button a flood is growing out of: it steps aside so only the flood shows. */ var flooding by mutableStateOf<Any?>(null) fun mark(r: Rect, key: Any) { @@ -72,10 +62,6 @@ internal object LaunchOrigin { at = SystemClock.uptimeMillis() } - /** - * Where the button the running session was started from sits now, if it is still on screen; - * it hides for the return, as for the flood. - */ fun takeReturn(): Rect? { val key = last?.get() as? Array<*> ?: return null val c = key.getOrNull(0) as? LayoutCoordinates ?: return null @@ -84,7 +70,6 @@ internal object LaunchOrigin { return c.boundsInRoot() } - /** The button behind a launch starting now, if one was pressed just before; it hides for the flood. */ fun take(): Rect? { val b = bounds?.takeIf { SystemClock.uptimeMillis() - at < 1_500 } if (b != null) flooding = button @@ -94,7 +79,6 @@ internal object LaunchOrigin { } } -/** A session stopped behind a flood: the blue the front end opens on, for a few seconds. */ internal object QuitFlood { private var color: Int? = null private var at = 0L @@ -111,7 +95,6 @@ internal object QuitFlood { } } -/** The page a flood grows over, sinking back a touch as it is covered; [progress] is the flood's. */ @Composable internal fun FloodBehind(progress: () -> Float, content: @Composable () -> Unit) { Box(Modifier.fillMaxSize().graphicsLayer { val k = sinkScale(progress()); scaleX = k; scaleY = k }) { content() } @@ -121,19 +104,10 @@ private fun sinkScale(progress: Float) = 1f - 0.04f * progress private val Gather = CubicBezierEasing(0.6f, 0f, 0.15f, 1f) -/** - * The pressed button, [from] in root px, stretching out to cover the page. Each edge rides its own - * loose spring: the ones with furthest to go set off first and quickest, the ones near the page's - * edge lag and then snap after them, so it pulls like something elastic rather than zooming. It - * goes blobby in flight, its gradient settles into the flat blue of the ball, and [onProgress] - * (0 to 1) lets the page behind sink a little. Calls [onCovered] once nothing but blue shows. - * Swallows touches while it runs. - */ @Composable internal fun LaunchFlood( from: Rect, onProgress: (Float) -> Unit, - /** The button's own colours, where it is not the page's blue one (a red Stop). */ fromColors: Pair<Color, Color>? = null, cornerAtRest: Dp = 12.dp, onCovered: () -> Unit, @@ -142,10 +116,6 @@ internal fun LaunchFlood( val pal = LocalPalette.current val start = fromColors ?: (colors.primary to pal.primary2) VeilRing() - // Left, top, right, bottom: 0 on the button, 1 on the page's edge. Springs overshoot past the - // edge, off the page, so the wobble is felt in the pull. An underdamped spring then swings back - // short of the edge (about 3% at this damping), so once an edge has reached the page's it holds - // there, or the page would show through in a sliver while the next screen opens. val edges = remember { List(4) { Animatable(0f) } } val reached = remember { BooleanArray(4) } fun edge(i: Int) = edges[i].value.let { if (reached[i]) maxOf(it, 1f) else it } @@ -174,7 +144,6 @@ internal fun LaunchFlood( } } } - // Covered once every edge has reached the page's. snapshotFlow { (0 until 4).all { edge(it) >= 1f } }.first { it } covered() runs.forEach { it.join() } @@ -185,15 +154,9 @@ internal fun LaunchFlood( } } -/** - * The flood from a button at [from] with its edges [v] of the way to the page's (left, top, right, - * bottom): the button sinking with the page behind, blobby in flight, its colours [start] giving - * way to the flat [signal] in the first half. - */ private fun DrawScope.drawFlood(from: Rect, v: List<Float>, start: Pair<Color, Color>, signal: Color, cornerAtRest: Float) { val (l, t, r, b) = v val mean = v.sumOf { it.coerceIn(0f, 1f).toDouble() }.toFloat() / 4f - // The button sinks with the page behind (FloodBehind), so the flood leaves from where it is now. val k = sinkScale(mean) val cx = size.width / 2f val cy = size.height / 2f @@ -203,10 +166,8 @@ private fun DrawScope.drawFlood(from: Rect, v: List<Float>, start: Pair<Color, C val bottom = lerp(cy + (from.bottom - cy) * k, size.height, b) val w = (right - left).coerceAtLeast(0f) val h = (bottom - top).coerceAtLeast(0f) - // A button's corners at rest, a blob in flight, square once it fills the page. val blob = sin(PI * mean).toFloat().coerceAtLeast(0f) val corner = lerp(cornerAtRest * (1f - mean), minOf(w, h) * 0.42f, blob) - // The button's gradient gives way to the ball's flat blue in the first half. val mix = (mean * 2f).coerceAtMost(1f) val brush = Brush.linearGradient( listOf(lerp(start.first, signal, mix), lerp(start.second, signal, mix)), @@ -215,12 +176,6 @@ private fun DrawScope.drawFlood(from: Rect, v: List<Float>, start: Pair<Color, C drawRoundRect(brush, topLeft = Offset(left, top), size = Size(w, h), cornerRadius = CornerRadius(corner)) } -/** - * The front end's first frames after a stop: the page in [flood] after a beat, drawn back down - * into the button at [to] (root px, measured before the page sank) on critically damped springs, - * the far edges first, so it sets down on the button without a bounce. [onProgress] (1 to 0) lets - * the page behind rise back. No button to return to: the blue fades. Calls [onLanded] at the end. - */ @Composable internal fun FloodReturn(flood: Color, to: Rect?, onProgress: (Float) -> Unit, onLanded: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -234,7 +189,6 @@ internal fun FloodReturn(flood: Color, to: Rect?, onProgress: (Float) -> Unit, o LaunchedEffect(page != Size.Zero) { if (page == Size.Zero) return@LaunchedEffect progress(1f) - // A beat of plain blue first, so the change of screen does not read as a cut. delay(Motion.ms(220).toLong()) if (to == null || Motion.scale == 0f) { fade.animateTo(0f, Motion.tw(240, easing = FastOutSlowInEasing)) @@ -262,10 +216,6 @@ internal fun FloodReturn(flood: Color, to: Rect?, onProgress: (Float) -> Unit, o } } -/** - * The session's first frames: the page in [flood], drawing in to a circle over [ball] (centre and - * radius in root px, once the throbber is laid out) that lands the size of it. Calls [onLanded] then. - */ @Composable internal fun FloodGather(flood: Color, ball: Pair<Offset, Float>?, onLanded: () -> Unit) { val gather = remember { Animatable(0f) } @@ -275,7 +225,6 @@ internal fun FloodGather(flood: Color, ball: Pair<Offset, Float>?, onLanded: () LaunchedEffect(ball != null) { if (ball == null || started) return@LaunchedEffect started = true - // A beat of plain blue first, so the change of screen does not read as a cut. delay(Motion.ms(90).toLong()) gather.animateTo(1f, Motion.tw(640, easing = Gather)) landed() @@ -292,18 +241,12 @@ internal fun FloodGather(flood: Color, ball: Pair<Offset, Float>?, onLanded: () } } -/** Where the loading screen's throbber ball sits now (centre and radius, root px), for a flood leaving from it. */ internal object ThrobberSpot { var ball: Pair<Offset, Float>? = null } private val Spread = CubicBezierEasing(0.7f, 0f, 0.4f, 1f) -/** - * Leaving a session: [flood] spreading out of [ball] (centre and radius in root px; the middle of - * the screen when there is none) until it covers the screen, the way FloodGather drew it in. Calls - * [onCovered] then, for the front end to open on the same blue. Swallows touches while it runs. - */ @Composable internal fun FloodSpread(flood: Color, ball: Pair<Offset, Float>?, onCovered: () -> Unit) { VeilRing() diff --git a/app/src/main/java/com/droiddeck/launcher/ui/Layout.kt b/app/src/main/java/com/droiddeck/launcher/ui/Layout.kt index 3ddef26c3..9777ee83b 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/Layout.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/Layout.kt @@ -5,16 +5,11 @@ import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp -// Sizes that decide how the launcher reflows on 4:3 and square screens (480x360 or 480x480 dp) as -// well as on the 16:9 ones it was first laid out for. -/** A page narrower than this reflows: settings rows stack, grids drop columns, posters go. */ val NarrowPaneWidth = 520.dp -/** Whether the page being drawn is narrow - the pane beside the rail on a 4:3 or square screen. */ val LocalNarrowPane = compositionLocalOf { false } -/** Too narrow a screen for a labelled rail: the rail shows icons only. */ @Composable internal fun isNarrowScreen(): Boolean = LocalConfiguration.current.screenWidthDp < 600 diff --git a/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt b/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt index bf64c327b..69835b0ef 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/LiftSheet.kt @@ -110,14 +110,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.roundToInt -// Play's settings, lifted out from under Play instead of opened on a page of their own. At rest a -// lip tucks out under Play and says what it will do. Down (or a pull up, or a tap on the lip) -// stretches the lip, the wall sinks back, and a sheet rises from the pane's foot with Play riding -// its top edge as the handle - StepOut's move, with the pane for a box. B, Up from the chips or a -// tap above folds it away; A on Play launches straight from it, the sheet folding into the pill -// before the launch flood. Open desktop does the same with the desktop's settings. -/** The sheets' tabs (their chips), by id. */ internal object SheetTab { const val DISPLAY = "display" const val CONTROLS = "controls" @@ -128,33 +121,23 @@ internal object SheetTab { const val FILES = "files" } -/** - * Where the open sheet's top edge sits in a pane [paneH] tall (px), for a button [buttonH] tall - * riding it: 60dp down a 360dp pane, the same share of any other, and never so high that the - * button's top comes closer than [minAbove] to the pane's. [ride] is how far below the edge the - * button's centre sits. - */ internal fun sheetTop(paneH: Float, buttonH: Float, minAbove: Float, ride: Float): Float = maxOf(paneH * 60f / 360f, minAbove + buttonH / 2f - ride) -/** A finger's hold on the sheet: where it started (0 rest, 1 open), where it is, whether it has it. */ private class SheetDrag { var from = 0f var progress = 0f var engaged = false } -/** One piece of the sheet's rows fades and rises in on its own beat (a settings group asks for its index). */ internal class SheetBeat(private val pieces: List<Animatable<Float, AnimationVector1D>>) { private var next = 1 fun take(): Int = next++ fun value(i: Int): Float = pieces[i.coerceIn(0, pieces.lastIndex)].value } -/** The sheet whose rows are being laid out, so each settings group can take its beat. */ internal val LocalSheetBeat = staticCompositionLocalOf<SheetBeat?> { null } -/** A settings group's piece of the sheet's entrance; nothing outside a sheet. */ @Composable internal fun Modifier.sheetPiece(): Modifier { val beat = LocalSheetBeat.current ?: return this @@ -172,13 +155,6 @@ private val ChipsTop = 14.dp private val RowsTop = 54.dp private const val PIECES = 12 -/** - * A hero with one big button and the sheet that lifts out from under it. [key] names the sheet - * (a hop comes back to it, the lip remembers what it said); [tabs] are its chips (id to label) and - * [rows] what each holds. [background] sinks and [words] fade as it opens. [button] draws the - * button itself with the click it should make; [onPrimary] is what that click does once the - * sheet is out of the way. [onOpen] reads the settings the rows show. - */ @OptIn(ExperimentalComposeUiApi::class, androidx.compose.animation.ExperimentalAnimationApi::class) @Composable internal fun LiftHero( @@ -187,26 +163,13 @@ internal fun LiftHero( onOpen: () -> Unit, onPrimary: () -> Unit, background: @Composable () -> Unit, - /** - * Drawn over [background] at the pane's own size: the wall's fades, which must not sink with - * it. Given how far the sheet is up (0 to 1), for anything there that should give way to it. - */ scrim: @Composable (lifted: () -> Float) -> Unit = {}, words: @Composable () -> Unit, - /** - * Buttons beside the big one (Desktop's Flathub and +). They stay where they are and fade as - * the sheet lifts: only the big button rides it. - */ beside: @Composable () -> Unit = {}, button: @Composable (onClick: () -> Unit) -> Unit, rows: @Composable (tab: String) -> Unit, modifier: Modifier = Modifier, - /** - * Down on the button lifts the sheet (Play). False: Down goes to the lip, which A lifts, and Down - * from the lip goes on past it to what is under the hero (Desktop's shelves). - */ downOpens: Boolean = true, - /** Told when the sheet starts to lift (true) and once it has gone (false). */ onOpenChange: (Boolean) -> Unit = {}, start: Dp = 44.dp, bottom: Dp = 44.dp, @@ -217,7 +180,6 @@ internal fun LiftHero( val pal = LocalPalette.current val ff = LocalFrontFocus.current val narrow = LocalNarrowPane.current - // Held as states too, so the sheet's focus rule reads what they are now, not at the last composition. val openState = remember { mutableStateOf(false) } var open by openState var shown by remember { mutableStateOf(false) } @@ -225,11 +187,8 @@ internal fun LiftHero( var launching by launchingState var tab by remember { mutableStateOf(0) } var travel by remember { mutableStateOf(1) } - // The control to focus once a sheet put back by a hop's B has lifted, instead of the first chip. var focusOnLift by remember { mutableStateOf<String?>(null) } - // Set as the sheet closes and Play lands: the lip may come back before the sheet has quite gone. var lipBack by remember { mutableStateOf(false) } - // Set as a finger's drag lets go: the sheet is already where it is going. var dragSettled by remember { mutableStateOf(false) } val scope = androidx.compose.runtime.rememberCoroutineScope() var dragJob by remember { mutableStateOf<Job?>(null) } @@ -248,18 +207,15 @@ internal fun LiftHero( val playSrc = remember { MutableInteractionSource() } val playHovered by playSrc.collectIsHoveredAsState() var playFocused by remember { mutableStateOf(false) } - // Set as a pad leaves the sheet to the left for Play, which folds it once Play has focus. val leftForPlay = remember { booleanArrayOf(false) } var heroAt by remember { mutableStateOf(Offset.Zero) } var rest by remember { mutableStateOf<Rect?>(null) } fun focusPlay() { if (ff != null && inputMode.inputMode == InputMode.Keyboard) runCatching { ff.primary.requestFocus() } } fun close() { if (!open) return - // Focus leaves the rows before they go; the sheet lets it go once it is closing. open = false focusPlay() } - // Back from a hop the sheet started (or a page it opened): put it back as it was. LaunchedEffect(Unit) { val r = Hops.restore ?: return@LaunchedEffect if (r.surface != HOP_SURFACE_SHEET || r.rail != key) return@LaunchedEffect @@ -268,7 +224,6 @@ internal fun LiftHero( focusOnLift = r.control open = true } - // Launched: the session covers the screen, and the sheet is put away under it. val lifecycle = androidx.compose.ui.platform.LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle) { val obs = androidx.lifecycle.LifecycleEventObserver { _, e -> @@ -282,7 +237,6 @@ internal fun LiftHero( LaunchedEffect(open, launching) { if (!dragSettled) { dragJob?.cancel(); dragJob = null } if (dragSettled) { - // A finger brought it here and has already animated it: only what opening needs. dragSettled = false if (open) { if (inputMode.inputMode == InputMode.Keyboard) { withFrameNanos { }; runCatching { chipFocus[tab].requestFocus() } } @@ -295,10 +249,8 @@ internal fun LiftHero( onOpen() fold.snapTo(0f); foldTop.snapTo(0f) coroutineScope { - // Pull: the lip stretches down as the wall sinks back and dims. launch { pull.animateTo(24f, Motion.sp(0.7f, 380f)) } launch { sink.animateTo(1f, Motion.tw(260)) } - // Lift: the sheet rises with Play riding its top edge; the lip lets go. launch { delay(Motion.ms(120).toLong()) launch { lipGone.animateTo(1f, Motion.tw(140)) } @@ -307,7 +259,6 @@ internal fun LiftHero( launch { ride.animateTo(1f, Motion.sp(0.7f, 380f)) } launch { hot.snapTo(1f); delay(Motion.ms(100).toLong()); hot.animateTo(0f, Motion.tw(200)) } } - // Each piece on its beat from the Lift, as the reel has it: 260ms + 55ms a piece after it starts. pieces.forEachIndexed { i, p -> launch { delay(Motion.ms(120 + 260 + i * 55).toLong()); p.animateTo(1f, Motion.tw(220)) } } if (inputMode.inputMode == InputMode.Keyboard) launch { val control = focusOnLift @@ -319,7 +270,6 @@ internal fun LiftHero( } } } else if (launching) { - // Play from the sheet: the rows go, the sheet folds into the pill, then the launch. coroutineScope { pieces.forEach { launch { it.animateTo(0f, Motion.tw(90)) } } launch { delay(Motion.ms(60).toLong()); fold.animateTo(1f, Motion.sp(1f, 420f)) } @@ -327,8 +277,6 @@ internal fun LiftHero( } shown = false onPrimary() - // The session covers the screen and the sheet is put away under it (ON_STOP). If - // nothing started, put it away anyway. delay(3_000) launching = false } else if (shown) { @@ -337,7 +285,6 @@ internal fun LiftHero( launch { delay(Motion.ms(60).toLong()); lift.animateTo(0f, Motion.sp(1f, 380f)) } launch { delay(Motion.ms(100).toLong()); ride.animateTo(0f, Motion.sp(0.55f, 300f)) } launch { sink.animateTo(0f, Motion.tw(300)) } - // The lip tucks back under Play as it lands, not once every spring has settled. launch { delay(Motion.ms(420).toLong()); lipGone.snapTo(0f); lipBack = true } } shown = false @@ -345,20 +292,16 @@ internal fun LiftHero( lipGone.snapTo(0f) hot.snapTo(1f) } else { - // Snapped shut under a session: everything back at rest, no animation. listOf(pull, sink, lift, ride, lipGone, fold, foldTop).forEach { it.snapTo(0f) } pieces.forEach { it.snapTo(0f) } hot.snapTo(1f) } } BackHandler(enabled = open && !launching) { close() } - // The ring keeps out of the lift and the fold, and comes back on whatever has focus once - // they are done: Play rides the sheet on a layer, and the ring hung where Play had been. if (ride.isRunning || (shown && !open)) VeilRing() val openNow = open || shown LaunchedEffect(openNow) { onOpenChange(openNow) } - // The lip: there while Play has a pad or a pointer on it, or always with touch alone. val touchOnly = inputMode.inputMode == InputMode.Touch val lipFocus = remember { FocusRequester() } val settingsLabel = stringResource(R.string.drawer_page_settings) @@ -366,7 +309,6 @@ internal fun LiftHero( val lipWanted = !open && (!shown || lipBack) && (playFocused || playHovered || touchOnly || lipFocused) LaunchedEffect(lipWanted) { if (lipWanted) { - // Back from the sheet it comes straight away; otherwise a beat after Play has focus. if (!lipBack) delay(Motion.ms(160).toLong()) lipShow.animateTo(1f, Motion.tw(220)) } else { @@ -375,10 +317,8 @@ internal fun LiftHero( } } - // How far the sheet travels, for a finger's drag to follow: set once the hero is measured. val travelPx = remember { floatArrayOf(1f) } val drag = remember { SheetDrag() } - /** The sheet [p] of the way up (0 at rest, 1 open) under a finger. */ fun follow(p: Float) { drag.progress = p scope.launch { @@ -387,7 +327,6 @@ internal fun LiftHero( pieces.forEach { it.snapTo(p.coerceIn(0f, 1f)) } } } - /** A finger takes the sheet where it is: up from rest, or down from open. */ fun beginDrag() { dragJob?.cancel(); dragJob = null dragSettled = false @@ -400,10 +339,6 @@ internal fun LiftHero( onOpenNow.value() } } - /** - * The finger lets go with [v] px/s (up negative): a flick goes the way it was flicked; a slow - * drag stays once it is a third of the way from where it started. - */ fun settleDrag(v: Float) { drag.engaged = false val flick = with(density) { 600.dp.toPx() } @@ -427,8 +362,6 @@ internal fun LiftHero( } if (open != opens) open = opens else dragSettled = false } - // Pulled down past the top of the rows, the sheet itself comes down with the finger; up again - // before letting go, it goes back up before the rows scroll. val rowsPull = remember { object : androidx.compose.ui.input.nestedscroll.NestedScrollConnection { override fun onPreScroll(available: Offset, source: androidx.compose.ui.input.nestedscroll.NestedScrollSource): Offset { @@ -452,9 +385,6 @@ internal fun LiftHero( } BoxWithConstraints( modifier.fillMaxSize().onGloballyPositioned { heroAt = it.boundsInRoot().topLeft } - // A swipe anywhere on the hero moves the sheet under the finger - up from rest, down - // when open - and lets go open or shut by how far it went and how fast. A swipe that - // starts the wrong way (down at rest) is left alone, for the page under it. .pointerInput(Unit) { val tracker = androidx.compose.ui.input.pointer.util.VelocityTracker() var decided = false @@ -466,7 +396,6 @@ internal fun LiftHero( if (launching) return@detectVerticalDragGestures if (!decided) { decided = true - // Up at rest opens; down when open closes. Anything else is not for the sheet. if (if (open) amount > 0f else amount < 0f) beginDrag() } if (!drag.engaged) return@detectVerticalDragGestures @@ -482,17 +411,12 @@ internal fun LiftHero( val restRect = rest?.translate(-heroAt) val buttonH = restRect?.height ?: with(density) { 56.dp.toPx() } val openTop = sheetTop(h, buttonH, with(density) { 16.dp.toPx() }, with(density) { 3.dp.toPx() }) - // Play's raised place: its centre 3dp under the sheet's edge, same left inset. val raise = restRect?.let { (openTop + with(density) { 3.dp.toPx() } - buttonH / 2f) - it.top } ?: 0f travelPx[0] = (h - openTop).coerceAtLeast(1f) - // The wall behind sinks back about the pane's centre. val foot = h Box(Modifier.fillMaxSize().clipToBounds().graphicsLayer { val k = 1f - 0.05f * sink.value; scaleX = k; scaleY = k }) { background() } - // The fades stay put: scaled with the wall they stopped short of the pane's edges and left - // a bright band of unfaded wall down the side and along the top. scrim { sink.value } - // The dim; a tap on it (above the sheet) folds the sheet away. if (shown || open) Box( Modifier.fillMaxSize() .graphicsLayer { alpha = sink.value }.background(Color.Black.copy(alpha = 0.42f)) @@ -500,7 +424,6 @@ internal fun LiftHero( .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, enabled = open && !launching) { close() }, ) - // The sheet: inset at the sides, rounded at the top, no bottom edge - the pane's foot is its foot. if (shown) { val top = lerp(foot, openTop, lift.value) val sheetLeft = inset @@ -526,7 +449,6 @@ internal fun LiftHero( val rimColor = pal.signal.copy(alpha = lerp(0.55f, 0.9f, hot.value)) if (ft > 0f) drawPath(path, rimColor, style = Stroke(rim)) else { - // Sides and top only: the sheet runs on under the pane's foot. rimPath.reset() rimPath.moveTo(l, b) rimPath.lineTo(l, t + c) @@ -537,40 +459,31 @@ internal fun LiftHero( drawPath(rimPath, rimColor, style = Stroke(rim)) } } - // What the sheet holds: the chips to the right of Play, the rows under them. val chipsX = with(density) { if (narrow || pill == null) 16.dp.toPx() else pill.right + 20.dp.toPx() - sheetLeft } val chipsY = with(density) { if (narrow && pill != null) (pill.bottom - openTop) + 10.dp.toPx() else ChipsTop.toPx() } - // The rows start under the chips, which are 44dp to touch: from 54dp they ran under the - // chips' feet and, scrolled, covered them. val rowsY = with(density) { maxOf(RowsTop.toPx(), chipsY + 52.dp.toPx()) } val sheetW = sheetRight - sheetLeft Box( Modifier .offset { IntOffset(sheetLeft.roundToInt(), top.roundToInt()) } .size(with(density) { sheetW.toDp() }, with(density) { (foot - openTop).coerceAtLeast(0f).toDp() }) - // Touches on the sheet stay on it; none reach the dim behind. .pointerInput(Unit) { detectTapGestures { } } .controllerBack { close() } .bumpers( - // LB / RB: focus goes to the new tab's chip, so a pad on a row (which goes with - // its tab) is never left with nothing. onPrevious = { if (tab > 0) { travel = -1; tab--; runCatching { chipFocus[tab].requestFocus() } } }, onNext = { if (tab < tabs.lastIndex) { travel = 1; tab++; runCatching { chipFocus[tab].requestFocus() } } }, ) - // A pad stays in the sheet until it folds; Left from it reaches Play, the handle, and folds it. .focusProperties { exit = { dir -> if (!openState.value || launchingState.value) FocusRequester.Default else if (dir == FocusDirection.Left && ff != null) { leftForPlay[0] = true; ff.primary } else FocusRequester.Cancel } } .focusGroup(), ) { Box( Modifier .offset { IntOffset(chipsX.roundToInt(), chipsY.roundToInt()) } - // Over the rows, whatever they scroll under them. .zIndex(1f) .width(with(density) { (sheetW - chipsX - 64.dp.toPx()).coerceAtLeast(0f).toDp() }) .graphicsLayer { val v = pieces[0].value; alpha = v; translationY = (1f - v) * 6.dp.toPx() } - // Up from the chips folds the sheet (a finger drags it down, see the hero's drag). .onPreviewKeyEvent { e -> val k = e.nativeKeyEvent if (k.keyCode == KeyEvent.KEYCODE_DPAD_UP) { if (k.action == KeyEvent.ACTION_DOWN) close(); true } else false @@ -600,8 +513,6 @@ internal fun LiftHero( CompositionLocalProvider(LocalSheetBeat provides remember { SheetBeat(pieces) }) { Column( Modifier.fillMaxSize() - // Focus never lands in rows on their way out. Same group always, only - // its rule changes (canFocus here made the scroll's own group a stop). .focusProperties { enter = { if (leavingNow.value) FocusRequester.Cancel else FocusRequester.Default } } .focusGroup() .verticalScroll(rememberScrollState()) @@ -625,7 +536,6 @@ internal fun LiftHero( } } - // The words and the button, where they always were; the button rides the sheet's edge. Column(Modifier.align(Alignment.BottomStart).padding(start = start, bottom = bottom, end = 20.dp)) { Box(Modifier.graphicsLayer { alpha = 1f - sink.value }) { words() } Spacer(Modifier.height(18.dp)) @@ -635,7 +545,6 @@ internal fun LiftHero( .onGloballyPositioned { rest = it.boundsInRoot() } .graphicsLayer { translationY = raise * ride.value } .onFocusChanged { - // Left out of the sheet (from the chips or a row) folds it, as Up from the chips does. if (it.hasFocus && leftForPlay[0]) { leftForPlay[0] = false; if (openState.value && !launchingState.value) close() } playFocused = it.hasFocus } @@ -658,7 +567,6 @@ internal fun LiftHero( if (open || shown) { launching = true; open = false } else onPrimary() } } - // Gone with the words while the sheet is up; nothing a pad or a finger can reach then. val besideShown = !(open || shown) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), @@ -668,9 +576,7 @@ internal fun LiftHero( } } - // The lip, under Play: stretched by the pull, gone as the sheet lifts. val r0 = restRect - // A lip a pad can reach stays composed (unseen at rest) so Down always has it to land on. if (r0 != null && (lipShow.value > 0.01f || pull.value > 0.5f || (!downOpens && !open && !shown))) { var handleW by remember { mutableStateOf(0f) } val lipH = 32.dp + pull.value.dp @@ -680,7 +586,6 @@ internal fun LiftHero( .offset { IntOffset((r0.center.x - handleW / 2f).roundToInt(), (r0.bottom + raise * ride.value + (lipShow.value - 1f) * 8.dp.toPx()).roundToInt()) } .onSizeChanged { handleW = it.width.toFloat() } .graphicsLayer { alpha = lipShow.value * (1f - lipGone.value) } - // 44dp to touch, with a 32dp visible handle. .heightIn(min = 44.dp).testTag("sheet-open") .semantics { contentDescription = settingsLabel } .then( @@ -697,10 +602,6 @@ internal fun LiftHero( } } -/** - * The sheet's handle, tucked under Play: "Settings" and a chevron that bobs up every so often, - * saying there is a drawer to pull up. Still with animations off. - */ @Composable private fun Handle(height: Dp) { val colors = MaterialTheme.colorScheme @@ -708,7 +609,6 @@ private fun Handle(height: Dp) { val shape = RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp) val still = Motion.scale == 0f val bob = androidx.compose.animation.core.rememberInfiniteTransition(label = "handle") - // A quick lift and settle, then a rest: a nudge, not a pulse. val lift by bob.animateFloat( 0f, 0f, androidx.compose.animation.core.infiniteRepeatable( @@ -736,7 +636,6 @@ private fun Handle(height: Dp) { } } -/** A 1dp outline down the sides and along the foot, none along the top (the lip tucks under Play). */ private fun Modifier.drawBorderSidesAndFoot(color: Color, corner: Dp): Modifier = this.drawWithContent { drawContent() @@ -753,31 +652,21 @@ private fun Modifier.drawBorderSidesAndFoot(color: Color, corner: Dp): Modifier drawPath(p, color, style = Stroke(sw)) } -/** - * The sheet's chips: equal capsules with one selection gliding under them (the stores' chip row), - * LB and RB a chip at a time, a tap or A on a far one a hop as a drop. A pad picks a chip by - * moving onto it, and comes into the row on the picked one. [first] takes focus as the sheet - * opens. Too narrow for equal chips, the row scrolls. - */ @OptIn(ExperimentalComposeUiApi::class) @Composable internal fun SheetChips(labels: List<String>, selected: Int, focus: List<FocusRequester>? = null, onPick: (Int) -> Unit) { val pal = LocalPalette.current val bounds = remember { mutableStateMapOf<Int, Rect>() } val tint = rememberUpdatedState(pal.signal) - // Each chip its own requester: one moved from chip to chip under a focused chip lost the ring. val chips = focus ?: remember(labels.size) { List(labels.size) { FocusRequester() } } var within by remember { mutableStateOf(false) } - // LB / RB with a pad on the chips: focus goes with the selection. LaunchedEffect(selected) { if (within) runCatching { chips[selected].requestFocus() } } BoxWithConstraints(Modifier.fillMaxWidth().onFocusChanged { within = it.hasFocus }) { val equal = maxWidth / labels.size.coerceAtLeast(1) >= 96.dp val scroll = rememberScrollState() - // Room inside the scroll for the focus ring, which the scroll's clip would otherwise cut. Box(if (equal) Modifier.fillMaxWidth() else Modifier.edgeFade(scroll).horizontalScroll(scroll).padding(3.dp)) { Row( horizontalArrangement = Arrangement.spacedBy(6.dp), - // Up from the rows lands on the picked chip, not whichever sits above: landing picks. modifier = (if (equal) Modifier.fillMaxWidth() else Modifier) .focusProperties { enter = { chips.getOrNull(selected) ?: FocusRequester.Default } } .focusGroup(), @@ -789,7 +678,6 @@ internal fun SheetChips(labels: List<String>, selected: Int, focus: List<FocusRe SheetChip(label, i == selected, m.onFocusChanged { if (it.isFocused && i != selected) onPick(i) }) { onPick(i) } } } - // Over the chips: under them, their rest fill hid it as it crossed them. ChipGlide(selected, bounds[selected], tint) } } @@ -802,8 +690,6 @@ private fun SheetChip(label: String, on: Boolean, modifier: Modifier, onClick: ( val src = remember { MutableInteractionSource() } val hot = rememberHot(src) val shape = RoundedCornerShape(99.dp) - // The row's glide draws the picked chip's look by touch only; with a pad it draws its own rest - // look, and the bar says which is picked. val under = on && LocalInputModeManager.current.inputMode != InputMode.Keyboard Box( contentAlignment = Alignment.Center, @@ -819,10 +705,6 @@ private fun SheetChip(label: String, on: Boolean, modifier: Modifier, onClick: ( } } -/** - * A read-only line that is a link: [title], [value] under it, and "[link] ↗" at the right in the - * signal blue - not a picker, so no value box. A (or a tap) follows it. [id] is its focus key. - */ @Composable internal fun LinkRow(title: String, value: String, link: String, id: String, arrow: String = "↗", onClick: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt index f51d7cb14..ac9a0524a 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/MainScreen.kt @@ -66,7 +66,6 @@ fun ConfirmDialog(title: String, text: String, confirm: String, onConfirm: () -> ) } -/** Setup's Reset all settings, with the choice to clear game profiles too (off unless ticked). */ @Composable fun ResetSettingsDialog(onConfirm: (gameProfiles: Boolean) -> Unit, onDismiss: () -> Unit) { var gameProfiles by remember { mutableStateOf(false) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt index 3a03fe305..35c7a1765 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt @@ -30,18 +30,15 @@ import com.droiddeck.launcher.runtime.DeckyManager import com.droiddeck.launcher.session.SessionPrefs import com.droiddeck.launcher.session.SessionService -/** [tag] is shown beside the name: [BUNDLED], [DOWNLOADED], [IMPORTED], or "" for Auto / Runtime default. */ class DriverRow(val id: String, val name: String, val detail: String, val removable: Boolean, val tag: String = "") { companion object { const val BUNDLED = "BUNDLED" const val DOWNLOADED = "DOWNLOADED" const val IMPORTED = "IMPORTED" - /** One half of an Android + Linux bundle: picked and deleted together with the other. */ const val BUNDLE = "ANDROID + LINUX" } } -/** A [DriverRow.tag] as shown, in the app's language. */ @Composable internal fun driverTagLabel(tag: String): String = when (tag) { DriverRow.BUNDLED -> stringResource(R.string.driver_tag_bundled) @@ -51,7 +48,6 @@ internal fun driverTagLabel(tag: String): String = when (tag) { else -> tag } -/** A release driver (Banners-Turnip, WinNative) that is not installed yet; [key] is its asset name. */ class DownloadRow(val key: String, val label: String, val detail: String, val progress: Int? = null) class ModeSettings( @@ -60,61 +56,43 @@ class ModeSettings( val panelSize: Pair<Int, Int> = 1280 to 720, val hdr: Boolean, val hdrReason: String?, - /** The GPU drivers in use, as the row that opens them on the Components page says it. */ - /** The drivers line; null shows Auto. */ val gpuDrivers: String? = null, - /** Frames per second the session is capped at; 0 = none. */ val fpsLimit: Int = 0, val upscaler: Int = 0, val upscaleSharpness: Int = 75, val touchMode: String, val suspendPolicy: String, val steamDownloadsInBackground: Boolean = false, - /** Steam only; null hides it. */ val zramCompression: Boolean? = null, - /** Null until checked off the main thread. */ val zramSupported: Boolean? = null, val pipSupported: Boolean = false, val pipAutoEnter: Boolean = false, - /** Steam only. */ val oscMode: String?, - /** Steam only: whether single and double Back actions are swapped. */ val backActionsInverted: Boolean = false, val directAudio: Boolean?, val mic: Boolean?, val gameStorage: String? = null, val storageOptions: List<com.droiddeck.launcher.session.GameStorage.Option> = emptyList(), - /** Steam only: record allocation and sampled storage timings in the next session's Share logs. */ val storageDiagnostics: Boolean = false, val fexPreset: String? = null, - /** Steam only: Force SSBS for Proton games (SessionPrefs.forceSsbs). */ val forceSsbs: Boolean = false, - /** Steam only: SessionPrefs.SYNC_* chosen for Proton games; null outside Steam. */ val syncBackend: String? = null, val stretch16x9: Boolean? = null, val steamRepairQueued: Boolean = false, - /** Steam: the QAM's performance overlay (mangoapp) is started. */ val mangoapp: Boolean = true, - /** Steam only: what the pad is to the client (SessionPrefs.CONTROLLER_*); null outside Steam. */ val steamController: String? = null, - /** Unfolded controls (Steam only; null hides the row). */ val unfoldedControls: Boolean? = null, - /** Steam only: start a Steam session when DroidDeck opens. */ val runSteamAtStartup: Boolean = false, - /** Null outside Steam; the saved choice is separate from Android's access and device switch. */ val wifiDiscovery: Boolean? = null, val wifiDiscoveryPermission: Boolean = false, val wifiDiscoveryLocation: Boolean = false, val wifiDiscoveryAsked: Boolean = false, val wifiDiscoveryBlocked: Boolean = false, - /** Latest Banners-Turnip release: what each driver menu offers to download, and the refresh line. */ - /** Steam only: Decky Loader is managed from the Steam session settings. */ val deckyInstalled: String? = null, val deckyLatestRelease: DeckyManager.Release? = null, val deckyChecking: Boolean = false, val deckyStage: String? = null, val deckyPercent: Int = -1, - /** The stage is the Decky plugin's binary download. */ val deckyDownloadingBinary: Boolean = false, val deckyEnabled: Boolean = false, val deckySessionRunning: Boolean = false, @@ -123,7 +101,6 @@ class ModeSettings( class ModeSettingsActions( val onResolution: (String) -> Unit, val onHdr: (Boolean) -> Unit, - /** Opens the GPU drivers on the Components page: they are shared by every session. */ val onGpuDrivers: () -> Unit = {}, val onFpsLimit: (Int) -> Unit = {}, val onUpscaler: (Int) -> Unit = {}, @@ -159,15 +136,10 @@ class ModeSettingsActions( val onDismiss: () -> Unit, ) -/** - * The chips of a mode's lifted sheet, in order: Steam's six; the desktop's Display, Controls and - * Session (its Files chip is the Desktop page's own). - */ internal fun modeSheetTabs(steam: Boolean): List<String> = if (steam) listOf(SheetTab.STEAM, SheetTab.DISPLAY, SheetTab.CONTROLS, SheetTab.AUDIO, SheetTab.SESSION, SheetTab.LIBRARY) else listOf(SheetTab.DISPLAY, SheetTab.CONTROLS, SheetTab.SESSION) -/** A sheet tab's chip label. */ @Composable internal fun sheetTabLabel(tab: String): String = when (tab) { SheetTab.DISPLAY -> stringResource(R.string.drawer_page_display) @@ -179,19 +151,13 @@ internal fun sheetTabLabel(tab: String): String = when (tab) { else -> stringResource(R.string.setup_tool_files) } -/** - * Rows the front end adds to a sheet's tabs: frame generation (Display), Performance (Session) and - * the Graphics drivers link to Components (Display, given the drivers' summary). - */ class SheetExtras( val frameGen: @Composable () -> Unit = {}, val performance: @Composable () -> Unit = {}, val drivers: @Composable (summary: String?) -> Unit = {}, - /** The Steam tab's account rows (offline mode), from the front end's own state. */ val account: @Composable (host: MenuHost) -> Unit = {}, ) -/** One tab of a mode's lifted sheet: the rows the settings page had, grouped by what they are for. */ @Composable fun ModeSheetRows(s: ModeSettings, a: ModeSettingsActions, tab: String, extras: SheetExtras = SheetExtras()) { val steam = s.mode == SessionService.MODE_STEAM @@ -331,7 +297,6 @@ fun ModeSheetRows(s: ModeSettings, a: ModeSettingsActions, tab: String, extras: if (tab == SheetTab.LIBRARY && steam) { if (s.gameStorage != null) SettingsGroup(stringResource(R.string.mode_storage)) { val custom = s.gameStorage.isNotEmpty() && s.gameStorage != "off" && s.storageOptions.none { it.path == s.gameStorage } - // Each choice: its path, its menu line, the chip's shorter line, and the label it is saved with. class StorageChoice(val path: String, val label: String, val chip: String, val saved: String) val internal = stringResource(R.string.mode_storage_internal) val options = buildList { @@ -478,10 +443,6 @@ fun ModeSheetRows(s: ModeSettings, a: ModeSettingsActions, tab: String, extras: ) } -/** - * The options that apply to every Proton - how Wine syncs, FEX's preset, Force SSBS and the games' - * environment - for the Components page's Proton layer. - */ @Composable internal fun ProtonOptions( syncBackend: String, fexPreset: String, forceSsbs: Boolean, @@ -508,10 +469,6 @@ internal fun ProtonOptions( GameEnvironmentRow() } -/** - * Uninstall for Decky Loader: are you sure steps out of it in red, Cancel first, so a stray A - * costs nothing. - */ @Composable private fun DeckyUninstall(enabled: Boolean, onUninstall: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -538,8 +495,6 @@ private fun DeckyUninstall(enabled: Boolean, onUninstall: () -> Unit) { } } -/** Width × height for the session, with the common handheld shapes one tap away. */ -/** Entry for the user's SteamGridDB API key; the saved key is never shown back. */ @Composable internal fun SgdbKeyDialog(hasKey: Boolean, onSave: (String) -> Unit, onDismiss: () -> Unit) { var key by remember { mutableStateOf("") } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/PackageRow.kt b/app/src/main/java/com/droiddeck/launcher/ui/PackageRow.kt index 2bf44f30c..20abe70a3 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/PackageRow.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/PackageRow.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.ui -/** Package metadata shown on an emulator's own detail page. */ class PackageRow( val id: String, val kind: String, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/PageFlood.kt b/app/src/main/java/com/droiddeck/launcher/ui/PageFlood.kt index ab8f4ff34..e0e95343d 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/PageFlood.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/PageFlood.kt @@ -42,38 +42,20 @@ import kotlinx.coroutines.launch import kotlin.math.PI import kotlin.math.sin -// A page opened from a control (the cog beside Play) grows out of it as a session grows out of -// Play (LaunchFlood): the control stretches out over the pane on loose springs, the far edges -// first, and the page rises into it once it is covered. Back draws it down into the control again. -/** - * Where a page opened from: the control's bounds and corner radius, in px, and the rail item that - * was showing it ([owner]), so the page only draws back into it when that content returns. [art] - * is the control's picture for the tile (a drawable id, or an image Coil loads), if it has one. - */ internal class Origin(val bounds: Rect, val corner: Float, val owner: String? = null, val art: Any? = null) { fun translate(by: Offset) = Origin(bounds.translate(by), corner, owner, art) fun ownedBy(key: String) = Origin(bounds, corner, key, art) } -/** How a page that opened from a control leaves. */ internal enum class PageExit { - /** Back into the control it came from, which is on screen again. */ Drain, - /** Out through a link into another section, which takes the page there itself. */ Hop, - /** Away with the pane, as any page goes: the control it came from is not coming back. */ Sink, } -/** Why a page is leaving: an ordinary change of page, or a link into another section. */ internal enum class PageLeave { Ordinary, Link } -/** - * The exit for a page opened from a control on [owner]'s content, leaving for [incoming]. It only - * draws back into the control when the content that control belongs to is what comes back; a page - * left for another rail item or another page drained into a control that was no longer there. - */ internal fun pageExit(owner: String?, incoming: String, leave: PageLeave): PageExit = when { owner != null && incoming == owner -> PageExit.Drain leave == PageLeave.Link -> PageExit.Hop @@ -84,57 +66,37 @@ internal object PageOrigin { private var origin: Origin? = null private var at = 0L - /** The control a page is about to open from, in root coordinates. */ fun mark(o: Origin) { origin = o at = SystemClock.uptimeMillis() } - /** The control just pressed, once: stale after a second, so a page opened another way never floods. */ fun take(): Origin? = origin.takeIf { SystemClock.uptimeMillis() - at < 1_000 }.also { origin = null } } -/** How long a leaving page is kept for its flood to draw back into the control. */ internal const val PAGE_RETURN_MS = 900 -/** - * [content], a page that opened from the control at [from] (in this element's coordinates): the - * control's tile stretching over it until covered, then the page rising in as the tile fades. - * Once [leaving], the reverse: the tile comes back over the page and draws down into the control. - * A page that [sinks] instead goes with the pane and draws no tile at all. - * The focus ring stays out of it until it is done. - */ @Composable internal fun PageFlood( from: Origin, leaving: Boolean, - /** What the tile shows instead of the control's fill: a game card's art, cropped to the tile as it grows. */ art: Painter? = null, sinks: Boolean = false, - /** - * Where on the page the control's tile lands (root px), when the page has its own picture of - * the control - a game page's hero art: the tile grows onto it instead of over the whole pane, - * corner to corner ([ontoCorner] there), and the page comes in around it. Back leaves from it. - * Null, or off screen: the pane, as any page. - */ onto: (() -> Rect?)? = null, ontoCorner: androidx.compose.ui.unit.Dp = 0.dp, content: @Composable () -> Unit, ) { val pal = LocalPalette.current val glide = LocalFocusGlide.current - // Left, top, right, bottom: 0 on the control, 1 on the pane's edge. val edges = remember { List(4) { Animatable(0f) } } val shown = remember { Animatable(0f) } val tile = remember { Animatable(1f) } var size by remember { mutableStateOf(Size.Zero) } var at by remember { mutableStateOf(Offset.Zero) } - // Where this flood lands (in its own space), read as it opens and again as it leaves. var target by remember { mutableStateOf<Rect?>(null) } val density = androidx.compose.ui.platform.LocalDensity.current LaunchedEffect(size != Size.Zero, leaving, sinks) { if (sinks) { - // Whatever the flood in was doing, the page is shown whole and leaves with the pane. tile.snapTo(0f) shown.snapTo(1f) glide?.hidden = false @@ -142,15 +104,12 @@ internal fun PageFlood( } if (size == Size.Zero) return@LaunchedEffect glide?.hidden = true - // The page lays out under the tile at once, so its picture is there within a frame or two. target = onto?.let { get -> kotlinx.coroutines.withTimeoutOrNull(300) { snapshotFlow { get() }.first { it != null } } }?.translate(-at)?.takeIf { it.height > 1f && it.overlaps(Rect(Offset.Zero, size)) } if (target != null) { coroutineScope { if (!leaving) { - // Onto the page's picture, every edge together on one firm spring: no blob, and - // nowhere to go past it. val runs = edges.map { launch { it.animateTo(1f, Motion.sp(0.85f, 320f)) } } snapshotFlow { edges.all { it.value >= 0.9f } }.first { it } launch { shown.animateTo(1f, Motion.tw(240)) } @@ -202,7 +161,6 @@ internal fun PageFlood( Modifier.fillMaxSize().clipToBounds().onSizeChanged { size = Size(it.width.toFloat(), it.height.toFloat()) } .onGloballyPositioned { at = it.positionInRoot() }, ) { - // Landing on the page's picture, the page must not rise: its picture would move off the tile. val rise = if (onto != null) 0f else 1f Box(Modifier.fillMaxSize().graphicsLayer { alpha = shown.value; translationY = (1f - shown.value) * 14.dp.toPx() * rise }) { content() } val onCorner = with(density) { ontoCorner.toPx() } @@ -212,13 +170,8 @@ internal fun PageFlood( } } -/** - * The control's tile with its edges [v] of the way to the pane's (left, top, right, bottom): - * sinking with the page behind, blobby in flight, square once it fills the pane. - */ private fun DrawScope.drawTile( from: Origin, v: List<Float>, fill: Color, line: Color, alpha: Float, art: Painter? = null, - /** Lands on this instead of the pane (a page's picture of the control), corner [toCorner]. */ to: Rect? = null, toCorner: Float = 0f, ) { val (l, t, r, b) = v @@ -229,7 +182,6 @@ private fun DrawScope.drawTile( left = lerp(f.left, to.left, l); top = lerp(f.top, to.top, t) right = lerp(f.right, to.right, r); bottom = lerp(f.bottom, to.bottom, b) } else { - // The control sinks with the page behind it (scaled to 0.95 about the pane's centre). val k = 1f - 0.05f * mean val cx = size.width / 2f val cy = size.height / 2f @@ -240,14 +192,11 @@ private fun DrawScope.drawTile( } val w = (right - left).coerceAtLeast(0f) val h = (bottom - top).coerceAtLeast(0f) - // Onto a picture it keeps a card's shape, corner to corner; over the pane it blobs in flight. val corner = if (to != null) lerp(from.corner, toCorner, mean).coerceAtMost(minOf(w, h) / 2f) else lerp(from.corner * (1f - mean), minOf(w, h) * 0.42f, sin(PI * mean).toFloat().coerceAtLeast(0f)) drawRoundRect(fill, Offset(left, top), Size(w, h), CornerRadius(corner), alpha = alpha) val pic = art?.intrinsicSize?.takeIf { it.isSpecified && it.width > 0f && it.height > 0f } if (art != null && pic != null && w > 0f && h > 0f) { - // The art covers the tile at every size (a centre crop), so a portrait card opens into the - // wide pane without stretching. val k = maxOf(w / pic.width, h / pic.height) val dw = pic.width * k val dh = pic.height * k @@ -255,7 +204,6 @@ private fun DrawScope.drawTile( translate(left + (w - dw) / 2f, top + (h - dh) / 2f) { with(art) { draw(Size(dw, dh), alpha = alpha) } } } } - // The control's outline, kept while it is still the size of one. val edge = (1f - mean * 3f).coerceIn(0f, 1f) if (edge > 0f) drawRoundRect(line, Offset(left, top), Size(w, h), CornerRadius(corner), alpha = alpha * edge, style = Stroke(1.dp.toPx())) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/PcKeyboard.kt b/app/src/main/java/com/droiddeck/launcher/ui/PcKeyboard.kt index e4450662d..0567319f3 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/PcKeyboard.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/PcKeyboard.kt @@ -54,16 +54,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -/** - * A PC keyboard on screen: every key is a physical key (a Linux evdev code) pressed and released - * on the session's wl_keyboard, as a USB keyboard would - Esc, the function row, Ctrl, Alt, Super, - * the arrows and the editing block included, none of which the Android keyboard can send. - * - * A key goes down when a finger lands on it and up when it lifts, so holding one repeats it the way - * the program's own key repeat does. Ctrl, Shift, Alt and Super are sticky: one tap holds the - * modifier for the next key, a second tap locks it, a third lets it go. A controller moves over the - * keys with the d-pad and presses with A. - */ private data class PcKey(val label: String, val code: Int, val weight: Float = 1f, val shifted: String? = null, @StringRes val labelRes: Int = 0) private fun k(label: String, code: Int, weight: Float = 1f, shifted: String? = null) = PcKey(label, code, weight, shifted) @@ -110,7 +100,6 @@ private const val MOD_LOCKED = 2 @Composable fun PcKeyboard( - /** evdev code, pressed. */ sendKey: (Int, Boolean) -> Unit, onAndroidKeyboard: () -> Unit, onClose: () -> Unit, @@ -142,21 +131,17 @@ fun PcKeyboard( if (key.code == 58) capsOn = !capsOn releaseOnceModifiers() } - // Nothing is left held when the keyboard goes away. androidx.compose.runtime.DisposableEffect(Unit) { onDispose { for ((code, state) in mods.toMap()) if (state != MOD_OFF) sendKey(code, false) } } LaunchedEffect(Unit) { runCatching { firstKey.requestFocus() } } BoxWithConstraints(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) { - // Slightly under half the screen, header included, so the program stays in view above it: - // six rows share what is left of 47% after the header, the gaps and the padding. val rowHeight = ((maxHeight * 0.47f - 24.dp - 3.dp * 6 - 10.dp) / 6).coerceIn(24.dp, 44.dp) Box( Modifier.fillMaxWidth() .background(Color(0xE6101418), RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)) .border(1.dp, pal.line2, RoundedCornerShape(topStart = 14.dp, topEnd = 14.dp)) - // Touches on the keyboard stop here; the program underneath never sees them. .pointerInput(Unit) { awaitEachGesture { awaitFirstDown().consume(); waitForUpOrCancellation()?.consume() } } .padding(horizontal = 8.dp, vertical = 5.dp), ) { @@ -221,7 +206,6 @@ private fun RowScope.KeyCap( down = false; onUp() } } - // A controller: A (or the d-pad's centre) is the finger. .onPreviewKeyEvent { e -> val kc = e.key.nativeKeyCode if (kc != KeyEvent.KEYCODE_BUTTON_A && kc != KeyEvent.KEYCODE_DPAD_CENTER && kc != KeyEvent.KEYCODE_ENTER) return@onPreviewKeyEvent false diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ResolutionRow.kt b/app/src/main/java/com/droiddeck/launcher/ui/ResolutionRow.kt index 2af914e4d..5a0b88daa 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ResolutionRow.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ResolutionRow.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.res.stringResource import com.droiddeck.launcher.R import com.droiddeck.launcher.session.SessionDisplay -/** Pre-session resolution choice, applied when the session starts. */ @Composable fun ResolutionRow( host: MenuHost, choice: String, panel: Pair<Int, Int>, onPick: (String) -> Unit, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SecondScreenPresentation.kt b/app/src/main/java/com/droiddeck/launcher/ui/SecondScreenPresentation.kt index 92cb32117..913f312ea 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SecondScreenPresentation.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SecondScreenPresentation.kt @@ -36,7 +36,6 @@ import com.termux.terminal.TerminalSessionClient import com.termux.view.TerminalView import com.termux.view.TerminalViewClient -/** Touch controls drawn on a second Android display while the compositor remains on the main one. */ class SecondScreenPresentation( private val sessionActivity: SessionActivity, display: Display, @@ -83,7 +82,6 @@ class SecondScreenPresentation( if (created) renderMode() } - /** Releases the auxiliary Linux shell before the window is dismissed. */ fun closeControls() { closeTerminal() keyboardHost?.hide() diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionDrawerPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionDrawerPage.kt index 0955651cc..898900f45 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionDrawerPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionDrawerPage.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.ui -/** Stable page identities keep focus and scroll positions when a secondary display comes and goes. */ internal enum class SessionDrawerPage(val firstControl: String) { CONTROLLER("touch"), SECOND_SCREEN("second-screen:none"), diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionLoading.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionLoading.kt index 3cf190efe..3cb47b070 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionLoading.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionLoading.kt @@ -76,7 +76,6 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.style.TextOverflow -// The session's loading screen, its paused screen and the HUD text style. @Composable fun HudText(text: String) { @@ -94,7 +93,6 @@ fun HudText(text: String) { } } -/** One line of the loading screen's checklist; [detail] is what it says while it is the one running. */ private class LoadStage(@androidx.annotation.StringRes val label: Int, @androidx.annotation.StringRes val detail: Int) private fun loadStages(steam: Boolean): List<LoadStage> = if (steam) listOf( @@ -108,11 +106,6 @@ private fun loadStages(steam: Boolean): List<LoadStage> = if (steam) listOf( LoadStage(R.string.load_opening, R.string.load_opening_detail), ) -/** - * The checklist stage a loading line's [topic] belongs to, -1 when it says nothing about the stage. - * The lines are the session script's "== STEP" milestones and the installer's own progress; they - * arrive in order, and the screen never steps back. - */ private fun stageOf(topic: LoadingState.Topic, steam: Boolean): Int = when (topic) { LoadingState.Topic.RUNTIME -> 0 LoadingState.Topic.SESSION -> 1 @@ -125,15 +118,12 @@ private fun stageOf(topic: LoadingState.Topic, steam: Boolean): Int = when (topi @Composable fun LoadingOverlay( step: String, percent: Int, elapsed: String, hint: String, ended: Boolean, - /** What [step] is about, and whether it is a download or install line worth showing as it is (see [LoadingState]). */ topic: LoadingState.Topic, readable: Boolean, title: String = stringResource(R.string.load_starting_steam), steam: Boolean = true, onCancel: (() -> Unit)? = null, - /** An ended session's exit status and log path, shown small under the advice. */ endedDetail: String? = null, onRetry: (() -> Unit)? = null, onShareLogs: (() -> Unit)? = null, onClose: (() -> Unit)? = null, - /** The blue the page opens on when a button flooded to it, gathered into the throbber's ball. */ flood: Color? = null, onFlooded: () -> Unit = {}, ) { @@ -148,7 +138,6 @@ fun LoadingOverlay( val active = reached.coerceAtMost(stages.lastIndex) val detail = if (percent >= 0 || readable) step.replaceFirstChar { it.uppercase() } else stringResource(stages[active].detail) val context = LocalContext.current - // The Steam tab's wall behind it, dimmed and slowed: starting Steam reads as the same place settling in. val games by produceState(emptyList<Library.SteamGame>()) { value = withContext(Dispatchers.IO) { runCatching { Library.steamGames(context).sortedByDescending { it.lastPlayed } }.getOrDefault(emptyList()) } } @@ -176,7 +165,6 @@ fun LoadingOverlay( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.align(Alignment.Center).verticalScroll(rememberScrollState()).padding(horizontal = 32.dp, vertical = 24.dp), ) { - // Held on the plain D while the flood gathers, and started fresh when it lands. key(flood == null) { LogoThrobber( Modifier.width(if (ended) 88.dp else 110.dp).onGloballyPositioned { ball = throbberBall(it.boundsInRoot()).also { b -> ThrobberSpot.ball = b } }, @@ -211,7 +199,6 @@ fun LoadingOverlay( } } -/** What to do about an ended session: nothing leaves on a timer, and the pad starts on Try again. */ @Composable private fun EndedActions(onRetry: (() -> Unit)?, onShareLogs: (() -> Unit)?, onClose: (() -> Unit)?) { val first = remember { FocusRequester() } @@ -229,7 +216,6 @@ private fun EndedActions(onRetry: (() -> Unit)?, onShareLogs: (() -> Unit)?, onC } } -/** One segment per stage: done ones filled, the current one filling (or running, with no percent). */ @Composable private fun LoadSegments(count: Int, active: Int, percent: Int, modifier: Modifier = Modifier) { val pal = LocalPalette.current @@ -257,7 +243,6 @@ private fun LoadSegments(count: Int, active: Int, percent: Int, modifier: Modifi } } -/** Cancel as the pad's B, in the corner; a tap works too. */ @Composable private fun CancelHint(onCancel: () -> Unit, modifier: Modifier = Modifier) { val colors = MaterialTheme.colorScheme @@ -281,7 +266,6 @@ private fun CancelHint(onCancel: () -> Unit, modifier: Modifier = Modifier) { } } -/** A session paused in the background, ready to pick up where it left off. */ @Composable fun SessionPausedOverlay(title: String = stringResource(R.string.load_steam_paused), onResume: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt index 18cd36810..3716418de 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt @@ -180,16 +180,13 @@ private class DrawerFocus { } else if (focused == key) focused = null } - /** Where focus was last on [page], if anywhere. */ fun target(page: SessionDrawerPage) = last[page] fun request(key: String) = runCatching { requester(key).requestFocus() } fun forget(page: SessionDrawerPage) { last.remove(page) } } -/** Everything the drawer shows and does. */ class DrawerActions( val steam: Boolean, - /** The drawer's heading: the emulator for a program from the rail, else Steam or Desktop. */ val title: String? = null, val isHomeApp: Boolean, val androidApps: List<HomeApp.LaunchableApp>, @@ -201,7 +198,6 @@ class DrawerActions( val suspendPolicy: String, val steamDownloadsInBackground: Boolean = false, val backActionsInverted: Boolean, - /** What the pad is to Steam (SessionPrefs.CONTROLLER_*), switched live; null hides the row. */ val padType: String? = null, val onPadType: (String) -> Unit = {}, val touchMode: String, @@ -209,15 +205,10 @@ class DrawerActions( val fexPreset: String, val upscaler: Int = 0, val upscaleSharpness: Int = 75, - /** The compositor's post chain (gpu/ScreenEffects), changed live. */ val effects: ScreenEffects = ScreenEffects.OFF, - /** Cardboard mode (gpu/Cardboard): side-by-side eyes for a phone VR viewer, changed live. */ val cardboard: Cardboard = Cardboard.OFF, - /** Whether the device has a rotation sensor (head tracking possible). */ val cardboardSensor: Boolean = false, - /** SteamVR in the guest for Cardboard mode (gpu/SteamVr); null while unknown. */ val steamVr: SteamVr.Status? = null, - /** Texture filtering for DirectX 9-11 games (core/TextureFiltering); lands on their next launch. */ val textureAnisotropy: Int? = 0, val textureLodBias: String? = TextureFiltering.LOD_BIAS_OFF, val secondScreenMode: SecondScreenMode, @@ -226,18 +217,13 @@ class DrawerActions( val onHud: (Boolean) -> Unit, val onFrameGenPick: (FrameGen.Mode) -> Unit, val onImportLossless: () -> Unit, - /** The Android keyboard (text, turned into key presses). */ val onKeyboard: () -> Unit, - /** The on-screen PC keyboard: real keys, Esc, F1-F12, Ctrl, Alt... */ val onHardwareKeyboard: () -> Unit, val onSteamMenu: (() -> Unit)?, val onQam: (() -> Unit)?, - /** Null hides the achievements row. */ val onAchievements: (() -> Unit)? = null, - /** (unlocked, total), or null when unknown. */ val achievementsProgress: Pair<Int, Int>? = null, val onOsc: (String) -> Unit, - /** Edits the Steam touch layout of the game in front, when Steam's controls are up. */ val onEditSteamTouch: (() -> Unit)? = null, val controller: com.droiddeck.launcher.input.ControllerPrefs.Settings? = null, val onRumble: (Boolean) -> Unit = {}, @@ -254,7 +240,6 @@ class DrawerActions( val onEffects: (ScreenEffects) -> Unit = {}, val onCardboard: (Cardboard) -> Unit = {}, val onCardboardRecenter: () -> Unit = {}, - /** install, start or stop SteamVR. */ val onSteamVr: (String) -> Unit = {}, val onTextureAnisotropy: (Int?) -> Unit = {}, val onTextureLodBias: (String?) -> Unit = {}, @@ -266,17 +251,13 @@ class DrawerActions( val pipAutoEnter: Boolean = false, val onPip: () -> Unit = {}, val onPipAutoEnter: (Boolean) -> Unit = {}, - /** Unfolded controls (on a foldable; null hides the row). */ val unfoldedControls: Boolean? = null, val onUnfoldedControls: (Boolean) -> Unit = {}, val onShareLogs: () -> Unit, val onStop: () -> Unit, - /** Stop with the dialog's button's bounds on screen, for the flood to grow out of; null falls back to [onStop]. */ val onStopFrom: ((androidx.compose.ui.geometry.Rect?) -> Unit)? = null, val onClose: () -> Unit, - /** Games tab: every Proton with what it uses; null until first read. */ val components: ComponentsManager.Snapshot? = null, - /** Re-reads the Protons (and runs swaps that waited for a game to close). */ val onComponentsRefresh: () -> Unit = {}, val componentCatalog: List<ComponentsManager.CatalogItem> = emptyList(), val componentCatalogAt: Long = 0, @@ -285,9 +266,7 @@ class DrawerActions( val componentDownloadRevision: Long = 0, val onComponentCatalogRefresh: () -> Unit = {}, val onComponentDownload: (ComponentsManager.CatalogItem) -> Unit = {}, - /** Installed games available for profile inspection in the session drawer. */ val gameProfiles: List<Pair<Long, String>> = emptyList(), - /** The selected game profile, or null for the Default profile. */ val selectedGameProfileId: Long? = null, val selectedGameProfile: SelectedGameProfile? = null, val gameProfilesLoaded: Boolean = false, @@ -297,12 +276,9 @@ class DrawerActions( val onSelectedGameProfileRefresh: () -> Unit = {}, val gameFexPresetSaving: Boolean = false, val onGameFexPreset: (Long, String?) -> Unit = { _, _ -> }, - /** Saves the selected game's Start without Steam (GameEnvironment.NO_STEAM). */ val gameNoSteamSaving: Boolean = false, val onGameNoSteam: (Long, Boolean) -> Unit = { _, _ -> }, - /** Swaps [value] ("orig:<build>" or a stored package file) into a Proton's component. */ val onComponentSwap: (protonId: String, comp: String, value: String) -> Unit = { _, _, _ -> }, - /** Saves a stored package for a game, or null to inherit that component from the Default profile. */ val onGameComponent: (appId: Long, comp: String, file: String?) -> Unit = { _, _, _ -> }, ) @@ -347,8 +323,6 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont LaunchedEffect(open, controllerActive) { if (open && !controllerActive) focusManager.clearFocus(force = true) } - // The page focus was last put on: turning to another (LB, RB, a tab) starts it at its first - // option; anything else (a menu closing, the drawer opening again) goes back where it was. var focusedPage by remember { mutableStateOf(page) } LaunchedEffect(open, page, controllerActive, host.open, appToChooseDisplay, confirmStop) { if (!open) { @@ -359,7 +333,6 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont if (host.open != null || appToChooseDisplay != null || confirmStop) return@LaunchedEffect val turned = page != focusedPage focusedPage = page - // The page's top control: on Session that is Android apps, when it is shown. val first = if (page == SessionDrawerPage.SESSION && a.isHomeApp) "apps" else page.firstControl if (turned) focus.forget(page) val target = focus.target(page) ?: first @@ -377,8 +350,6 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { host.open = null; a.onClose() }, ) androidx.compose.foundation.layout.BoxWithConstraints(modifier = Modifier.fillMaxSize().exposeTestTags(), contentAlignment = Alignment.CenterEnd) { - // A 4:3 or near-square screen (under 440dp tall) gets tighter padding and a shorter tab row, - // so the settings keep most of the height; a narrow one keeps some of the game in view. val short = maxHeight < 440.dp val sheetWidth = minOf(360.dp, maxWidth * 0.92f) AnimatedVisibility( @@ -500,7 +471,6 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont ChoiceRow(host, "osc", stringResource(R.string.mode_osc), null, if (a.steam) listOf(SessionPrefs.OSC_AUTO to stringResource(R.string.common_auto), SessionPrefs.OSC_ALWAYS to stringResource(R.string.common_always), SessionPrefs.OSC_STEAM_TOUCH to stringResource(R.string.osc_steam_touch), SessionPrefs.OSC_STEAM_QAM to stringResource(R.string.mode_osc_qam), SessionPrefs.OSC_NEVER to stringResource(R.string.common_never)) else listOf(SessionPrefs.OSC_AUTO to stringResource(R.string.common_auto), SessionPrefs.OSC_ALWAYS to stringResource(R.string.common_always), SessionPrefs.OSC_NEVER to stringResource(R.string.common_never)), - // Steam's touch controller is Steam's alone; elsewhere that choice behaves as Auto. if (!a.steam && a.oscMode == SessionPrefs.OSC_STEAM_TOUCH) SessionPrefs.OSC_AUTO else a.oscMode, chipModifier = focus.track(page, "osc"), onPick = a.onOsc) a.onEditSteamTouch?.let { edit -> @@ -736,7 +706,6 @@ private fun StopSessionButton(modifier: Modifier = Modifier, onClick: () -> Unit } } -/** A compact shortcut into Steam, sharing the header with Stop. */ @Composable private fun SteamActionPill( description: String, icon: androidx.compose.ui.graphics.vector.ImageVector, modifier: Modifier, @@ -761,7 +730,6 @@ private fun SteamActionPill( } } -/** A bumper as a keycap, 44dp to touch; LB / RB on the pad turn the page too. */ @Composable private fun DrawerBumper(label: String, description: String, modifier: Modifier = Modifier, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -785,13 +753,6 @@ private fun DrawerBumper(label: String, description: String, modifier: Modifier } } -/** - * The drawer's page tabs, QAM-style: one icon per page standing in a line in page order. The - * selected one steps forward - full size, bright, a soft glow - while the others step back to - * a little smaller and fade, and the whole line leans a little toward the selection. LB / RB still turn - * the page (the buttons beside it and the pad's bumpers); a tab can be tapped or picked with the - * pad like the dots it replaces. - */ @Composable private fun DrawerPageTabs(page: SessionDrawerPage, pages: List<SessionDrawerPage>, modifier: Modifier = Modifier, compact: Boolean = false, onSelect: (SessionDrawerPage) -> Unit) { val colors = MaterialTheme.colorScheme @@ -805,9 +766,6 @@ private fun DrawerPageTabs(page: SessionDrawerPage, pages: List<SessionDrawerPag modifier = modifier.height(if (compact) 44.dp else 64.dp).clipToBounds(), ) { val tab = if (compact) 36f else 44f - // Fit the line to the room between LB and RB. Worst case is an end tab selected: full size - // ([tab] dp) and pushed outward by the lean, which scales with the spacing. So - // (count-1) * spacing * (1 + lean/spacingMax) + tab <= width val spacing = ((maxWidth.value - tab) / ((count - 1) * (1f + DRAWER_TAB_LEAN_DP / DRAWER_TAB_SPACING_DP))).coerceIn(24f, DRAWER_TAB_SPACING_DP) val leanStep = DRAWER_TAB_LEAN_DP * spacing / DRAWER_TAB_SPACING_DP val lean by animateFloatAsState(-(selectedIndex - middle) * leanStep, motion, label = "tabLean") @@ -848,10 +806,8 @@ private fun DrawerPageTabs(page: SessionDrawerPage, pages: List<SessionDrawerPag } } -/** The Look row's value when the rows below match no Look. */ private const val LOOK_CUSTOM = "custom" -/** Presets and simple effects first; detailed adjustments stay behind Advanced. */ @Composable private fun ScreenEffectsGroup( host: MenuHost, a: DrawerActions, advancedExpanded: Boolean, onAdvancedToggle: () -> Unit, @@ -916,7 +872,6 @@ private fun ScreenEffectsGroup( } } -/** Cardboard mode: the eyes, the lens and the virtual screen; everything applies live. */ @Composable private fun CardboardGroup(host: MenuHost, a: DrawerActions, track: (String) -> Modifier) { val c = a.cardboard @@ -963,7 +918,6 @@ private fun CardboardGroup(host: MenuHost, a: DrawerActions, track: (String) -> } } -/** Secondary-display modes are mutually exclusive and apply immediately. */ @Composable private fun SecondScreenGroup(host: MenuHost, a: DrawerActions, track: (String) -> Modifier) { val colors = MaterialTheme.colorScheme @@ -1032,7 +986,6 @@ private fun DrawerFexPresetRow(host: MenuHost, a: DrawerActions, modifier: Modif } } -/** A game's Start without Steam; only with a game selected - the Default profile has no such switch. */ @Composable private fun DrawerNoSteamRow(host: MenuHost, a: DrawerActions, modifier: Modifier) { val appId = a.selectedGameProfileId ?: return @@ -1043,7 +996,6 @@ private fun DrawerNoSteamRow(host: MenuHost, a: DrawerActions, modifier: Modifie ) { on -> a.onGameNoSteam(appId, on) } } -/** Texture filtering on the Games page: DXVK options for the next DirectX 9-11 launch. */ @Composable private fun TextureFilteringGroup(host: MenuHost, a: DrawerActions, track: (String) -> Modifier) { val inherited = a.selectedGameProfileId != null @@ -1194,11 +1146,6 @@ private fun ProfileSummaryRow(label: String, value: String) { } } -/** - * The Games tab's components: quick swaps between what is already installed, per Proton. A swap - * into the Proton a running game uses waits until that game closes. Downloads below share the - * launcher's package store; importing and deleting stay on the Components page. - */ @Composable private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (String) -> Modifier) { val colors = MaterialTheme.colorScheme @@ -1260,7 +1207,6 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin val running = snap.protons.filter { it.inUseByGame } var pick by rememberSaveable { mutableStateOf<String?>(null) } val view = snap.protons.firstOrNull { it.proton.id == pick } ?: running.firstOrNull() ?: snap.protons.firstOrNull() - // Focusable, so the pad can climb from the Proton box up to it and the page scrolls it into view. SettingsGroup(stringResource(R.string.drawer_running_now)) { val src = remember { MutableInteractionSource() } val hot = src.collectIsFocusedAsState().value @@ -1315,7 +1261,6 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin val shown = options.firstOrNull { it.first == current }?.second DrawerStackedChoice( host, "cmp-$comp", ComponentsManager.LABEL.getValue(comp), - // The box already shows the choice: only say more when there is more to say. st.queued?.let { stringResource(R.string.comp_next_queued, it) } ?: st.inUse.takeIf { it != shown }, options, current, note = if (view.inUseByGame) stringResource(R.string.comp_waits_long) else stringResource(R.string.comp_applies_long), @@ -1330,7 +1275,6 @@ private fun ComponentsDrawerPage(host: MenuHost, a: DrawerActions, track: (Strin ) } -/** Downloads only store packages; the profile/Proton choices above apply them separately. */ @Composable internal fun ComponentDownloadsDrawerGroup( host: MenuHost, @@ -1408,10 +1352,6 @@ internal fun ComponentDownloadsDrawerGroup( } } -/** - * A choice for the drawer's narrow column: label, then the box across the full width, then the - * detail underneath - so a long Proton or package name never squeezes the text beside it. - */ @Composable private fun <T> DrawerStackedChoice( host: MenuHost, key: String, label: String, hint: String?, @@ -1445,7 +1385,6 @@ private fun <T> DrawerStackedChoice( Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line)) } -/** A close row of icons: inactive tabs stay legible while the active tab leads. */ private const val DRAWER_TAB_SPACING_DP = 48f private const val DRAWER_TAB_LEAN_DP = 8f @@ -1454,18 +1393,11 @@ private const val DRAWER_TAB_SIDE_SCALE = 0.8f private const val DRAWER_TAB_SIDE_ALPHA = 0.8f -/** Room between the Stop pill and the box that steps out under it: the pull. */ private val StepPull = 6.dp private val StepBoxCorner = 16.dp private val StepFillet = 10.dp private val StepBoxMaxWidth = 264.dp -/** - * Stop's confirm, grown out of the Stop pill at [pill] (px, in the sheet): a pull the pill's width - * drops out of it and a wider box steps out to the left under it for the choices, right edges - * flush, so Stop lands under the pill. Cancel, B and a tap outside fold it back into the pill, - * width first. [onStop] gets Stop's bounds on screen for the flood. - */ @Composable private fun StopConfirm( open: Boolean, @@ -1481,7 +1413,6 @@ private fun StopConfirm( val tint = remember { Animatable(0f) } val dim = remember { Animatable(0f) } val items = remember { List(3) { Animatable(0f) } } - // Composed from the first open until it has folded back into the pill. var shown by remember { mutableStateOf(false) } var grown by remember { mutableStateOf(false) } var content by remember { mutableStateOf<IntSize?>(null) } @@ -1535,7 +1466,6 @@ private fun StopConfirm( val p = pill ?: return Box(Modifier.fillMaxSize()) { - // The rest of the sheet dims behind it, and a tap there cancels. Box( Modifier.fillMaxSize().graphicsLayer { alpha = dim.value }.background(Color(0x59000000)) .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, enabled = open, onClick = onCancel), @@ -1552,7 +1482,6 @@ private fun StopConfirm( drawPath(path, lerp(hotFill, restFill, tint.value)) drawPath(path, colors.error.copy(alpha = lerp(0.9f, 0.55f, tint.value)), style = Stroke(rim)) } - // The pill's label rides on top, as the pull's handle. Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.offset { IntOffset(p.left.roundToInt(), p.top.roundToInt()) } @@ -1570,7 +1499,6 @@ private fun StopConfirm( .onSizeChanged { if (content != it) content = it } .controllerBack(onCancel) .onPreviewKeyEvent { e -> - // Keep a controller in here, as the dialog's own window used to. if (e.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false when (e.key) { Key.DirectionUp, Key.DirectionDown -> true @@ -1609,7 +1537,6 @@ private fun StopConfirm( } } -/** One of the confirm's two choices: a capsule that fills when focused; Stop in the danger red. */ @Composable private fun ConfirmChoice(label: String, danger: Boolean, enabled: Boolean, modifier: Modifier = Modifier, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SettingsWidgets.kt b/app/src/main/java/com/droiddeck/launcher/ui/SettingsWidgets.kt index 9de13ee8c..0a07695b3 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SettingsWidgets.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SettingsWidgets.kt @@ -117,7 +117,6 @@ import androidx.compose.ui.window.PopupProperties private val RowShape = RoundedCornerShape(12.dp) private val GroupShape = RoundedCornerShape(14.dp) -/** Whether a pad's A is held right now, as the activity sees it: a focus move waits for its release. */ internal object HeldKeys { @Volatile var confirm = false } @@ -142,7 +141,6 @@ internal fun Modifier.controllerBack(onBack: () -> Unit): Modifier = onPreviewKe } } -/** LB and RB turn a page's tabs; the keys are taken whenever focus is anywhere inside [this]. */ internal fun Modifier.bumpers(onPrevious: () -> Unit, onNext: () -> Unit): Modifier = onPreviewKeyEvent { event -> val keyEvent = event.nativeKeyEvent val previous = keyEvent.keyCode == KeyEvent.KEYCODE_BUTTON_L1 @@ -154,11 +152,6 @@ internal fun Modifier.bumpers(onPrevious: () -> Unit, onNext: () -> Unit): Modif } } -/** - * A page's tabs with the pad's bumpers drawn beside them. The host turns them with LB/RB (see - * [bumpers]); the keycaps are for touch and stay out of the d-pad's path. The row scrolls sideways - * when a narrow page cannot fit every tab. - */ @OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class) @Composable internal fun TabStrip( @@ -168,8 +161,6 @@ internal fun TabStrip( val colors = MaterialTheme.colorScheme val pal = LocalPalette.current val shape = RoundedCornerShape(12.dp) - // Tabs that overflow (long labels on a small phone) first close up; only what still does not - // fit scrolls. Once tight the row stays tight, so it cannot flip back and forth. val scroll = rememberScrollState() var tight by remember(tabs) { mutableStateOf(false) } LaunchedEffect(scroll.maxValue) { if (scroll.maxValue in 1 until Int.MAX_VALUE) tight = true } @@ -186,7 +177,6 @@ internal fun TabStrip( val hot = src.collectIsFocusedAsState().value || src.collectIsHoveredAsState().value val pick = { onSelect(i) } val tabShape = RoundedCornerShape(9.dp) - // The chosen tab scrolls fully into view, so one at the end is not left cut off. val reveal = remember { androidx.compose.foundation.relocation.BringIntoViewRequester() } if (on) LaunchedEffect(Unit) { reveal.bringIntoView() } Text( @@ -209,7 +199,6 @@ internal fun TabStrip( } } -/** A bumper drawn as a keycap, 44dp to touch; the pad presses the real one. */ @Composable private fun BumperKey(label: String, description: String, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -230,7 +219,6 @@ private fun BumperKey(label: String, description: String, onClick: () -> Unit) { } } -/** The narrowest a setting's value box gets; the session menu's sheet sets it lower. */ val LocalChipMinWidth = androidx.compose.runtime.compositionLocalOf { 150.dp } class MenuHost { @@ -240,7 +228,6 @@ class MenuHost { @Composable fun rememberMenuHost(): MenuHost = remember { MenuHost() } -/** Under the anchor with left edges aligned, kept on screen: a dropdown that belongs to a tab. */ private class BelowStartProvider(private val gap: Int) : PopupPositionProvider { override fun calculatePosition(anchorBounds: IntRect, windowSize: IntSize, layoutDirection: LayoutDirection, popupContentSize: IntSize): IntOffset { var x = anchorBounds.left @@ -267,7 +254,6 @@ private class BelowEndProvider(private val gap: Int) : PopupPositionProvider { @Composable fun AnchoredMenu( open: Boolean, onDismiss: () -> Unit, title: String? = null, note: String? = null, - /** A tab's dropdown: under its anchor, left edges aligned, as wide as its entries (within [minWidth]..260 dp). */ compact: Boolean = false, minWidth: androidx.compose.ui.unit.Dp = 120.dp, content: @Composable ColumnScope.(FocusRequester) -> Unit, ) { @@ -283,8 +269,6 @@ fun AnchoredMenu( android.util.Log.i("AnchoredMenu", "requested first option focus") } } - // Popup installs Android's window density. Keep the app scale from the anchor, including - // the matching dp window dimensions used to cap and scroll the menu. val density = LocalDensity.current val configuration = LocalConfiguration.current val gap = with(density) { 6.dp.roundToPx() } @@ -299,8 +283,6 @@ fun AnchoredMenu( exit = fadeOut(Motion.tw(140)) + scaleOut(Motion.tw(140), targetScale = 0.96f, transformOrigin = TransformOrigin(if (compact) 0f else 1f, 0f)), label = "menu", ) { - // A long list (a driver menu with its downloads) must not run off the screen: the menu is - // capped at most of the window's height and its entries scroll under the fixed title. val maxHeight = (androidx.compose.ui.platform.LocalConfiguration.current.screenHeightDp * 0.86f).dp Column( modifier = Modifier @@ -317,8 +299,6 @@ fun AnchoredMenu( title.uppercase(), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 1.5.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(start = 10.dp, top = 6.dp, bottom = 6.dp), ) - // While entries remain below the fold the list fades out at the bottom over a down - // arrow, so a short screen (a phone in landscape) shows that the menu goes on. val scroll = rememberScrollState() Box(Modifier.weight(1f, fill = false)) { Column( @@ -368,7 +348,6 @@ fun MenuItem( val src = remember { MutableInteractionSource() } val hot = src.collectIsFocusedAsState().value || src.collectIsHoveredAsState().value val shift by animateFloatAsState(if (hot) 2f else 0f, Motion.sp(0.5f), label = "miShift") - // A menu taller than the screen opens scrolled to the current choice, not past it. val reveal = remember { androidx.compose.foundation.relocation.BringIntoViewRequester() } if (checked) LaunchedEffect(Unit) { reveal.bringIntoView() } Row( @@ -431,7 +410,6 @@ fun ValueChip(text: String, open: Boolean, enabled: Boolean = true, modifier: Mo fun SettingsGroup(title: String, compact: Boolean = false, content: @Composable ColumnScope.() -> Unit) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current - // In a lifted sheet each group fades and rises in on its own beat. Column(Modifier.fillMaxWidth().sheetPiece()) { Row( verticalAlignment = Alignment.CenterVertically, @@ -445,11 +423,6 @@ fun SettingsGroup(title: String, compact: Boolean = false, content: @Composable } } -/** - * One setting: its label and [hint] with the [control] beside them. [hintLines] fixes the hint to - * that many lines, for a hint that changes with the value - the row keeps its height, so the rows - * under it stay put. - */ @Composable fun SettingsRow(label: String, hint: String?, highlighted: Boolean = false, hintLines: Int? = null, stacked: Boolean = false, control: @Composable () -> Unit) { val colors = MaterialTheme.colorScheme @@ -459,7 +432,6 @@ fun SettingsRow(label: String, hint: String?, highlighted: Boolean = false, hint Text(hint ?: "", fontSize = size, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 2.dp), minLines = hintLines ?: 1, maxLines = hintLines ?: Int.MAX_VALUE, overflow = TextOverflow.Ellipsis) } - // A narrow page stacks the control under its label, so neither squeezes the other. if (stacked || LocalNarrowPane.current) Column( modifier = Modifier.fillMaxWidth().background(bg).padding(start = 14.dp, end = 14.dp, top = 11.dp, bottom = 12.dp), ) { @@ -483,9 +455,7 @@ fun SettingsRow(label: String, hint: String?, highlighted: Boolean = false, hint fun <T> ChoiceRow( host: MenuHost, key: String, label: String, hint: String?, options: List<Pair<T, String>>, selected: T, enabled: Boolean = true, note: String? = null, - /** For the box itself - a page's FocusRequester for its first control. */ chipModifier: Modifier = Modifier, - /** See SettingsRow: a fixed height for a hint that changes with the choice. */ hintLines: Int? = null, onPick: (T) -> Unit, ) { @@ -505,7 +475,6 @@ fun <T> ChoiceRow( } } -/** A small "?" beside a value the app guessed and is not sure of; [description] says what to check. */ @Composable fun UncertainMark(description: String) { val colors = MaterialTheme.colorScheme @@ -524,7 +493,6 @@ fun ToggleRow(host: MenuHost, key: String, label: String, hint: String?, checked ToggleSwitch(checked, enabled, label, chipModifier.testTag("setting-$key")) { host.open = null; onChange(it) } } -/** An on/off switch: one tap or one A press flips it, where a menu of On and Off took three. */ @Composable fun ToggleSwitch(checked: Boolean, enabled: Boolean = true, label: String? = null, modifier: Modifier = Modifier, onChange: (Boolean) -> Unit) { val colors = MaterialTheme.colorScheme @@ -537,7 +505,6 @@ fun ToggleSwitch(checked: Boolean, enabled: Boolean = true, label: String? = nul val onLabel = stringResource(R.string.widgets_on) val offLabel = stringResource(R.string.widgets_off) val shape = RoundedCornerShape(99.dp) - // 48dp tall to touch; the visible track sits inside it. Box( contentAlignment = Alignment.Center, modifier = modifier @@ -562,11 +529,6 @@ fun ToggleSwitch(checked: Boolean, enabled: Boolean = true, label: String? = nul } } -/** - * A bounded value as a track: the d-pad's left and right step it by [step] (held, it keeps - * going), a finger drags or taps it, and [format] prints the value beside it. [modifier] goes on - * the focusable part, for a page's focus tracking. - */ @Composable fun SliderRow( label: String, hint: String?, value: Int, range: IntRange, step: Int = 1, enabled: Boolean = true, @@ -599,7 +561,6 @@ fun ValueSlider( var trackPx by remember { mutableIntStateOf(0) } val text = format(value) val shape = RoundedCornerShape(8.dp) - // Preserve off-grid preset values until the user adjusts; every input snaps to the same step. fun stepped(v: Int) = (((v - range.first).toFloat() / step).roundToInt() * step + range.first).coerceIn(range.first, range.last) fun fromX(x: Float) { val width = trackPx - 2 * thumbInset @@ -619,7 +580,6 @@ fun ValueSlider( val left = keyEvent.keyCode == KeyEvent.KEYCODE_DPAD_LEFT if (!left && keyEvent.keyCode != KeyEvent.KEYCODE_DPAD_RIGHT) false else { - // Repeated key-down events let a held direction keep adjusting. if (enabled && keyEvent.action == KeyEvent.ACTION_DOWN) { val next = stepped(value + if (left) -step else step) if (next != value) currentOnChange(next) @@ -699,13 +659,10 @@ fun SettingsPage( onBack: () -> Unit, eyebrow: String? = null, lede: String? = null, - /** A small control at the right of the title (the driver pages' refresh button). */ action: (@Composable () -> Unit)? = null, - /** Pass one held above the page to keep its scroll position across a page opened over it. */ scroll: androidx.compose.foundation.ScrollState? = null, scrollContent: Boolean = true, compactLayout: Boolean = false, - /** Fixed controls below the title, or beside Back when the title is omitted. */ header: (@Composable () -> Unit)? = null, modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit, @@ -733,7 +690,6 @@ fun SettingsPage( } } } else if (narrow) { - // Back and the title share one line: a 4:3 screen has no height to spare. Rise(0) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -804,11 +760,6 @@ fun SettingsPage( } } -/** - * A short row of tabs with one chosen: a pill in the signal blue slides under the choice and its - * label turns to sit on it. Left and right move between tabs; a tap or A picks one. [selected] - * null leaves no tab chosen. - */ @Composable fun <T> SegmentedTabs(options: List<Pair<T, String>>, selected: T?, modifier: Modifier = Modifier, onPick: (T) -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SharedMotion.kt b/app/src/main/java/com/droiddeck/launcher/ui/SharedMotion.kt index 75fd06660..aa37d5f48 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SharedMotion.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SharedMotion.kt @@ -37,20 +37,9 @@ import kotlin.math.abs import kotlin.math.atan2 import kotlin.math.sin -// The moves the stores made first, for any section: one selection shape that glides along a row -// (or down a column) of choices, and a pressed control that squashes into a dot and flies home. -/** Which way a [ChipGlide]'s choices run. */ internal enum class GlideAxis { Horizontal, Vertical } -/** - * The selection under a row of chips: [at] (in the row) of chip [index], in [tint]. A move to the - * next chip stretches it - the leading edge on the focus ring's lead spring, the trailing one a - * beat later on its trail spring - and a hop past chips stretches the same way, across them. - * Draw it over the chips: under them, their own rest fill hid it in flight. [axis] runs it down a column instead (the rail, a stack of layers). [corner] is the - * chips' own corner; null draws a capsule. [fill] and [outline] are the resting alphas. Not - * [visible], it fades away, and comes back on whichever chip it is on by then. - */ @Composable internal fun BoxScope.ChipGlide( index: Int, @@ -63,12 +52,9 @@ internal fun BoxScope.ChipGlide( visible: Boolean = true, ) { val density = androidx.compose.ui.platform.LocalDensity.current - // The two edges along the axis (left and right, or top and bottom), and the two across it. val start = remember { Animatable(Float.NaN) } val end = remember { Animatable(0f) } val pinch = remember { Animatable(0f) } - // With a pad the focus ring fills what it holds, and is the only fill: a selection of the - // row's own, sliding on its own beat, trailed the ring and sat on items the pad had left. val pad = androidx.compose.ui.platform.LocalInputModeManager.current.inputMode == androidx.compose.ui.input.InputMode.Keyboard @Suppress("NAME_SHADOWING") val visible = visible && !pad val shown = remember { Animatable(if (visible) 1f else 0f) } @@ -91,15 +77,12 @@ internal fun BoxScope.ChipGlide( if (visibleNow.value) shown.snapTo(1f) return@LaunchedEffect } - // Away or on its way back: it goes straight to the chip, unseen. if (!visibleNow.value || shown.value < 0.5f) { start.snapTo(s); end.snapTo(e); pinch.snapTo(0f) return@LaunchedEffect } coroutineScope { run { - // Every move stretches across, however far: a long one used to go as a drop (which - // sat on chips already left) and then as a fade (a flash of no fill at all). launch { pinch.animateTo(0f, Motion.sp(0.6f, 500f)) } launch { shown.animateTo(1f, Motion.tw(120)) } val forward = index > from @@ -119,11 +102,9 @@ internal fun BoxScope.ChipGlide( val size = shape.size val round = minOf(size.width, size.height) / 2f val r = CornerRadius(corner?.let { lerp(minOf(with(density) { it.toPx() }, round), round, p) } ?: round) - // A drop in flight is solid; on a chip it is the chip's tinted fill and outline. val a = shown.value drawRoundRect(tint.value.copy(alpha = lerp(fill, 0.9f, p) * a), at0, size, r) if (outline > 0f) { - // Inside the chip's edge: a stroke on it would lose its outer half to a scrolling row's clip. val w = with(density) { 1.dp.toPx() } val inset = w / 2f drawRoundRect( @@ -134,11 +115,6 @@ internal fun BoxScope.ChipGlide( } } -/** - * The chosen one of a row of chips (or a column, [vertical]): a short bar of [color] along its foot - * (or its start edge). Not a fill or an outline, so it never reads as focus; with a pad the ring's - * own infill is the only fill. Goes inside the chip's clip. - */ @Composable internal fun Modifier.shownBar(on: Boolean, color: Color, vertical: Boolean = false): Modifier { val shown = androidx.compose.animation.core.animateFloatAsState(if (on) 1f else 0f, Motion.tw(if (on) 140 else 100), label = "shownBar").value @@ -156,11 +132,6 @@ internal fun Modifier.shownBar(on: Boolean, color: Color, vertical: Boolean = fa } } -/** - * Where a glide's shape is: its edges along the axis at [start] and [end] (either order), across - * it from [crossStart] to [crossEnd], pinched [pinch] of the way (0 to 1) into a drop [drop] each - * side of its centre. [vertical] runs the axis down instead of across. - */ internal fun glideShape(start: Float, end: Float, crossStart: Float, crossEnd: Float, pinch: Float, drop: Float, vertical: Boolean): Rect { val a0 = minOf(start, end) val a1 = maxOf(start, end) @@ -173,11 +144,6 @@ internal fun glideShape(start: Float, end: Float, crossStart: Float, crossEnd: F return if (vertical) Rect(c0, s0, c1, s1) else Rect(s0, c0, s1, c1) } -/** - * A dot on its way somewhere: from the control at [from] (root px) to the point [to] gives (root - * px, asked each frame, so a target that moves is followed). [color] null is the palette's signal - * blue. [onHalf] runs as it passes the top of its arc, [onLand] once it lands. - */ internal class Flight( val from: Rect, val to: Density.() -> Offset?, @@ -190,27 +156,20 @@ internal class Flight( private companion object { var next = 0L } } -/** Dots flying anywhere outside the Stores page (which keeps its own): a hop, a swap, a toggle. */ internal object Flights { val all = mutableStateListOf<Flight>() - /** Sends [f], or with animations off just runs its ends. */ fun fly(f: Flight) { if (Motion.scale == 0f) { f.onHalf(); f.onLand(); return } all += f } } -/** [Flights] drawn over everything, in the space of an element at [origin] (root px). */ @Composable internal fun FlightsLayer(origin: Offset) { for (f in Flights.all.toList()) key(f.id) { FlightDot(f, origin) { Flights.all.remove(f) } } } -/** - * One dot: the control squashes into a 14dp dot (width first, a little bloop), which then flies - * on an arc to its target, stretched along its path like the focus ring's drop. - */ @Composable internal fun FlightDot(f: Flight, origin: Offset, onDone: () -> Unit) { val pal = LocalPalette.current @@ -232,7 +191,6 @@ internal fun FlightDot(f: Flight, origin: Offset, onDone: () -> Unit) { val b = (f.to(this) ?: return@Canvas) - origin val v = t.value if (v < 1f) { - // Squash: the width goes first, then the height, into the dot; a bloop at the end. val w = lerp(from.width, 2 * r, (v * 1.4f).coerceAtMost(1f)) val h = lerp(from.height, 2 * r, ((v - 0.25f) / 0.75f).coerceIn(0f, 1f)) val k = 1f - 0.15f * sin(PI * v).toFloat() @@ -254,10 +212,6 @@ internal fun FlightDot(f: Flight, origin: Offset, onDone: () -> Unit) { } } -/** - * A row that scrolls sideways fades out at an edge it has more past, instead of slicing a pill or - * a card through: the content's own alpha, so whatever is behind the row shows through. - */ internal fun Modifier.edgeFade(scroll: androidx.compose.foundation.ScrollState, width: Dp = 28.dp): Modifier = this.graphicsLayer { compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen } .drawWithContent { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SheetRows.kt b/app/src/main/java/com/droiddeck/launcher/ui/SheetRows.kt index ac6364a2a..2b2ecf904 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SheetRows.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SheetRows.kt @@ -5,10 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.droiddeck.launcher.R -// The rows the lifted sheets add to what the session settings already had: frame generation, -// Performance and the link to the GPU drivers on Components. -/** Frame generation, moved here from Setup: it is how Play's games are drawn. */ @Composable internal fun FrameGenGroup(s: FrontEndState, a: FrontEndActions) { val host = rememberMenuHost() @@ -22,10 +19,6 @@ internal fun FrameGenGroup(s: FrontEndState, a: FrontEndActions) { } } -/** - * Performance (CPU core assignment), opened from the sheet's Session chip: its page floods out of - * this row, and Back draws it into the row again with the sheet lifted where it was. - */ @Composable internal fun PerformanceLink(a: FrontEndActions, rail: String, tab: String) { val id = "sheet:performance" @@ -41,10 +34,6 @@ internal fun PerformanceLink(a: FrontEndActions, rail: String, tab: String) { } } -/** - * The GPU drivers in use, read only, and a link to where they are set: A hops to Components with - * the GPU drivers layer chosen, and B there comes back to this row. - */ @Composable internal fun DriversLink(a: FrontEndActions, summary: String?, rail: String, back: String) { val id = "sheet:gpu-drivers" diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt b/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt index f6952fc65..a0a306270 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SteamHome.kt @@ -67,23 +67,14 @@ import coil.size.Scale import coil.transform.RoundedCornersTransformation import kotlin.math.roundToInt -/** From this many games the Steam tab is a wall; fewer would only repeat the same covers. */ private const val WALL_MIN_GAMES = 4 private const val DRIFT_STEP_NS = 15_000_000L -/** - * The Steam tab: the library as a slowly drifting, tilted wall of capsules (a few games instead - * lean on their own; none, the DroidDeck mark wandering a blank wall), and over it the wordmark and the one thing to do here - - * Play. Everything about a single game lives on the Games tab. - */ @Composable internal fun SteamHome(s: FrontEndState, a: FrontEndActions, modifier: Modifier) { - // A fixed order, ties included: the wall places each game by its position, so an order that - // came out differently from one read of the library to the next moved every capsule. val games = remember(s.steamGames) { s.steamGames.sortedWith(compareByDescending<Library.SteamGame> { it.lastPlayed }.thenBy { it.gameId }) } val narrow = LocalNarrowPane.current - // Where the words and buttons sit, for the empty library's wandering mark to keep off. var words by remember { mutableStateOf<Rect?>(null) } val mode = com.droiddeck.launcher.session.SessionService.MODE_STEAM val tabs = modeSheetTabs(steam = true).map { it to sheetTabLabel(it) } @@ -92,7 +83,6 @@ internal fun SteamHome(s: FrontEndState, a: FrontEndActions, modifier: Modifier) frameGen = { FrameGenGroup(s, a) }, performance = { PerformanceLink(a, "steam", SheetTab.SESSION) }, drivers = { summary -> DriversLink(a, summary, "steam", back) }, - // Offline mode lives with Steam's other settings; Setup only checks the account. account = { host -> SettingsGroup(stringResource(R.string.setup_account)) { ToggleRow( @@ -110,8 +100,6 @@ internal fun SteamHome(s: FrontEndState, a: FrontEndActions, modifier: Modifier) start = if (narrow) 20.dp else 44.dp, bottom = if (narrow) 20.dp else 44.dp, background = { when { - // The mark wanders over this fade, so it sinks with the wall: drawn a little oversized, - // it still reaches the pane's edges at the sink's 0.95. games.isEmpty() -> EmptyLibrary(avoid = words) { Box(Modifier.fillMaxSize().graphicsLayer { scaleX = 1.06f; scaleY = 1.06f }) { WallFade() } } games.size < WALL_MIN_GAMES -> CapsuleFan(games) else -> CapsuleWall(games) @@ -124,19 +112,16 @@ internal fun SteamHome(s: FrontEndState, a: FrontEndActions, modifier: Modifier) modifier = Modifier.onPlaced { words = it.boundsInParent() }, ) { Wordmark(if (narrow) 30.dp else 40.dp) - // Said only when something stands between Play and Steam; a ready runtime needs no words. if (s.busy || !s.ready || (s.available != null && s.available != s.installed)) RuntimeChip(s) } }, button = { click -> - // Enabled without a runtime: the session's loading screen installs it first. PrimaryButton(stringResource(R.string.games_play_steam), enabled = !s.busy, main = true, large = true, icon = Icons.Filled.PlayArrow, modifier = Modifier.testTag("play-steam"), onClick = click) }, rows = { tab -> a.modeSheet(mode, tab, extras) }, ) } -/** The wall fades out behind the words, and toward the bottom where the buttons sit. */ @Composable internal fun WallFade() { val colors = MaterialTheme.colorScheme @@ -153,11 +138,6 @@ internal fun WallFade() { Box(Modifier.fillMaxSize().background(Brush.verticalGradient(0.55f to Color.Transparent, 1f to colors.background.copy(alpha = 0.9f)))) } -/** - * Columns of capsules tilted a few degrees, alternate columns drifting up and down. Each column - * holds its run of covers twice and moves by exactly one run, so the loop has no seam. An empty - * library draws the same wall in blank capsules, slower. - */ @Composable internal fun CapsuleWall( games: List<Library.SteamGame>, @@ -169,37 +149,20 @@ internal fun CapsuleWall( TiltedWall(games.size, driftMs, modifier) { i -> if (games.isEmpty()) blank else WallCapsule(artBrush(hueOf(games[i].name)), games[i].art) } } -/** - * One capsule of a [TiltedWall]: its ground, the art laid over it once loaded, and an outline. A - * tile that is an app rather than a cover carries an [icon] instead (a drawable id or an image), - * drawn whole at the tile's centre, and a window's [bar] along its foot. - */ internal class WallCapsule(val ground: Brush, val art: Any? = null, val edge: Color? = null, val icon: Any? = null, val bar: Color? = null) -/** - * [CapsuleWall]'s tilted, drifting columns for any [count] of capsules; [capsule] describes number i - * (0 until [count], or 0 for each blank one when [count] is 0). Each column is one drawing with the - * covers' corners cut into their bitmaps: a rounded clip under the tilt is rasterised on the CPU, - * for every capsule on every frame. - */ @Composable internal fun TiltedWall( count: Int, driftMs: Int, modifier: Modifier = Modifier, - /** One tile's size, the gap between tiles and their corner: portrait capsules unless a wall says otherwise. */ capW: androidx.compose.ui.unit.Dp = 112.dp, capH: androidx.compose.ui.unit.Dp = 168.dp, gap: androidx.compose.ui.unit.Dp = 14.dp, corner: androidx.compose.ui.unit.Dp = 10.dp, capsule: (index: Int) -> WallCapsule, ) { - // Animations off in the system settings: a still wall. val still = Motion.scale == 0f - // The wall moves a few pixels a step. Stepped on frames at most every 15 ms it looks the same as - // stepped on every frame, which on a 120 Hz screen drew it twice as often. The phase advances by - // the time since the last step over the period now, so a period that changes (Flathub's wall - // slowing while it sets up) changes the speed without the wall jumping. val drift = remember { mutableFloatStateOf(0f) } val period = androidx.compose.runtime.rememberUpdatedState(driftMs) if (!still) LaunchedEffect(Unit) { @@ -214,7 +177,6 @@ internal fun TiltedWall( } val density = LocalDensity.current BoxWithConstraints(modifier.fillMaxSize()) { - // Centred on the page and big enough that, once tilted, it still covers every corner. val tilt = Math.toRadians(13.0) val cos = kotlin.math.cos(tilt).toFloat() val sin = kotlin.math.sin(tilt).toFloat() @@ -263,7 +225,6 @@ internal fun TiltedWall( } } -/** A cover sized for a capsule, its corners already cut, so it draws without a clip. */ @Composable private fun rememberCapsuleArt(model: Any, size: Size, radius: Float): Painter { val context = LocalContext.current @@ -277,7 +238,6 @@ private fun rememberCapsuleArt(model: Any, size: Size, radius: Float): Painter { ) } -/** An app's icon for a wall tile: a bundled drawable, or an image Coil loads. */ @Composable private fun rememberIcon(model: Any): Painter = if (model is Int) androidx.compose.ui.res.painterResource(model) else rememberAsyncImagePainter(model) @@ -286,7 +246,6 @@ private fun DrawScope.drawCapsule(look: WallCapsule, art: Painter?, radius: Floa drawRoundRect(look.ground, size = size, cornerRadius = CornerRadius(radius)) look.bar?.let { drawRect(it, Offset(0f, size.height - barH), Size(size.width, barH)) } if (art != null && look.icon != null) { - // An app's icon whole at the tile's centre (above a window's bar), not cropped over it. val side = minOf(size.width, size.height - (if (look.bar != null) barH else 0f)) * 0.5f val cy = (size.height - (if (look.bar != null) barH else 0f)) / 2f translate((size.width - side) / 2f, cy - side / 2f) { with(art) { draw(Size(side, side)) } } @@ -296,7 +255,6 @@ private fun DrawScope.drawCapsule(look: WallCapsule, art: Painter?, radius: Floa } } -/** One to three games, each a large capsule leaning on the others and floating a little. */ @Composable private fun CapsuleFan(games: List<Library.SteamGame>) { val pal = LocalPalette.current @@ -306,7 +264,6 @@ private fun CapsuleFan(games: List<Library.SteamGame>) { transition.animateFloat(0f, 1f, infiniteRepeatable(tween(ms, easing = FastOutSlowInEasing), RepeatMode.Reverse), label = "float") } val still = Motion.scale == 0f - // Where each sits (share of the page), its height (share of the page's) and its lean; front first. val slots = listOf( FanSlot(0.54f, 0.14f, 0.62f, -9f), FanSlot(0.70f, 0.24f, 0.56f, 5f), diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StepOut.kt b/app/src/main/java/com/droiddeck/launcher/ui/StepOut.kt index 7940d8dbe..7b902eeef 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StepOut.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StepOut.kt @@ -65,17 +65,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.roundToInt -// End session's confirm, for any control: a pull drops out of the control and a wider box steps -// out under it for the choices, one edge flush with the control. Stop draws it from its pill on -// the drawer; the stores draw it from Install, the Library tab and a download's cancel. -/** - * The stair-step outline: the pill [p] on top, a box under it from [left] to the pill's right edge - * and down to [bottom], the pull between them [pullTop] (the box's top). Right edges are flush and - * the inside corner where the pull meets the box is rounded the other way. While the box is still - * no wider than the pill it is just the pill, stretched down. [pillCorner] is the control's own - * corner: a pill's is half its height. - */ internal fun stepPath( path: Path, p: Rect, left: Float, bottom: Float, pullTop: Float, box: Float, fillet: Float, pillCorner: Float = minOf(p.height / 2f, p.width / 2f), @@ -112,13 +102,10 @@ internal fun stepPath( path.close() } -/** [stepPath] for a box that steps out to the right of the pill, out to [right], left edges flush. */ internal fun stepPathRight( path: Path, p: Rect, right: Float, bottom: Float, pullTop: Float, box: Float, fillet: Float, pillCorner: Float = minOf(p.height / 2f, p.width / 2f), ) { - // Drawn as the left-hand outline about the pill's centre, which maps the pill onto itself, - // then mirrored back: x -> (p.left + p.right) - x. val axis = p.left + p.right stepPath(path, p, axis - right, bottom, pullTop, box, fillet, pillCorner) path.transform(Matrix().apply { this[0, 0] = -1f; this[3, 0] = axis }) @@ -126,17 +113,12 @@ internal fun stepPathRight( private fun lerpF(a: Float, b: Float, t: Float) = a + (b - a) * t -/** Which way the box steps out of its control. */ internal enum class StepSide { - /** Out to the left, right edges flush (a control at the right: Stop, a row's cancel). */ Left, - /** Out to the right, left edges flush (a control at the left: Install, a tab). */ Right, } -/** The choices' side of a [StepOut]: each piece fades in on its own beat, and [first] takes focus. */ internal class StepOutScope(private val items: List<Animatable<Float, AnimationVector1D>>, val first: FocusRequester, val open: Boolean, val maxHeight: Dp) { - /** Piece [i] (0 first) of the box: it fades and rises in after the box has stepped out. */ fun Modifier.stepItem(i: Int): Modifier = graphicsLayer { val v = items[i.coerceIn(0, items.lastIndex)].value alpha = v @@ -144,17 +126,10 @@ internal class StepOutScope(private val items: List<Animatable<Float, AnimationV } } -/** Room between the control and the box that steps out under it: the pull. */ private val OutPull = 6.dp private val OutBoxCorner = 16.dp private val OutFillet = 10.dp -/** - * A confirm or a short list grown out of the control at [pill] (px, in this element's space, which - * should fill the screen area it may cover): a pull the control's width drops out of it, then a - * box steps out to [side] for [content]. [onDismiss] (B, a tap outside) asks to fold it; it folds - * back width first once [open] is false, then [onClosed]. Exactly StopConfirm's springs. - */ @OptIn(ExperimentalComposeUiApi::class) @Composable internal fun StepOut( @@ -181,13 +156,9 @@ internal fun StepOut( var grown by remember { mutableStateOf(false) } var measured by remember { mutableStateOf<IntSize?>(null) } val first = remember { FocusRequester() } - // The control focus was on as it opened: focus goes back to it as it folds, not to wherever - // the screen puts a pad with nothing focused (the rail). val glide = LocalFocusGlide.current val opener = remember { arrayOfNulls<GlideSource>(1) } val openNow = androidx.compose.runtime.rememberUpdatedState(open) - // While the box grows or folds the ring keeps out (it flew across the moving shape) and comes - // back on whatever has focus once it has settled: the first choice, or the control again. var moving by remember { mutableStateOf(false) } if (moving) VeilRing() val pull = with(density) { OutPull.toPx() } @@ -199,13 +170,10 @@ internal fun StepOut( val availableHeight = with(density) { maxOf(hostHeight - pill.bottom - pull, pill.top - pull).coerceAtLeast(44.dp.toPx()).toDp() } - // No room under the control (a row near the foot of the screen): it steps out upward - // instead, the same outline mirrored about the control. val up = measured?.let { pill.bottom + pull + it.height > hostHeight && pill.top - pull - it.height >= 0f } ?: false val room = if (side == StepSide.Left) pill.right else hostWidth - pill.left val maxW = with(density) { maxWidth.toPx() }.coerceAtMost(room).coerceAtLeast(pill.width) LaunchedEffect(open, measured) { - // Asked to close before it ever measured: nothing to fold. if (!open && !grown) { onClosed(); return@LaunchedEffect } val c = measured ?: return@LaunchedEffect val rest = if (side == StepSide.Left) pill.left else pill.right @@ -221,8 +189,6 @@ internal fun StepOut( coroutineScope { launch { dim.animateTo(1f, Motion.tw(260)) } launch { tint.animateTo(1f, Motion.tw(200)) } - // Down and out together, on one spring: down first drew the control as a stick - // that then snapped out sideways. moving = true val down = launch { bottom.animateTo(boxBottom, Motion.sp(0.8f, 380f)) } val across = launch { edge.animateTo(out, Motion.sp(0.8f, 380f)) } @@ -242,8 +208,6 @@ internal fun StepOut( coroutineScope { pieces.forEach { launch { it.animateTo(0f, Motion.tw(90)) } } launch { delay(Motion.ms(60).toLong()); dim.animateTo(0f, Motion.tw(300)) } - // In and up together on a timed ease: width first left a tall tail under the - // control, and a damped spring's tail kept a nub of it on screen. moving = true launch { delay(Motion.ms(60).toLong()); edge.animateTo(rest, Motion.tw(240)) } launch { delay(Motion.ms(60).toLong()); bottom.animateTo(pill.bottom, Motion.tw(240)) } @@ -255,11 +219,9 @@ internal fun StepOut( } } - // Opened by touch and then answered with a pad: the pad starts in the box, not behind it. LaunchedEffect(open, grown, inputMode.inputMode) { if (open && grown && inputMode.inputMode == InputMode.Keyboard) { withFrameNanos { }; runCatching { first.requestFocus() } } } - // The rest of the screen dims behind it, and a tap there folds it. Never a pad's stop. Box( Modifier.fillMaxSize().graphicsLayer { alpha = dim.value }.background(Color(0x59000000)) .focusProperties { canFocus = false } @@ -276,11 +238,6 @@ internal fun StepOut( if (side == StepSide.Left) stepPath(path, pill, edge.value, bottom.value, pill.bottom + pull, boxCorner, fillet, corner) else stepPathRight(path, pill, edge.value, bottom.value, pill.bottom + pull, boxCorner, fillet, corner) if (up) path.transform(Matrix().apply { this[1, 1] = -1f; this[3, 1] = pill.top + pill.bottom }) - // Barely out of the control, the pull and its fillet drew a hook off the control's foot: - // the outline comes in over the first part of the way out (and goes over the last part - // of the way back), by how far along it is. A share of the way, not a distance: a box - // no wider than its control (a confirm under a full-width row) never got 24dp out, and - // was never drawn at all. val a = measured?.let { c -> val rest = if (side == StepSide.Left) pill.left else pill.right val out = if (side == StepSide.Left) (pill.right - c.width).coerceAtLeast(0f) else (pill.left + c.width).coerceAtMost(hostWidth) @@ -294,7 +251,6 @@ internal fun StepOut( drawPath(path, lerp(hotFill, restFill, tint.value), alpha = a) drawPath(path, accent.copy(alpha = lerpF(0.9f, 0.55f, tint.value) * a), style = Stroke(rim)) } - // The control's label rides on top, as the pull's handle. if (grown) Box( contentAlignment = Alignment.Center, modifier = Modifier.offset { IntOffset(pill.left.roundToInt(), pill.top.roundToInt()) } @@ -308,7 +264,6 @@ internal fun StepOut( .heightIn(max = availableHeight) .onSizeChanged { if (measured != it) measured = it } .controllerBack(onDismiss) - // A pad stays in the box until it is answered or folded, as it did in Stop's dialog. .focusProperties { exit = { if (openNow.value) FocusRequester.Cancel else FocusRequester.Default } } .focusGroup() .verticalScroll(rememberScrollState()) @@ -317,10 +272,6 @@ internal fun StepOut( } } -/** - * One choice in a [StepOut]: a capsule that fills when focused, like Stop's Cancel and Stop. [danger] - * draws it in red; [icon] and [trailing] make it a row (a place to install to and its free space). - */ @Composable internal fun StepChoice( label: String, @@ -357,7 +308,6 @@ internal fun StepChoice( } } -/** A [StepOut]'s heading line, the first piece to fade in. */ @Composable internal fun StepOutScope.StepTitle(text: String) { Text( @@ -366,7 +316,6 @@ internal fun StepOutScope.StepTitle(text: String) { ) } -/** The choices of a [StepOut] stacked, each the box's width. */ @Composable internal fun StepList(content: @Composable ColumnScope.() -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(6.dp), content = content) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/Steps.kt b/app/src/main/java/com/droiddeck/launcher/ui/Steps.kt index d0b9369a3..18a4b9703 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/Steps.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/Steps.kt @@ -7,12 +7,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset -// The front end's confirms and short lists, each stepping out of the control that asked for it -// (StepOut): a sheet's Uninstall, a Components swap, a remove. The Stores page keeps its own, in -// StoresMotion; everything else asks here and is drawn over the whole screen. internal object Steps { - /** The confirm or list stepping out of a control now, and whether it is open. */ var step by mutableStateOf<StepAsk?>(null) private set var open by mutableStateOf(false) @@ -24,10 +20,8 @@ internal object Steps { open = true } - /** Folds the current one back into its control. */ fun fold() { open = false } - /** Drops it at once: the control it came from has gone. */ fun drop() { step?.let { step = null; it.onDismiss() } open = false @@ -39,11 +33,9 @@ internal object Steps { } } -/** [Steps] drawn over an element at [origin] (root px) that fills the screen. */ @Composable internal fun StepsLayer(origin: Offset) { val ask = Steps.step ?: return - // Back (B, or the system's) folds what is stepped out, before it reaches the page under it. androidx.activity.compose.BackHandler(enabled = Steps.open) { Steps.fold() } key(ask) { StepOut( diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoreAchievementsUi.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoreAchievementsUi.kt index 0e87128b4..f033aa9dc 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoreAchievementsUi.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoreAchievementsUi.kt @@ -73,7 +73,6 @@ internal class AchievementsState(val list: List<Achievement>?, val reason: Reaso companion object { val READING = AchievementsState(null, null, true) } } -/** The cached list at once, then the store's answer (the cache stays if the store fails). */ @Composable internal fun rememberAchievements(game: StoreAchievements.Game?): AchievementsState? { val ctx = LocalContext.current.applicationContext @@ -134,14 +133,12 @@ private fun AchievementGroupLabel(label: String) { } } -/** Unlocked (newest first), locked, then hidden ones not yet unlocked. */ private fun groups(list: List<Achievement>): List<Pair<Int, List<Achievement>>> = listOf( R.string.achievements_group_unlocked to list.filter { it.unlocked }.sortedByDescending { it.unlockedAt }, R.string.achievements_group_locked to list.filter { !it.unlocked && !it.hidden }, R.string.achievements_group_hidden to list.filter { !it.unlocked && it.hidden }, ).filter { it.second.isNotEmpty() } -/** Focusable so the pad can scroll the list. */ @Composable internal fun AchievementRow(a: Achievement, compact: Boolean, modifier: Modifier = Modifier) { val colors = MaterialTheme.colorScheme @@ -200,7 +197,6 @@ internal fun StoreAchievementsSection(game: StoreAchievements.Game) { internal fun StoreAchievementsSectionContent(label: String, state: AchievementsState) { val colors = MaterialTheme.colorScheme val list = state.list - // Hidden for games without achievements. if (list.isNullOrEmpty() && !state.reading && state.reason != Reason.SIGNED_OUT && state.reason != Reason.UNREACHABLE) return val narrow = LocalNarrowPane.current Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth().padding(top = 18.dp)) { @@ -278,7 +274,7 @@ internal fun SessionAchievementsPanel(label: String, state: AchievementsState, t verticalArrangement = Arrangement.spacedBy(6.dp), ) { achievementItems(list, short, first) } LaunchedEffect(list.isNotEmpty()) { - withFrameNanos {} // rows lay out a frame after composition + withFrameNanos {} runCatching { first.requestFocus() } } } @@ -286,7 +282,6 @@ internal fun SessionAchievementsPanel(label: String, state: AchievementsState, t } } -/** [progress]: (unlocked, total), or null when unknown. */ @Composable internal fun DrawerAchievementsRow(progress: Pair<Int, Int>?, modifier: Modifier = Modifier, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoreChips.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoreChips.kt index 410d44e99..ec12d19af 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoreChips.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoreChips.kt @@ -17,16 +17,12 @@ import com.droiddeck.launcher.R import com.droiddeck.launcher.frontend.Library import com.droiddeck.launcher.stores.Store -// Where a game came from, as a small coloured chip: Steam, one of the stores, or an added folder. -/** Flathub's own accent, as each store has one, and the ink that reads on it. */ internal val FlathubTeal = Color(0xFF3FB8AF) internal val FlathubInk = Color(0xFF04211F) -/** The Store rail section's source id, for its colours. */ internal const val FLATHUB = "flathub" -/** The chip's colours per source: a tinted ground and a readable ink on it, one pair per store. */ internal class SourceColours(val fill: Color, val ink: Color, val dot: Color) internal fun sourceColours(source: String): SourceColours = when (source) { @@ -47,7 +43,6 @@ internal fun sourceLabel(source: String): String = when (source) { else -> stringResource(R.string.source_added) } -/** The source chip itself; [small] for a list row, the regular size for a hero or a card. */ @Composable internal fun SourceChip(source: String, small: Boolean = false, modifier: Modifier = Modifier) { val c = sourceColours(source) @@ -59,7 +54,6 @@ internal fun SourceChip(source: String, small: Boolean = false, modifier: Modifi ) } -/** A status chip in the stores' colours: green for done ("Installed"), blue for busy, amber for a warning. */ @Composable internal fun StateChip(text: String, tone: ChipTone, small: Boolean = false) { val pal = LocalPalette.current diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoreGameFacts.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoreGameFacts.kt index f5967bdac..d57b63b5e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoreGameFacts.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoreGameFacts.kt @@ -69,13 +69,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -// A store game's facts under its hero: who made it and when, from the store; the Metacritic score -// and HowLongToBeat's times, looked up elsewhere by name. The store's facts are there at once; the -// looked-up ones hold their place while they come and step out if there is nothing to show. internal class GameScores(val metacritic: Lookup<GameFacts.Metacritic>, val hltb: Lookup<GameFacts.Hltb>) -/** Both lookups at once and off the main thread, each landing on its own; what memory already has shows without a wait. */ @Composable internal fun rememberGameScores(title: String?): GameScores { val ctx = LocalContext.current.applicationContext @@ -89,16 +85,10 @@ internal fun rememberGameScores(title: String?): GameScores { return GameScores(metacritic, hltb) } -/** - * The facts: who made it, when, and what critics made of it on the first row; how long it takes, - * across the page, on the second. A looked-up tile holds its place while it asks and steps out if - * the site does not have the game; nothing is drawn when there is nothing to say. - */ @OptIn(ExperimentalLayoutApi::class) @Composable internal fun StoreGameFacts(developer: String, publisher: String, releaseDate: String, scores: GameScores, modifier: Modifier = Modifier) { val released = remember(releaseDate) { GameFacts.releaseLabel(releaseDate) } - // A studio that publishes itself is named once. val publishedBy = publisher.takeUnless { it.isBlank() || it.trim().equals(developer.trim(), ignoreCase = true) } val firstRow = developer.isNotBlank() || publishedBy != null || released != null || scores.metacritic != Lookup.None if (!firstRow && scores.hltb == Lookup.None) return @@ -123,10 +113,8 @@ internal fun StoreGameFacts(developer: String, publisher: String, releaseDate: S } } -/** A looked-up tile: in from the start (holding its place while it asks), out with a fade when there was nothing. */ @Composable private fun <T> Looked(lookup: Lookup<T>, exit: ExitTransition, content: @Composable (Lookup<T>) -> Unit) { - // The last answer worth drawing, so the tile does not blank as it leaves. var shown by remember { mutableStateOf(lookup) } if (lookup != Lookup.None) shown = lookup AnimatedVisibility(visible = lookup != Lookup.None, enter = fadeIn(), exit = fadeOut() + exit) { content(shown) } @@ -135,10 +123,6 @@ private fun <T> Looked(lookup: Lookup<T>, exit: ExitTransition, content: @Compos private val FactShape = RoundedCornerShape(12.dp) private val ValueHeight = 24.dp -/** - * A label over its value; [fill] takes the row's whole width, for a value laid out across it. With - * a [link] it is a control like any other on the page: focus, hover and A or a tap open the page. - */ @Composable private fun FactTile(label: String, fill: Boolean = false, link: String? = null, value: @Composable () -> Unit) { val pal = LocalPalette.current @@ -166,7 +150,6 @@ private fun FactTile(label: String, fill: Boolean = false, link: String? = null, private fun FactText(text: String) = Text(text, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis) -/** Metacritic's own bands: green from 75, yellow from 50, red below. */ internal fun metacriticColour(score: Int): Color = when { score >= 75 -> Color(0xFF66CC33) score >= 50 -> Color(0xFFFFCC33) @@ -180,7 +163,6 @@ private fun MetacriticBadge(score: Int) { } } -/** Each kind of run HowLongToBeat has times for, sharing the tile's width: the hours in full, what they are for beside them. */ @Composable private fun HltbTimes(t: GameFacts.Hltb) { val colors = MaterialTheme.colorScheme @@ -201,7 +183,6 @@ private fun HltbTimes(t: GameFacts.Hltb) { } } -/** Where a value will be, breathing while it is asked for. */ @Composable private fun Placeholder(width: Dp) { val pulse by rememberInfiniteTransition(label = "fact").animateFloat( @@ -210,7 +191,6 @@ private fun Placeholder(width: Dp) { Box(Modifier.width(width).height(16.dp).alpha(pulse).clip(RoundedCornerShape(5.dp)).background(LocalPalette.current.line2)) } -// Previews: every state the row can be in, at a TV pane's width and a narrow one. private val previewScores = GameScores( Lookup.Found(GameFacts.Metacritic(93, "https://www.metacritic.com/game/the-witcher-3-wild-hunt/")), @@ -272,7 +252,6 @@ private fun FactsPaperPreview() = FactsPreview(theme = Themes.PAPER) { StoreGameFacts("CD PROJEKT RED", "CD PROJEKT RED", "2015-05-19T00:00:00+02:00", previewScores) } -// The real hero reads the stores' live state, so this stands one in to show where the row sits. @Preview(name = "Game page, facts in place", widthDp = 800, heightDp = 520) @Composable private fun FactsOnPagePreview() = FactsPreview { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoreGridNavigation.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoreGridNavigation.kt index 5676929ba..d019bc009 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoreGridNavigation.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoreGridNavigation.kt @@ -1,6 +1,5 @@ package com.droiddeck.launcher.ui -/** Vertical controller moves keep the same column, including across lazy item boundaries. */ internal fun storeGridNeighbor(index: Int, count: Int, columns: Int, direction: Int): Int? { if (index !in 0 until count || columns < 1 || direction !in listOf(-1, 1)) return null return (index + columns * direction).takeIf { it in 0 until count } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StorePage.kt b/app/src/main/java/com/droiddeck/launcher/ui/StorePage.kt index 495fb9ba2..0facc0c07 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StorePage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StorePage.kt @@ -106,20 +106,13 @@ import com.droiddeck.launcher.store.FlathubApi import com.droiddeck.launcher.store.StoreState import kotlinx.coroutines.delay -// The Store rail section: Flathub's apps for this device's architecture, installed into the -// Linux runtime with Flatpak, and the ones already installed - in the stores' style, in Flathub's -// teal. Until Flatpak is set up it is the stores' sign-in hero: a wall of real app icons, one word, -// one button that becomes its own progress. Then a chip row, shelves of square cards that flood -// open into an app's page, and installs that fly home to the Installed chip. private const val MODE_DISCOVER = "discover" private const val MODE_INSTALLED = "installed" private const val MODE_UPDATES = "updates" -/** One app as a card shows it: Flathub's summary or what Flatpak says is installed. */ private class CardApp(val id: String, val name: String, val icon: Any?, val summary: String? = null, val verified: Boolean = false) -/** Where the Installed chip is, for an install's dot to land in, and how many have landed. */ private object FlathubMotion { var installedChip: Rect? = null var landed by mutableIntStateOf(0) @@ -131,8 +124,6 @@ internal fun StorePage(s: FrontEndState, a: FrontEndActions, modifier: Modifier) LaunchedEffect(s.ready) { StoreState.refresh(ctx) } LaunchedEffect(Unit) { StoreState.loadSections() } FlathubTheme { - // The storefront takes over from the hero once Flatpak is ready: the setup rail rings - // once, then the hero sinks away and the storefront rises. var front by remember { mutableStateOf(StoreState.ready && s.ready) } var ring by remember { mutableIntStateOf(0) } LaunchedEffect(StoreState.ready, s.ready) { @@ -156,11 +147,6 @@ internal fun StorePage(s: FrontEndState, a: FrontEndActions, modifier: Modifier) } } -/** - * Flatpak is not set up yet: the stores' sign-in shape in teal. A tilted wall of the popular apps' - * icons (Flathub's public pages need no Flatpak) drifts behind, fading toward the words; one big - * button sets it up, and stretches into its own progress while it does. - */ @Composable private fun FlathubHero(s: FrontEndState, ring: Int) { val ctx = LocalContext.current @@ -174,7 +160,6 @@ private fun FlathubHero(s: FrontEndState, ring: Int) { BoxWithConstraints(Modifier.fillMaxSize().padding(horizontal = if (narrow) 14.dp else 22.dp, vertical = if (narrow) 10.dp else 14.dp)) { val shape = RoundedCornerShape(20.dp) Box(Modifier.fillMaxWidth().height(maxHeight.coerceAtLeast(320.dp)).clip(shape).background(colors.background).border(1.dp, pal.line, shape)) { - // Slower while it sets up, as the empty wall drifts. androidx.compose.animation.Crossfade(icons, animationSpec = Motion.tw(600), label = "flathubWall") { tiles -> val ground = Brush.linearGradient(listOf(Color(0xFF0B2E2B), Color(0xFF0B2E2B))) val edge = FlathubTeal.copy(alpha = 0.18f) @@ -217,11 +202,6 @@ private fun FlathubHero(s: FrontEndState, ring: Int) { } } -/** - * Set up Flathub, and its own progress: while it sets up the button springs out to the full width - * and fills in teal from the left with how far it is, the stage and the figure inside it. Without - * a runtime it says what is missing instead. - */ @Composable private fun SetupButton(s: FrontEndState, busy: Boolean, full: Dp, ring: Int, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -240,7 +220,6 @@ private fun SetupButton(s: FrontEndState, busy: Boolean, full: Dp, ring: Int, on } return } - // A pad keeps its place on the rail that took the button's. if (ff != null) LaunchedEffect(Unit) { if (inputMode.inputMode == InputMode.Keyboard) focusSoon { ff.primary } } val width = androidx.compose.ui.unit.lerp(restW.coerceAtLeast(120.dp), full, grow) val shape = RoundedCornerShape(12.dp) @@ -269,7 +248,6 @@ private fun SetupButton(s: FrontEndState, busy: Boolean, full: Dp, ring: Int, on } } -// ---- storefront ------------------------------------------------------------------------------------ @OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class) @Composable @@ -285,9 +263,7 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { var openApp by rememberSaveable { mutableStateOf<String?>(null) } val updates = StoreState.updates if (mode == MODE_UPDATES && updates.isEmpty()) mode = MODE_INSTALLED - // Once a session is plenty: the Updates chip shows itself when there is something to update. LaunchedEffect(StoreState.ready) { if (StoreState.ready && !StoreState.updatesChecked && !StoreState.checkingUpdates) StoreState.checkUpdates(ctx) } - // A search runs a beat after typing stops, or as a category is picked. LaunchedEffect(query, category) { if (query.isBlank() && category == null) { StoreState.clearSearch(); return@LaunchedEffect } delay(350) @@ -298,7 +274,6 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { val focusManager = LocalFocusManager.current val keyboard = LocalSoftwareKeyboardController.current val view = LocalView.current - // Cards, where they are, for the page that opens from one. val cards = remember { HashMap<String, Rect>() } var viewportAt by remember { mutableStateOf(Offset.Zero) } var detail by remember { mutableStateOf<String?>(null) } @@ -308,8 +283,6 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { LaunchedEffect(openApp) { val id = openApp if (id != null) { - // The grid stays composed for the return animation. Release its native search field - // before covering it, so its focus ring and keyboard do not follow onto the app page. focusManager.clearFocus(force = true) view.findFocus()?.clearFocus() keyboard?.hide() @@ -323,20 +296,17 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { detail = null; leaving = false; detailFrom = null } } - // Back on the grid: focus goes to the card the page came from. LaunchedEffect(detail) { if (detail != null || inputMode.inputMode != InputMode.Keyboard || ff == null) return@LaunchedEffect val id = lastApp ?: return@LaunchedEffect ff.items["tile:store:$id"]?.let { req -> focusSoon { req } } } BackHandler(enabled = openApp != null) { openApp = null } - // An app's card on Desktop asked for its page. LaunchedEffect(StoreState.openRequest) { StoreState.openRequest?.let { openApp = it; StoreState.openRequest = null } } val covering = detail != null && !leaving val sink by animateFloatAsState(if (covering) 0.95f else 1f, if (covering) Motion.tw(480) else Motion.tw(460, 40), label = "flathubSink") val fade by animateFloatAsState(if (covering) 0f else 1f, if (covering) Motion.tw(320, 80) else Motion.tw(300, 100), label = "flathubFade") val modes = buildList { add(MODE_DISCOVER); add(MODE_INSTALLED); if (updates.isNotEmpty()) add(MODE_UPDATES) } - // LB / RB with a pad: focus goes to the new mode's chip; the cards it was on leave with the old mode. val seenMode = remember { arrayOf(mode) } LaunchedEffect(mode) { if (seenMode[0] == mode) return@LaunchedEffect @@ -354,7 +324,6 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { ) { Rise(0) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { - // A page of Desktop's: the way back to it, for a finger (B does the same). val show = LocalShowSection.current BackLink(stringResource(R.string.rail_desktop), compact = true) { show("desktop") } Box(Modifier.weight(1f)) { FlathubChips(mode, modes, StoreState.installed.size, updates.size) { mode = it } } @@ -386,7 +355,6 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { } if (from == null) body() else { - // The card's own icon, cropped over the tile as it grows: the page arrives in the app's colour. val art = coil.compose.rememberAsyncImagePainter( remember(icon) { coil.request.ImageRequest.Builder(ctx).data(icon).size(256, 256).build() }, ) @@ -397,10 +365,6 @@ internal fun Storefront(s: FrontEndState, a: FrontEndActions) { } } -/** - * Discover, Installed (with its count) and Updates (when there are any): one selection glides under - * them in teal; the Installed count pops as an install's dot lands in it. - */ @Composable private fun FlathubChips(mode: String, modes: List<String>, installed: Int, updates: Int, onPick: (String) -> Unit) { val pal = LocalPalette.current @@ -415,7 +379,6 @@ private fun FlathubChips(mode: String, modes: List<String>, installed: Int, upda val hot = rememberHot(src) val shape = RoundedCornerShape(99.dp) val on = m == mode - // Drawn by the row's glide by touch only; with a pad its own rest look and the bar. val under = on && androidx.compose.ui.platform.LocalInputModeManager.current.inputMode != androidx.compose.ui.input.InputMode.Keyboard Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), @@ -446,15 +409,10 @@ private fun FlathubChips(mode: String, modes: List<String>, installed: Int, upda } } } - // Over the chips: under them, their rest fill hid it as it crossed them. ChipGlide(modes.indexOf(mode), bounds[mode], tint) } } -/** - * The card width for a width: room for a name on two lines and a two-line summary under the art, - * so a long name wraps instead of being cut to a few letters. - */ private fun cardSize(width: Dp): Dp = when { width >= 700.dp -> 164.dp width >= 480.dp -> 148.dp @@ -470,7 +428,6 @@ private fun Discover( ModeHeader( stringResource(R.string.store_tab_discover), query, onQuery, stringResource(R.string.store_search_placeholder), ) { - // The categories as small pills, at the right of the header. val pills = rememberScrollState() Row( horizontalArrangement = Arrangement.spacedBy(6.dp), @@ -529,7 +486,6 @@ private fun UpdatesMode(cards: HashMap<String, Rect>, onOpen: (String) -> Unit) Rise(2) { CardGrid(apps.map { CardApp(it.id, it.name, it.icon ?: StoreState.details[it.id]?.icon ?: FlathubApi.iconUrl(it.id), it.summary) }, cards, onOpen) } } -/** A horizontal row of cards under a title, the stores' shelf; nothing while it is empty. */ @Composable private fun Shelf(title: String, apps: List<CardApp>?, first: Boolean, cards: HashMap<String, Rect>, onOpen: (String) -> Unit) { val colors = MaterialTheme.colorScheme @@ -546,7 +502,6 @@ private fun Shelf(title: String, apps: List<CardApp>?, first: Boolean, cards: Ha } } -/** The Installed and Updates modes, and search: a grid of the same cards. */ @Composable private fun CardGrid(apps: List<CardApp>, cards: HashMap<String, Rect>, onOpen: (String) -> Unit) { BoxWithConstraints(Modifier.fillMaxWidth().padding(top = 10.dp)) { @@ -562,11 +517,6 @@ private fun CardGrid(apps: List<CardApp>, cards: HashMap<String, Rect>, onOpen: } } -/** - * One app: its icon over a blurred, darkened copy of itself (darkened only, before Android 12), - * with one status on the art - Get, ✓, or how far an install is - and under it the name, on two - * lines if it needs them, and what the app is. - */ @Composable private fun AppCard(app: CardApp, size: Dp, first: Boolean, cards: HashMap<String, Rect>, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -595,7 +545,6 @@ private fun AppCard(app: CardApp, size: Dp, first: Boolean, cards: HashMap<Strin .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)).background(Color(0xFF0B2E2B)), ) { if (app.icon != null) { - // The icon's own colour, blurred and darkened, as the card's ground. AsyncImage( model = app.icon, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize().graphicsLayer { scaleX = 1.8f; scaleY = 1.8f; alpha = 0.55f } @@ -604,7 +553,6 @@ private fun AppCard(app: CardApp, size: Dp, first: Boolean, cards: HashMap<Strin Box(Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.35f))) AsyncImage(model = app.icon, contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier.size(artH * 0.62f)) } else Icon(Icons.Outlined.Apps, null, tint = colors.onSurfaceVariant, modifier = Modifier.size(artH * 0.5f)) - // One status, on the art: what A on the card leads to. Box(Modifier.align(Alignment.TopEnd).padding(6.dp)) { when { busy -> Box(Modifier.clip(RoundedCornerShape(99.dp)).background(Color.Black.copy(alpha = 0.55f)).padding(4.dp)) { @@ -633,7 +581,6 @@ private fun AppCard(app: CardApp, size: Dp, first: Boolean, cards: HashMap<Strin ) if (app.verified) Icon(Icons.Outlined.Verified, stringResource(R.string.store_verified), tint = pal.signal, modifier = Modifier.padding(top = 2.dp).size(13.dp)) } - // Two lines kept for the summary even when it is short, so cards in a row line up. Text( app.summary.orEmpty(), fontSize = 12.sp, lineHeight = 15.sp, color = colors.onSurfaceVariant, minLines = 2, maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(top = 3.dp), @@ -642,11 +589,6 @@ private fun AppCard(app: CardApp, size: Dp, first: Boolean, cards: HashMap<Strin } } -/** - * An app's page, flooded out of its card: its icon, name and what it is, one action (Install or - * Open, with Update beside it when there is one) and Website; then its screenshots and details. - * Install flies a dot to the Installed chip; Remove asks by stepping out of its button, in red. - */ @Composable private fun AppDetail(s: FrontEndState, a: FrontEndActions, id: String, onBack: () -> Unit) { val ctx = LocalContext.current @@ -688,7 +630,6 @@ private fun AppDetail(s: FrontEndState, a: FrontEndActions, id: String, onBack: enabled = StoreState.busy == null && s.ready && (d == null || d.arches.isEmpty() || "aarch64" in d.arches), ) { StoreState.install(ctx, id, name) - // The button squashes into a dot that flies to the Installed chip, whose count pops. val from = at if (from != null) Flights.fly(Flight(from, to = { FlathubMotion.installedChip?.center }, onLand = { FlathubMotion.landed++ })) else FlathubMotion.landed++ @@ -749,7 +690,6 @@ private fun AppDetail(s: FrontEndState, a: FrontEndActions, id: String, onBack: } } -/** Remove, which asks first: are you sure steps out of it in red, Keep first, so a stray A costs nothing. */ @Composable private fun RemoveButton(id: String, name: String, enabled: Boolean) { val ctx = LocalContext.current @@ -774,7 +714,6 @@ private fun RemoveButton(id: String, name: String, enabled: Boolean) { } } -/** A small text action at the right of a header. */ @Composable private fun SmallLink(text: String, enabled: Boolean = true, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -790,7 +729,6 @@ private fun SmallLink(text: String, enabled: Boolean = true, onClick: () -> Unit ) } -/** An app's icon from Flathub (a URL) or from its installed files; a placeholder until it loads. */ @Composable internal fun AppIcon(model: Any?, name: String, size: Int) { val colors = MaterialTheme.colorScheme @@ -800,7 +738,6 @@ internal fun AppIcon(model: Any?, name: String, size: Int) { } } -/** A category pill: small, focusable, and filled when it is the one in use. */ @Composable private fun PillButton(label: String, selected: Boolean, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt index e77bd70a1..1a3480f95 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresDetail.kt @@ -41,7 +41,6 @@ import com.droiddeck.launcher.stores.StoresState import com.droiddeck.launcher.stores.download.DownloadState import com.droiddeck.launcher.stores.formatBytes -// A store game's own page: hero, the one main action, how it is launched and where it lives. @Composable internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: FrontEndActions, onBack: () -> Unit) { @@ -53,7 +52,6 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro val item = (StoresState.library[store].orEmpty() + StoresState.shelves[store]?.all.orEmpty()).firstOrNull { it.id == id } val installed = StoresState.installedGame(store, id) val download = StoresState.download("${store.id}:$id")?.takeIf { it.isActive } - // Removal asks twice: one stray press of A should not cost a download. val title = item?.title ?: installed?.sidecar?.title ?: id Rise(0) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.padding(bottom = 12.dp)) { @@ -61,13 +59,10 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro SourceChip(store.id) } } - // Out of a card, the hero holds still in its place: the card's flood lands on it, and a hero - // still rising in sat below where the flood had measured it (the art showed twice). val hero: @Composable () -> Unit = { androidx.compose.runtime.DisposableEffect(Unit) { onDispose { StoresMotion.heroAt = null } } Box( Modifier.fillMaxWidth().heightIn(min = if (narrow) 150.dp else 180.dp) - // The card it opened from floods onto this, and draws back into the card from it. .onGloballyPositioned { StoresMotion.heroAt = it.boundsInRoot() } .clip(Shape16).background(artBrush(hueOf(title))), ) { @@ -75,7 +70,6 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro if (art != null) AsyncImage(model = art, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize()) Spacer(Modifier.matchParentSize().background(Brush.horizontalGradient(0f to colors.background.copy(alpha = 0.94f), 0.5f to colors.background.copy(alpha = 0.55f), 1f to Color.Transparent))) Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.align(Alignment.BottomStart).padding(horizontal = 22.dp, vertical = 18.dp)) { - // An install names where it went (internal storage, the card), a download where it is going. val state = when { installed != null -> stringResource(R.string.stores_eyebrow_installed) + " · " + com.droiddeck.launcher.stores.StoreInstallRoot.labelFor(ctx, installed.folder) download != null && download.location.isNotBlank() -> stringResource(R.string.stores_eyebrow_library) + " · " + download.location @@ -88,11 +82,6 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro Actions { when { installed != null -> { - // Launching, the prefix and the files belong to the Games tab, where every - // installed game has them; here an install only says it is in Steam and - // offers the one thing Games does not: removing it. - // The row keeps its shape: the chip stays, and Uninstall becomes its own - // progress in place while the files go; then the page reads Install again. ActionChip(stringResource(R.string.stores_in_steam), ok = true) val gone = StoresState.uninstalling["${store.id}:$id"] if (gone != null) SecondaryButton(stringResource(R.string.stores_uninstalling), enabled = false, progress = gone) {} @@ -123,7 +112,6 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro } if (StoresMotion.heroLands) hero() else Rise(1) { hero() } if (store == Store.GOG || store == Store.EPIC) { - // An owned game resolves to its library entry; the storefront's copy of it may know more. val listed = StoresState.shelves[store]?.all.orEmpty().firstOrNull { it.id == id } val developer = item?.developer?.ifBlank { null } ?: listed?.developer.orEmpty() val publisher = item?.publisher?.ifBlank { null } ?: listed?.publisher.orEmpty() @@ -131,8 +119,6 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro val scores = rememberGameScores(title.takeIf { it != id }) Rise(2) { StoreGameFacts(developer, publisher, releaseDate, scores) } } - // Plain text only, and nothing at all when the store sent a template key instead of words. - // A description that is only the title again (some stores fill it so) is no description. val description = item?.let { com.droiddeck.launcher.stores.cleanStoreText(it.description) }.orEmpty() .takeUnless { it.trim().equals(title.trim(), ignoreCase = true) }.orEmpty() if (description.isNotBlank()) Rise(3) { @@ -149,10 +135,6 @@ internal fun StoreGameDetail(store: Store, key: String, s: FrontEndState, a: Fro if (achievements != null) Rise(4) { StoreAchievementsSection(achievements) } } -/** - * Uninstall that asks the way Cancel does: a confirm steps out of it, naming the game and what - * goes (its size on disk), with Keep first so a stray A does nothing. - */ @Composable private fun UninstallButton(title: String, game: com.droiddeck.launcher.stores.InstalledStoreGame, enabled: Boolean, onUninstall: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -165,8 +147,6 @@ private fun UninstallButton(title: String, game: com.droiddeck.launcher.stores.I Box(Modifier.onGloballyPositioned { placed[0] = it }) { SecondaryButton(remove, enabled = enabled) { val at = placed[0]?.takeIf { it.isAttached }?.boundsInRoot() ?: return@SecondaryButton - // It starts once the confirm has folded back into the button: started at once, the button - // turned into the progress under the fold and it drew back into a control that had gone. var confirmed = false StoresMotion.ask(StepAsk(anchor = at, side = StepSide.Right, accent = colors.error, items = 3, pillCorner = 12.dp, maxWidth = 340.dp, onDismiss = { if (confirmed) onUninstall() }, handle = { @@ -183,10 +163,6 @@ private fun UninstallButton(title: String, game: com.droiddeck.launcher.stores.I } } -/** - * Install (or Resume install) that says where it sits when pressed: where to install grows out of - * it, and the download it starts leaves it as a dot for the Downloads chip. - */ @Composable private fun InstallButton(label: String, onInstall: () -> Unit) { val placed = remember { arrayOfNulls<androidx.compose.ui.layout.LayoutCoordinates>(1) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt index 47b356e7d..d332ee0e5 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresDownloads.kt @@ -92,9 +92,7 @@ import com.droiddeck.launcher.stores.download.StoreDownloadTier import com.droiddeck.launcher.stores.formatBytes import com.droiddeck.launcher.stores.formatSpeed -// The Downloads chip: one queue for the three stores, its settings and the engine log. -/** The queue. Its settings (speed tier, downloads at a time) live in the chip row's cog only. */ @Composable internal fun StoresDownloadsPane(s: FrontEndState, a: FrontEndActions) { DownloadList(s, a) @@ -106,21 +104,12 @@ private fun DownloadList(s: FrontEndState, a: FrontEndActions) { val pal = LocalPalette.current val scope = rememberCoroutineScope() val entries = StoresState.downloads - // Clearing: the finished rows go into the divider's X one by one, bottom first, and only then - // leave the queue. var clearing by remember { mutableStateOf(false) } - // Under way on top; finished below a line whose X clears them all (rows only - a failed - // download's kept files stay until its game page's Clear). - // Under way first, then paused, then the queue in its order: a queued row sat above the one running. val (active, finished) = entries.partition { it.isActive }.let { (on, off) -> on.sortedWith(compareBy<DownloadEntry>({ when (it.state) { DownloadState.RUNNING -> 0; DownloadState.PAUSED -> 1; else -> 2 } }, { it.queuePosition })) to off } val finishedShown = if (clearing) emptyList() else finished - // Installs that stopped without the queue knowing (the app closed or was killed): the queue - // lives in memory, so after a restart they were only on their game's page. They resume here too. val queued = entries.mapTo(HashSet()) { it.key } - // Matched against each store's library: one not read yet (Downloads opened first) is read from - // its cache, quietly, so the rows are there without visiting the store. val ctx = LocalContext.current val missing = StoresState.unfinished.map { it.store }.filter { StoresState.library[it] == null }.toSet() LaunchedEffect(missing) { missing.forEach { StoresState.backend(it)?.syncLibrary(ctx, false) } } @@ -135,9 +124,6 @@ private fun DownloadList(s: FrontEndState, a: FrontEndActions) { Text(stringResource(R.string.stores_downloads_empty), fontSize = 13.sp, color = colors.onSurfaceVariant) } } - // The list changes only when rows come, go or change state; each row reads its own progress. - // A row pours out of the count's side (the right), and leaves the way it came; one that - // installed stays a beat for its bar to gather into Play first. AnimatedRows( active, key = { it.key }, enter = { i, later -> @@ -178,9 +164,7 @@ private fun DownloadList(s: FrontEndState, a: FrontEndActions) { } AnimatedRows( finishedShown, key = { it.key }, - // One that just finished joins after its row above has gathered; the rest are simply there. enter = { i, later -> if (later) expandVertically(Motion.tw(300, GATHER_HOLD_MS)) + fadeIn(Motion.tw(220, GATHER_HOLD_MS + 50)) else fadeIn(Motion.tw(300, 60 * i.coerceAtMost(5))) }, - // Into the X at the top right, bottom row first. exit = { _, i, n -> val wait = 60 * (n - 1 - i).coerceIn(0, 8) scaleOut(Motion.tw(220, wait), targetScale = 0.2f, transformOrigin = TransformOrigin(1f, 0f)) + @@ -193,7 +177,6 @@ private fun DownloadList(s: FrontEndState, a: FrontEndActions) { } } -/** An install that stopped before it finished, from an earlier run: Resume picks it up, Clear deletes what it had. */ @Composable private fun InterruptedRow(item: com.droiddeck.launcher.stores.CatalogItem) { val ctx = LocalContext.current @@ -207,13 +190,8 @@ private fun InterruptedRow(item: com.droiddeck.launcher.stores.CatalogItem) { } } -/** How long a row that installed holds for its bar to gather into Play before it moves under the line. */ private const val GATHER_HOLD_MS = 900 -/** - * A row in the download card's look, shorter: the game's art behind it under the same dark wash, - * the copy in white. Finished and interrupted downloads read as the same thing as one under way. - */ @Composable private fun ArtStrip(cover: String?, name: String, content: @Composable androidx.compose.foundation.layout.RowScope.() -> Unit) { val pal = LocalPalette.current @@ -228,7 +206,6 @@ private fun ArtStrip(cover: String?, name: String, content: @Composable androidx } } -/** A finished download: its name and store, and Play when it installed - blooming out of a dot when it just did. */ @Composable private fun FinishedRow(d: DownloadEntry, s: FrontEndState, a: FrontEndActions, bloom: Boolean = false) { val ctx = LocalContext.current @@ -243,7 +220,6 @@ private fun FinishedRow(d: DownloadEntry, s: FrontEndState, a: FrontEndActions, } } -/** Grows out of a dot (sp 0.5 / 300) after [waitMs], when [on]; otherwise just there. */ private fun Modifier.bloomIn(on: Boolean, waitMs: Int): Modifier = composed { val k = remember { Animatable(if (on && Motion.scale > 0f) 0f else 1f) } LaunchedEffect(Unit) { @@ -255,10 +231,6 @@ private fun Modifier.bloomIn(on: Boolean, waitMs: Int): Modifier = composed { graphicsLayer { scaleX = k.value; scaleY = k.value; alpha = k.value.coerceIn(0f, 1f) } } -/** - * A row's action as a round icon button - the label is its description - sized like the old - * compact buttons and focusable by pad. [primary] fills it with the accent, [danger] in red. - */ @Composable internal fun RoundAction( icon: androidx.compose.ui.graphics.vector.ImageVector, description: String, primary: Boolean = false, danger: Boolean = false, @@ -280,10 +252,6 @@ internal fun RoundAction( ) { androidx.compose.material3.Icon(icon, description, tint = tint, modifier = Modifier.size(size * 0.5f)) } } -/** - * Cancel as End session's confirm: the X drops a pull and Keep and Delete step out to its left, - * in red. Keep's rim runs out the four seconds after which it folds back by itself. - */ @Composable private fun CancelAction(onDelete: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -299,7 +267,6 @@ private fun CancelAction(onDelete: () -> Unit) { ) { val left = remember { Animatable(1f) } LaunchedEffect(Unit) { - // Wall-clock seconds, as before; the rim follows them when animations run. if (Motion.scale > 0f) launch { left.animateTo(0f, androidx.compose.animation.core.tween((CANCEL_HOLD_MS / Motion.scale).toInt(), easing = LinearEasing)) } delay(CANCEL_HOLD_MS.toLong()) StoresMotion.fold() @@ -315,7 +282,6 @@ private fun CancelAction(onDelete: () -> Unit) { private const val CANCEL_HOLD_MS = 4000 -/** A capsule's outline drawn for [left] (1 to 0) of its length, clockwise from the top centre: time running out. */ private fun Modifier.timeRim(left: () -> Float, color: Color): Modifier = drawWithContent { drawContent() val v = left() @@ -343,8 +309,6 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) val narrow = LocalNarrowPane.current val ink = Color.White val dim = Color.White.copy(alpha = 0.72f) - // The game's wide art is the row: cropped to it, a dark wash from the left so the copy reads - // in white there, the art clear on the right. Box(Modifier.fillMaxWidth().heightIn(min = 88.dp).clip(Shape12).background(Color(0xFF101318)).border(1.dp, pal.line, Shape12)) { if (!d.cover.isNullOrEmpty()) AsyncImage(model = d.cover, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize()) else Spacer(Modifier.matchParentSize().background(artBrush(hueOf(d.name)))) @@ -362,7 +326,6 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) val meta = when (d.state) { DownloadState.INSTALLED -> stringResource(R.string.stores_installed_chip) DownloadState.CANCELLED -> stringResource(R.string.stores_dl_cancelled) - // The reason is in the engine log; the row only says what is kept for Resume. DownloadState.FAILED -> if (d.bytesDone > 0) stringResource(R.string.stores_dl_failed_kept, formatBytes(d.bytesDone)) else stringResource(R.string.stores_dl_failed) DownloadState.PAUSED -> stringResource(R.string.stores_dl_paused) DownloadState.QUEUED -> if (d.queuePosition > 0) stringResource(R.string.stores_dl_queued_at, d.queuePosition) else stringResource(R.string.stores_dl_queued) @@ -382,7 +345,6 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) } } } - // The row's actions on the right, over the art: icons, their labels as descriptions. Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { when (d.state) { DownloadState.RUNNING, DownloadState.QUEUED -> { @@ -394,7 +356,6 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) CancelAction { DownloadQueue.cancel(ctx, d.key) } } DownloadState.FAILED -> RoundAction(Icons.Filled.Refresh, stringResource(R.string.stores_dl_resume), primary = true) { DownloadQueue.retry(ctx, d.key) } - // Just installed (this row is on its way under the line): Play blooms where the bar gathered. DownloadState.INSTALLED -> Box(Modifier.bloomIn(true, 420)) { RoundAction(Icons.Filled.PlayArrow, stringResource(R.string.stores_play), primary = true) { launchStoreGame(ctx, d.store, d.id, s, a) } } @@ -405,13 +366,6 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) } } -/** - * One segment per stage - Manifest, Download, Verify, Install - equal widths: a passed or skipped - * stage full, the active one filling with its own progress (a sliver while it has nothing to count), - * the rest empty. Fills ride a spring; a stage that completes swells and settles (the bloop); a - * paused one breathes; and once the download has installed the four gather into one green line - * that draws up into a dot, where Play blooms. - */ @Composable private fun StageBar(d: DownloadEntry) { val pal = LocalPalette.current @@ -422,8 +376,6 @@ private fun StageBar(d: DownloadEntry) { val fills = stages.mapIndexed { i, st -> when { actives[i] -> d.stageFraction.let { if (it < 0f) 0.04f else it } - // A stage a store skips or folds into another (Epic checks chunks while it fetches, - // Amazon writes while it downloads) reads complete once a later one has started. d.passed(st) || st.ordinal < d.stage.ordinal || d.stage == DownloadStage.DONE -> 1f else -> 0f } @@ -452,7 +404,6 @@ private fun StageBar(d: DownloadEntry) { val cy = size.height / 2f val g = gather.value if (g < 0.5f) { - // The segments: the gaps close and every fill turns done-green as the gather starts. val join = (g * 2f).coerceIn(0f, 1f) val gap = 3.dp.toPx() * (1f - join) val segW = (size.width - gap * 3f) / 4f @@ -472,7 +423,6 @@ private fun StageBar(d: DownloadEntry) { ) } } else { - // One line drawing up into a dot at its right end. val p = ((g - 0.5f) * 2f).coerceIn(0f, 1f) val dot = 10.dp.toPx() val left = androidx.compose.ui.util.lerp(0f, size.width - dot, p) @@ -482,7 +432,6 @@ private fun StageBar(d: DownloadEntry) { } } -/** 1, or while [on] (and animations run), an alpha going 1 → 0.5 → 1 over the rail's live-dot period. */ @Composable private fun breathing(on: Boolean): Float { if (!on || Motion.scale == 0f) return 1f @@ -497,10 +446,6 @@ private fun eta(seconds: Long): String = when { else -> "~${seconds / 3600} h ${(seconds % 3600) / 60} m" } -/** - * The queue's two knobs on one line - the speed tier on the left, downloads at a time on the - * right; it sits in the cog's popup. A narrow page puts them on two tight lines. - */ @Composable internal fun DownloadControls(s: FrontEndState, a: FrontEndActions) { val ctx = LocalContext.current @@ -524,12 +469,6 @@ internal fun DownloadControls(s: FrontEndState, a: FrontEndActions) { } } -/** - * The chip row's cog: the section's own settings stepped out of it - which tab a store opens on, - * mature titles, the speed tier and how many at once, and the download engine. Their only home; - * Setup no longer repeats them. Every install is added to Steam; that is not a choice. [s] is - * read live, so a change shows at once in what is stepped out. - */ internal fun storesSettingsStep(at: Rect, s: androidx.compose.runtime.State<FrontEndState>, a: FrontEndActions, accent: androidx.compose.ui.graphics.Color): StepAsk = StepAsk( anchor = at, side = StepSide.Left, accent = accent, items = 2, maxWidth = 440.dp, @@ -541,7 +480,6 @@ internal fun storesSettingsStep(at: Rect, s: androidx.compose.runtime.State<Fron verticalArrangement = Arrangement.spacedBy(6.dp), ) { val st = s.value - // No OK: B or a tap outside folds it, and a pad starts on the first choice. SettingsRow(stringResource(R.string.setup_stores_open_on), null, stacked = true) { Box(Modifier.focusRequester(first).focusGroup()) { SegmentedTabs( listOf(SessionPrefs.STORES_OPEN_LIBRARY to stringResource(R.string.stores_tab_library), SessionPrefs.STORES_OPEN_STORE to stringResource(R.string.stores_tab_store)), @@ -551,7 +489,6 @@ internal fun storesSettingsStep(at: Rect, s: androidx.compose.runtime.State<Fron SettingsRow(stringResource(R.string.stores_show_mature), null) { ToggleSwitch(st.storesShowMature, label = stringResource(R.string.stores_show_mature)) { a.onStoresShowMature(it) } } - // The queue's two knobs as one compact row, the same control the Downloads page has. Column(Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 11.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { DownloadControls(st, a) } val context = androidx.compose.ui.platform.LocalContext.current var gogAchievements by androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(com.droiddeck.launcher.stores.gog.GogComet.enabled(context)) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt index e7d645a03..fb7f2d4ee 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresInstallDialog.kt @@ -40,11 +40,6 @@ import com.droiddeck.launcher.stores.StoreInstallRoot import com.droiddeck.launcher.stores.StoresState import com.droiddeck.launcher.stores.formatBytes -/** - * "Install <game>:" over one row per place - internal storage, the card - each the action - * itself: icon, name, free space. Asked only when a card is there. The row picked last time - * starts focused, so a pad confirms the usual place with one press; nothing is chosen for the user. - */ @Composable internal fun InstallWhereDialog(item: CatalogItem, onDismiss: () -> Unit) { val ctx = LocalContext.current diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresMotion.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresMotion.kt index de7bcce6c..3d5ba671c 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresMotion.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresMotion.kt @@ -47,13 +47,8 @@ import kotlin.math.atan2 import kotlin.math.max import kotlin.math.sin -// The Stores section's moves, in the launcher's own vocabulary: an install bloops into a dot that -// flies to the Downloads chip, sign-in floods in the store's colour and comes home to the chip's -// dot, and the confirms step out of the control that asked (StepOut). Everything here is in root -// coordinates; the Stores page draws it in its own space. internal object StoresMotion { - /** The Install (or Resume install) button last pressed, its label and when: a download that starts soon after flies from it. */ var installFrom: Rect? = null private set var installLabel: String = "" @@ -66,28 +61,21 @@ internal object StoresMotion { installAt = SystemClock.uptimeMillis() } - /** The button just pressed, if it was pressed in the last few seconds; not consumed. */ fun recentInstall(): Rect? = installFrom.takeIf { SystemClock.uptimeMillis() - installAt < 5_000 } - /** The button a download that just started came from, once; stale after 20 s (a slow manifest). */ fun takeInstall(): Rect? = installFrom.takeIf { SystemClock.uptimeMillis() - installAt < 20_000 }.also { installAt = 0L } - /** Where the Downloads chip sits, for a download's dot to land in. */ var downloadsChip by mutableStateOf<Rect?>(null) - /** Bumped each time a dot lands: the chip's count and the rail's badge pop on it. */ var landed by mutableIntStateOf(0) - /** Dots in flight to the Downloads chip. */ val flights = mutableStateListOf<Flight>() fun fly(from: Rect) { if (Motion.scale == 0f || downloadsChip == null) { landed++; return } - // The count sits at the chip's right end. flights += Flight(from, to = { downloadsChip?.let { Offset(it.right - 22.dp.toPx(), it.center.y) } }, onLand = { landed++ }) } - /** The confirm or list stepping out of a control on the Stores page now, and whether it is open. */ var step by mutableStateOf<StepAsk?>(null) private set var stepOpen by mutableStateOf(false) @@ -99,14 +87,12 @@ internal object StoresMotion { stepOpen = true } - /** Drops whatever is stepped out or flooding, at once: the page it belonged to has gone. */ fun drop() { step?.let { step = null; it.onDismiss() } stepOpen = false signIn = null } - /** Folds the current one back into its control. */ fun fold() { stepOpen = false } internal fun closed(a: StepAsk) { @@ -114,24 +100,18 @@ internal object StoresMotion { a.onDismiss() } - /** A store's sign-in flooding the page, or draining back out of it. */ var signIn by mutableStateOf<SignInFlood?>(null) - /** Per store, bumped to send its chip's dot one pulse ring (signed in, picked). */ val pulses = androidx.compose.runtime.mutableStateMapOf<Store, Int>() fun pulse(store: Store) { pulses[store] = (pulses[store] ?: 0) + 1 } - /** Where each store's dot sits on its chip, for a sign-in to come home to. */ val dots = HashMap<Store, Rect>() - /** Where the open game page's hero art sits (root px): what a card's flood lands on, and leaves from. */ var heroAt by mutableStateOf<Rect?>(null) - /** The open game page came out of a card: its hero holds still, for the card's flood to land on. */ var heroLands by mutableStateOf(false) - /** The card a game page is about to open from. */ private var card: CardMark? = null private var cardAt = 0L @@ -140,17 +120,11 @@ internal object StoresMotion { cardAt = SystemClock.uptimeMillis() } - /** The card game [key] was just opened from, once; a page opened another way shows without a flood. */ fun takeCard(key: String): CardMark? = card?.takeIf { it.key == key && SystemClock.uptimeMillis() - cardAt < 1_000 }.also { card = null } } -/** A game card at [bounds] (root px) with corner [corner] (px) and its art, for the page it opens to flood out of. */ internal class CardMark(val key: String, val bounds: Rect, val corner: Float, val art: String?) -/** - * A confirm or short list for [StepOut] on the Stores page, asked by a control that sits at [anchor] - * (root px). [content] gets a close that folds it; [onDismiss] runs once it has folded, however. - */ internal class StepAsk( val anchor: Rect, val side: StepSide, @@ -164,22 +138,15 @@ internal class StepAsk( ) internal class SignInFlood(val store: Store, val from: Rect, val start: Color, val color: Color) { - /** Covered and handed to the login page; waiting for the page to come back. */ var away by mutableStateOf(false) - /** Back from the login page: draining into [to]. */ var drainTo by mutableStateOf<Rect?>(null) } -/** The dots flying to the Downloads chip, drawn over the page whose root offset is [origin]. */ @Composable internal fun FlightLayer(origin: Offset) { for (f in StoresMotion.flights.toList()) key(f.id) { FlightDot(f, origin) { StoresMotion.flights.remove(f) } } } -/** - * Pops (1.3 to 1, with one ring) each time [trigger] changes: by default each time a download's - * dot lands, for the count on the chip and the rail; any other landing passes its own count. - */ internal fun Modifier.landingPop(trigger: Int = StoresMotion.landed): Modifier = composed { val pal = LocalPalette.current val scale = remember { Animatable(1f) } @@ -201,11 +168,6 @@ internal fun Modifier.landingPop(trigger: Int = StoresMotion.landed): Modifier = }.graphicsLayer { scaleX = scale.value; scaleY = scale.value } } -/** - * A pressed button in [start] stretching out over this element in [color] (LaunchFlood's move: - * each edge on its own loose spring, the furthest first, blobby in flight). [from] is in this - * element's px. Calls [onCovered] once, when nothing but [color] shows; then holds. - */ @Composable internal fun ColorFlood(from: Rect, start: Color, color: Color, onCovered: () -> Unit) { val edges = remember { List(4) { Animatable(0f) } } @@ -220,7 +182,6 @@ internal fun ColorFlood(from: Rect, start: Color, color: Color, onCovered: () -> val lead = travel[i] / far launch { delay(Motion.ms((120 * (1f - lead)).toInt()).toLong()) - // Overshoots past the edge, off the element, so the wobble is felt in the pull. edge.animateTo(1f, if (Motion.scale == 0f) Motion.sp() else spring(dampingRatio = 0.48f, stiffness = lerp(110f, 190f, lead))) { if (value >= 1f) reached[i] = true } @@ -237,10 +198,6 @@ internal fun ColorFlood(from: Rect, start: Color, color: Color, onCovered: () -> } } -/** - * [color] over the whole element drawing down into [to] (px, here), critically damped, the near - * edges first (PageFlood's way back), then fading out on it; [onDone] after. - */ @Composable internal fun ColorDrain(color: Color, to: Rect, toCorner: Float, onDone: () -> Unit) { val edges = remember { List(4) { Animatable(1f) } } @@ -270,7 +227,6 @@ internal fun ColorDrain(color: Color, to: Rect, toCorner: Float, onDone: () -> U private fun Modifier.onSizeChangedCompat(onSize: (Size) -> Unit) = onSizeChanged { onSize(Size(it.width.toFloat(), it.height.toFloat())) } -/** A rect from [r] with its edges [v] of the way to this scope's (left, top, right, bottom): blobby in flight. */ private fun DrawScope.drawFloodShape(r: Rect, restCorner: Float, v: List<Float>, color: Color) { val left = lerp(r.left, 0f, v[0]) val top = lerp(r.top, 0f, v[1]) @@ -284,7 +240,6 @@ private fun DrawScope.drawFloodShape(r: Rect, restCorner: Float, v: List<Float>, drawRoundRect(color, Offset(left, top), Size(w, h), CornerRadius(corner)) } -/** Sends one pulse ring out of a dot each time [trigger] changes after the first composition. */ internal fun Modifier.pulseRing(trigger: Int, color: Color): Modifier = composed { val ring = remember { Animatable(1f) } val seen = remember { intArrayOf(trigger) } @@ -303,11 +258,6 @@ internal fun Modifier.pulseRing(trigger: Int, color: Color): Modifier = composed } } -/** - * [items] in a column where a row that comes enters with [enter] and a row that goes stays on to - * leave with [exit] (a Column has no item animations of its own). [content] gets the row and - * whether it came after the list was first shown. - */ @Composable internal fun <T> AnimatedRows( items: List<T>, @@ -322,7 +272,6 @@ internal fun <T> AnimatedRows( val later = remember { mutableMapOf<Any, Boolean>() } val mounted = remember { booleanArrayOf(false) } val now = items.associateBy(key) - // Keep a leaving row where it was among the ones that stay. val merged = ArrayList<Any>() var j = 0 for (k in now.keys) { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt index 1d722dd3d..a070e0158 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresPage.kt @@ -133,24 +133,15 @@ import com.droiddeck.launcher.stores.download.DownloadStage import com.droiddeck.launcher.stores.download.DownloadState import com.droiddeck.launcher.stores.formatBytes -// The Stores section: GOG, Epic Games and Amazon Games libraries and storefronts, and one -// download queue for the three, in a single full-width pane. -/** The chip row's fourth entry, beside the three stores. */ private const val DOWNLOADS = "downloads" private val TABS = listOf("store", "library", "installed") -/** - * The modes a store has, picked from its chip (A steps them out of it) or cycled with LB / RB: - * Store, Library and Installed. Amazon has no public catalog, so no Store. - */ internal fun tabsFor(store: Store?): List<String> = if (store == Store.AMAZON) TABS.drop(1) else TABS -/** [tab] if [store] has it, else Library (the open-on Store choice on Amazon falls back there). */ internal fun tabFor(store: Store?, tab: String): String = if (tab in tabsFor(store)) tab else "library" -/** What the pane under the chips shows: a store's tab (signed in or not) or the downloads. */ private data class PaneKey(val chip: String, val tab: String, val signedIn: Boolean) @OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class, androidx.compose.animation.ExperimentalAnimationApi::class) @@ -164,26 +155,18 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier var tab by rememberSaveable { mutableStateOf(if (SessionPrefs.storesOpenTab(ctx) == SessionPrefs.STORES_OPEN_STORE) "store" else "library") } var query by rememberSaveable { mutableStateOf("") } var openGame by rememberSaveable { mutableStateOf<String?>(null) } - // The cog's settings read the front end's state live while stepped out. val liveState = androidx.compose.runtime.rememberUpdatedState(s) LaunchedEffect(StoresState.openDownloads) { if (StoresState.openDownloads) { chip = DOWNLOADS; StoresState.openDownloads = false } } val store = Store.byId(chip) - // A tab this store does not have (Store or All on Amazon) reads as Library. val shownTab = tabFor(store, tab) val pane = PaneKey( chip, if (store == null) "" else shownTab, store == null || (StoresState.isSignedIn(store) && StoresState.expired[store] != true), ) - // Incoming and outgoing panes must not share a scroll state while both are on screen. val grids = remember { HashMap<PaneKey, LazyGridState>() } val grid = grids.getOrPut(pane) { LazyGridState() } LaunchedEffect(Unit) { StoresState.refresh(ctx) } - // A store the account is signed into fills itself when its chip is on screen. LaunchedEffect(chip, StoresState.accounts[store]) { if (store != null && StoresState.isSignedIn(store)) StoresState.open(ctx, store) } - // A pad's focus sits on something the page is about to replace - the card that opens a game, - // a tab's contents - and would be lost with it, leaving the next press to land on the rail. - // Each move says where focus goes next: into the game page's main action, back to the card - // it was opened from, or onto the first card of a tab or store just picked. val ff = LocalFrontFocus.current val inputMode = androidx.compose.ui.platform.LocalInputModeManager.current var focusMove by remember { mutableStateOf<Pair<String, Int>?>(null) } @@ -192,15 +175,11 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier fun open(key: String) { lastOpened = key; openGame = key; move("detail") } fun closeGame() { openGame = null; move("card") } fun switchTab(to: String) { tab = to; move("first") } - // Picking a chip opens the tab Setup says (Library or Store); a tab the user switches to - // afterwards stays until another chip is picked. val pickChip: (String) -> Unit = { id -> chip = id; openGame = null; query = "" if (id != DOWNLOADS) tab = tabFor(Store.byId(id), if (s.storesOpenTab == SessionPrefs.STORES_OPEN_STORE) "store" else "library") move("first") } - // A pad moving along the chips shows each store it stops on, without A: a beat after it - // settles, so sweeping past two chips does not open both. Focus stays on the chip. var focusedChip by remember { mutableStateOf<String?>(null) } LaunchedEffect(focusedChip) { val id = focusedChip ?: return@LaunchedEffect @@ -210,20 +189,15 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier if (id != DOWNLOADS) tab = tabFor(Store.byId(id), if (s.storesOpenTab == SessionPrefs.STORES_OPEN_STORE) "store" else "library") } } - // A store's chip steps its modes out of itself (End session's shape, in the store's colour); - // picking one shows that store in it. A store not signed in has none: its chip just opens it. val pal = LocalPalette.current fun openModes(st: Store, at: Rect?) { - // Another store's chip (a tap; a pad's focus has already shown it): that store. if (chip != st.id) { pickChip(st.id); return } - // Not signed in: nothing to pick, so A goes on to its Sign in. if (at == null || !StoresState.isSignedIn(st) || StoresState.expired[st] == true) { move("first"); return } val modes = tabsFor(st) val current = if (chip == st.id) shownTab else null var picked = false StoresMotion.ask(StepAsk( anchor = at, side = StepSide.Right, accent = storePalette(pal, st).signal, items = modes.size, - // Folded: focus goes to the first card of what was picked, or back to the chip. onDismiss = { move(if (picked) "first" else "item:storechip:${st.id}") }, handle = { ChipLabel(st, MaterialTheme.colorScheme.onBackground) }, ) { @@ -255,17 +229,10 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier BackHandler(enabled = openGame != null) { closeGame() } val scroll = rememberScrollState() val detailScroll = rememberScrollState() - // A store or tab just picked starts at the top. Closing a game page leaves the grid where it - // was, so the page draws back into the card it came from and focus lands on it. LaunchedEffect(chip, tab) { scroll.scrollTo(0); grid.scrollToItem(0) } LaunchedEffect(openGame) { detailScroll.scrollTo(0) } LaunchedEffect(focusMove, openGame) { if (inputMode.inputMode != androidx.compose.ui.input.InputMode.Keyboard || ff == null) return@LaunchedEffect - // Looked up each frame, and asked again until focus is really there: what it goes to (the - // game page's action, the card under it) is composed and placed a frame or two after the - // move that asks for it, and a request before then is dropped without a word. - // A mode with nothing in it (Installed, none installed) has no first card: after a few - // frames focus goes to the store's chip rather than falling back to the rail. var frames = 0 fun attached(id: String) = (ff.attached[id] ?: 0) > 0 fun target(): Pair<androidx.compose.ui.focus.FocusRequester, String?>? { @@ -276,15 +243,12 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier kind == "card" -> lastOpened?.let { "card:$it" }?.let { id -> ff.items[id]?.takeIf { attached(id) }?.let { it to id } } ?: ff.firstTile.takeIf { ff.firstTileAttached > 0 }?.let { it to null } kind == "first" -> ff.firstTile.takeIf { ff.firstTileAttached > 0 }?.let { it to null } - // A store not signed in: its Sign in. ?: ff.primary.takeIf { ff.primaryAttached > 0 }?.let { it to FrontFocus.PRIMARY } ?: "storechip:$chip".let { id -> ff.items[id]?.takeIf { ++frames > 12 && attached(id) }?.let { it to id } } kind?.startsWith("item:") == true -> kind.removePrefix("item:").let { id -> ff.items[id]?.takeIf { attached(id) }?.let { it to id } } else -> null } } - // The press that asked for this move must not land on where focus goes: its release - // would press that too (A on a chip went on to Sign in). Wait for A to come up. repeat(60) { if (!HeldKeys.confirm) return@repeat; androidx.compose.runtime.withFrameNanos { } } repeat(45) { androidx.compose.runtime.withFrameNanos { } @@ -294,9 +258,6 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier } } - // The game page is a layer over the grid: it floods out of the card it was opened from (the - // cog's PageFlood, carrying the card's art), and Back draws it into that card again. [detail] - // outlives [openGame] while it draws back. var detail by remember { mutableStateOf<String?>(null) } var leaving by remember { mutableStateOf(false) } var detailFrom by remember { mutableStateOf<Origin?>(null) } @@ -337,9 +298,6 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier onNext = { if (openGame == null && chip != DOWNLOADS) { val t = tabsFor(store); switchTab(t[(t.indexOf(shownTab) + 1) % t.size]) } }, ), ) { - // The chip row stays put and paints nothing - the page's background shows through it. What - // scrolls is clipped at the row's lower edge and, once scrolled, faded out there: the - // content's own alpha, not a colour laid over it. Box(Modifier.fillMaxWidth().padding(bottom = 8.dp)) { StoreTheme(Store.byId(focusedChip ?: chip)) { Rise(0) { StoreChips(chip, focusedChip, s.storeDownloadsActive, onStore = { st, at -> openModes(st, at) }, onDownloads = { if (chip != DOWNLOADS) pickChip(DOWNLOADS) else move("first") }, @@ -362,16 +320,11 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier ) }, ) { - // Under a game page the grid stays composed - its scroll and the card to come back to - // stay put - but sinks away, and a pad can neither reach it nor be sent to it. Once the - // page starts drawing back the grid takes focus again, so it lands on the card at once. val gridFocus = if (covering) null else ff CompositionLocalProvider(LocalFrontFocus provides gridFocus) { Column( modifier = Modifier.fillMaxSize() .graphicsLayer { scaleX = gridScale; scaleY = gridScale; alpha = gridAlpha } - // Always the same group, only its rule changes: a focus group added around - // the card that has focus leaves the focus tree refusing every request after. .focusProperties { enter = { if (coveringNow.value) androidx.compose.ui.focus.FocusRequester.Cancel else androidx.compose.ui.focus.FocusRequester.Default } } .focusGroup(), ) { @@ -381,29 +334,20 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier transitionSpec = { val sameStore = initialState.chip == targetState.chip && initialState.signedIn == targetState.signedIn (if (sameStore && initialState.tab != targetState.tab) { - // LB / RB: the tab's contents leave the way the bumper points and the - // next tab's come in from the other side, the column nearest leading. val dir = if (TABS.indexOf(targetState.tab) > TABS.indexOf(initialState.tab)) 1 else -1 (slideInHorizontally(Motion.sp(0.7f, Spring.StiffnessMediumLow)) { it / 6 * dir } + fadeIn(Motion.tw(220, 60))) .togetherWith(slideOutHorizontally(Motion.tw(170)) { -it / 6 * dir } + fadeOut(Motion.tw(170)) + scaleOut(Motion.tw(170), targetScale = 0.97f)) } else { - // Another store: the shelf sinks away, the next rises in (the pane's own move). (fadeIn(Motion.tw(300, 80)) + slideInVertically(Motion.tw(420, 80)) { it / 24 }) .togetherWith(fadeOut(Motion.tw(170)) + scaleOut(Motion.tw(170), targetScale = 0.97f)) }).using(SizeTransform(clip = false)) }, label = "storesPane", ) { k -> - // What is leaving is not where a pad's next move should land. val outgoing = transition.targetState == androidx.compose.animation.EnterExitState.PostExit CompositionLocalProvider(LocalFrontFocus provides if (outgoing) null else LocalFrontFocus.current) { - // AnimatedContent stacks its child in a Box: the pane's rows need their own column. - // A pad moving while the pane changes must land on what is coming, not on - // what is leaving: focus there vanishes with it. Same group always, only - // its rule changes (adding one around focus breaks the focus tree). val leavingNow = androidx.compose.runtime.rememberUpdatedState(outgoing) val st = Store.byId(k.chip) - // Keep the outgoing pane's palette stable while the next store comes in. StoreTheme(st) { Column( Modifier.fillMaxSize() @@ -413,7 +357,6 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier when { k.chip == DOWNLOADS -> Column(Modifier.fillMaxWidth().verticalScroll(scroll).padding(top = 4.dp, bottom = 16.dp)) { StoresDownloadsPane(s, a) } st == null -> {} - // Signed out, or a sign-in that ran out (a launch could not get its code): sign in again. !k.signedIn -> Box(Modifier.fillMaxWidth().verticalScroll(scroll).padding(top = 4.dp, bottom = 16.dp)) { SignInCard(st, viewportH - 20.dp) } else -> Storefront(st, k.tab, query, s, a, grids.getOrPut(k) { LazyGridState() }, onQuery = { query = it }, onOpen = { open(it) }) } @@ -427,9 +370,6 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier val detailStore = Store.byId(key.substringBefore(':')) ?: store StoreTheme(detailStore) { val body: @Composable () -> Unit = { - // One focus group, so the pad walks the page's own controls - back, the hero's - // actions - and reaches the rail only with Left from them. It takes every touch, - // so nothing reaches the grid under it. Column( Modifier.fillMaxSize().pointerInput(Unit) { detectTapGestures { } } .verticalScroll(detailScroll).padding(top = 4.dp, bottom = 16.dp), @@ -440,9 +380,6 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier val from = detailFrom if (from == null) body() else { - // Sized up front: the flood draws the art only once it knows its size, and a painter - // left to take its size from drawing would never load. The card's own bitmap stands - // in from the memory cache until the full one is decoded. val art = detailArt?.let { url -> coil.compose.rememberAsyncImagePainter( remember(url) { @@ -451,16 +388,12 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier }, ) } - // Onto the page's own hero art, not the whole pane: grown to the pane, the art - // then cut down to the hero's banner (and back out to the pane to close). PageFlood(from, leaving, art, onto = { StoresMotion.heroAt }, ontoCorner = 16.dp) { body() } } } } } } - // Over the whole page: an install's dot on its way to Downloads, a confirm stepping out of the - // control that asked for it, and a sign-in's flood. FlightLayer(hostAt) StoresMotion.step?.let { ask -> key(ask) { @@ -473,9 +406,7 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier } SignInFloodLayer(hostAt) } - // Leaving the section: nothing stays stepped out of a control that is no longer on screen. androidx.compose.runtime.DisposableEffect(Unit) { onDispose { StoresMotion.drop() } } - // An install that started from a button here flies from it to the Downloads chip. val activeKeys = StoresState.downloads.filter { it.isActive }.map { it.key }.toSet() val seenActive = remember { mutableSetOf<String>().also { it.addAll(activeKeys) } } LaunchedEffect(activeKeys) { @@ -486,10 +417,6 @@ internal fun StoresPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier StoresState.pendingInstall?.let { item -> InstallWhere(item) } } -/** - * "Install <game>:" with a row per place, grown out of the Install button that asked (End - * session's step-out, in blue); the plain dialog when that button is not on screen. - */ @Composable private fun InstallWhere(item: CatalogItem) { val ctx = LocalContext.current @@ -519,7 +446,6 @@ private fun InstallWhere(item: CatalogItem) { modifier = Modifier.fillMaxWidth().stepItem(1 + i).then(if (i == defaultIndex) Modifier.focusRequester(first) else Modifier), ) { SessionPrefs.setStoresInstallTarget(ctx, t.root.absolutePath) - // The fold and the dot leave together: the download it starts flies from Install. StoresMotion.markInstall(from, label) StoresState.install(ctx, item, t.root) StoresMotion.fold() @@ -530,7 +456,6 @@ private fun InstallWhere(item: CatalogItem) { } } -/** A sign-in's flood: out of the button in the store's colour, and home again once the login page closes. */ @Composable private fun SignInFloodLayer(hostAt: Offset) { val f = StoresMotion.signIn ?: return @@ -539,7 +464,6 @@ private fun SignInFloodLayer(hostAt: Offset) { val density = androidx.compose.ui.platform.LocalDensity.current key(f) { var paused by remember { mutableStateOf(false) } - // Signed in: into the store's dot on its chip. Not (closed, or it failed): back into the button. fun back() { if (f.drainTo == null) f.drainTo = if (StoresState.isSignedIn(f.store)) StoresMotion.dots[f.store] ?: f.from else f.from } androidx.compose.runtime.DisposableEffect(lifecycle) { val obs = androidx.lifecycle.LifecycleEventObserver { _, e -> @@ -549,7 +473,6 @@ private fun SignInFloodLayer(hostAt: Offset) { lifecycle.addObserver(obs) onDispose { lifecycle.removeObserver(obs) } } - // The store had no login page to open: nothing will come back, so drain now. LaunchedEffect(f.away) { if (f.away) { delay(1_500); if (!paused) back() } } val to = f.drainTo if (to == null) ColorFlood(f.from.translate(-hostAt), f.start, f.color) { @@ -568,27 +491,17 @@ private fun SignInFloodLayer(hostAt: Offset) { } } -// ---- the chip row ------------------------------------------------------------------------------- -/** - * Four equal chips - the stores with a signed-in dot, Downloads with its count - and the cog. The - * selection is one shape that belongs to the row, not each chip's background: it glides from chip - * to chip like the focus ring, its leading edge first, tinted in the store's colour, and a hop - * past a chip goes as a drop. - */ @Composable private fun StoreChips( selected: String, focused: String?, active: Int, onStore: (Store, Rect?) -> Unit, onDownloads: () -> Unit, onSettings: (Rect?) -> Unit, - /** The chip a pad's focus is on (null when it leaves the row): the page shows that store. */ onFocusChip: (String?) -> Unit, ) { val pal = LocalPalette.current val keys = Store.entries.map { it.id } + DOWNLOADS val bounds = remember { androidx.compose.runtime.mutableStateMapOf<String, Rect>() } val rootBounds = remember { HashMap<String, Rect>() } - // The fill travels with focus; only the expensive pane change waits for the debounce. val highlighted = focused ?: selected - // With a pad the row has no fill of its own: the focus ring's infill is the only one. val inputMode = androidx.compose.ui.platform.LocalInputModeManager.current.inputMode val fillShown = inputMode != androidx.compose.ui.input.InputMode.Keyboard val tint = androidx.compose.animation.animateColorAsState( @@ -603,7 +516,6 @@ private fun StoreChips( modifier = Modifier.weight(1f).onGloballyPositioned { bounds[store.id] = it.boundsInParent(); rootBounds[store.id] = it.boundsInRoot() }, lead = { val c = sourceColours(store.id) - // A store's dot pulses when a sign-in lands (and, with the chip's lead, when picked). Box( Modifier.size(12.dp) .onGloballyPositioned { StoresMotion.dots[store] = it.boundsInRoot() } @@ -629,7 +541,6 @@ private fun StoreChips( IconChip(Icons.Filled.Settings, stringResource(R.string.stores_settings)) { onSettings(cogAt) } } } - // Over the chips: the fill is translucent, and the other chips' rest fill would hide it in flight. ChipGlide(keys.indexOf(highlighted), bounds[highlighted], tint, visible = fillShown) } } @@ -649,15 +560,12 @@ private fun StoreChip( val scale by animateFloatAsState(if (pressed) 0.95f else 1f, Motion.sp(0.5f, Spring.StiffnessMedium), label = "chipScale") val shape = RoundedCornerShape(99.dp) val ink = if (on) colors.onBackground else if (hot) colors.onBackground else colors.onSurfaceVariant - // The chip whose page shows has a bar of its colour along its foot. Picking it pulses its - // lead (a store's dot, Downloads' icon). var picked by remember { mutableStateOf(0) } val wasOn = remember { booleanArrayOf(on) } LaunchedEffect(on) { if (on && !wasOn[0]) picked++ wasOn[0] = on } - // The row's glide draws the look of the chip [under] it, so that one draws no rest state of its own. Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), modifier = modifier.paneItem("storechip:$key") @@ -682,7 +590,6 @@ private fun StoreChip( } } -/** The chip row's cog: icon only, the chips' height; opens the section's settings. */ @Composable private fun IconChip(icon: androidx.compose.ui.graphics.vector.ImageVector, description: String, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -703,18 +610,11 @@ internal fun CountPill(count: Int) { val pal = LocalPalette.current Text( count.toString(), fontSize = 11.sp, fontWeight = FontWeight.Bold, color = pal.onSignal, maxLines = 1, - // Pops as an install's dot lands in it. modifier = Modifier.landingPop().clip(RoundedCornerShape(8.dp)).background(pal.signal).padding(horizontal = 6.dp, vertical = 1.dp), ) } -// ---- sign-in ----------------------------------------------------------------------------------- -/** - * No account for this store yet: the Steam tab's shape in the store's colour. A tilted wall of the - * store's own games drifts behind (its public storefront; blank capsules in its colour when it has - * none), fading out toward the words, and Sign in sits large at the foot. [height] is the pane's. - */ @Composable private fun SignInCard(store: Store, height: Dp) { val ctx = LocalContext.current @@ -722,9 +622,7 @@ private fun SignInCard(store: Store, height: Dp) { val pal = LocalPalette.current val narrow = LocalNarrowPane.current val c = sourceColours(store.id) - // The storefront needs no account; it is what the wall is made of. LaunchedEffect(store) { StoresState.preview(ctx, store) } - // A handful of games would repeat down every column: under eight, the blank wall in its colour. val art = remember(StoresState.shelves[store]) { StoresState.shelves[store]?.all.orEmpty().filter { !it.mature }.mapNotNull { it.tallImageUrl ?: it.imageUrl }.distinct().take(48) .takeIf { it.size >= 8 }.orEmpty() @@ -733,11 +631,9 @@ private fun SignInCard(store: Store, height: Dp) { val blank = WallCapsule(Brush.linearGradient(listOf(c.fill, colors.background)), edge = c.dot.copy(alpha = 0.22f)) val ground = SolidColor(c.fill) Box(Modifier.fillMaxWidth().heightIn(min = height.coerceAtLeast(0.dp)).clip(shape).background(colors.background).border(1.dp, pal.line, shape)) { - // The storefront arrives a moment after the page: its wall fades in over the blank one. androidx.compose.animation.Crossfade(art, animationSpec = Motion.tw(600), label = "signInWall", modifier = Modifier.matchParentSize()) { tiles -> TiltedWall(tiles.size, if (tiles.isEmpty()) 75_000 else 50_000) { i -> if (tiles.isEmpty()) blank else WallCapsule(ground, tiles[i]) } } - // The wall gives way to the words: dark at the left and the foot, and the store's glow under them. Box( Modifier.matchParentSize().background( Brush.horizontalGradient( @@ -768,10 +664,6 @@ private fun SignInCard(store: Store, height: Dp) { } } -/** - * Sign in: the button floods the page in the store's colour (LaunchFlood's move), the login page - * opens on that colour, and the flood comes home into the store's dot once it closes. - */ @Composable private fun SignInButton(store: Store, large: Boolean = false) { val ctx = LocalContext.current @@ -786,7 +678,6 @@ private fun SignInButton(store: Store, large: Boolean = false) { } } -/** A square in the store's colour with its name on it: the stores ship no logo this app may bundle. */ @Composable internal fun StoreLogo(store: Store, size: Dp) { val c = sourceColours(store.id) @@ -795,7 +686,6 @@ internal fun StoreLogo(store: Store, size: Dp) { } } -// ---- storefront -------------------------------------------------------------------------------- @Composable private fun Storefront( @@ -807,19 +697,14 @@ private fun Storefront( val colors = MaterialTheme.colorScheme val library = StoresState.library[store] ?: emptyList() val shelves = StoresState.shelves[store] - // The Installed tab, its count and every card's installed mark come from the installs on disk. val installedItems = remember(StoresState.installed, library) { com.droiddeck.launcher.stores.installedCards(store, StoresState.installed, library) } val installedKeys = remember(installedItems) { installedItems.map { it.id }.toSet() + StoresState.installed.filter { it.sidecar.store == store }.map { it.sidecar.id } } val q = query.trim().lowercase() fun matches(i: CatalogItem) = q.isEmpty() || i.title.lowercase().contains(q) - // The storefront leaves out what the store rates or tags as adult unless the user shows it; owned games always show. fun shown(i: CatalogItem) = s.storesShowMature || !i.mature || i.owned fun shelf(l: List<CatalogItem>?) = l?.filter(::shown) - // The Store tab's search: the catalog the shelves brought and the library, each title once. val everything = remember(library, shelves) { (library + (shelves?.all ?: emptyList())).distinctBy { it.id } } - // Installed is a mode of its own now, picked from the store's chip like Store and Library. val installedOnly = tab == "installed" - // Until the cache is read (a moment, off the main thread) there is nothing to count or to say is missing. val loaded = StoresState.library.containsKey(store) val showGrid = tab != "store" || q.isNotEmpty() val visible = remember(library, installedItems, everything, tab, q, s.storesShowMature) { @@ -834,8 +719,6 @@ private fun Storefront( tab == "library" && library.isEmpty() && StoresState.status[store] != null -> R.string.stores_library_loading else -> R.string.stores_nothing_matches }) - // A lazy item can be recomposed or prefetched out of order; the first grid card is identified - // by its key, rather than whichever happens to compose first. var firstPlaced = false var columns by remember { mutableIntStateOf(1) } val density = androidx.compose.ui.platform.LocalDensity.current @@ -856,12 +739,11 @@ private fun Storefront( val next = storeGridNeighbor(index, visible.size, columns, direction) when { direction == 0 || index < 0 -> false - next == null -> direction > 0 // The last row stays in its column; Up can reach the header. + next == null -> direction > 0 else -> { if (key.action == KeyEvent.ACTION_DOWN) { val target = "card:${visible[next].key}" val targetIndex = headerItems + next - // Prefetched cards can accept focus without being on screen. val visibleTarget = grid.layoutInfo.visibleItemsInfo.any { it.index == targetIndex } if (!visibleTarget || (focus.attached[target] ?: 0) == 0 || runCatching { focus.items.getValue(target).requestFocus() }.isFailure) scope.launch { @@ -900,7 +782,6 @@ private fun Storefront( else -> stringResource(R.string.stores_search_library) }, ) { - // A purchase made elsewhere shows on a refresh; otherwise the library refreshes itself every six hours. SmallTextButton(stringResource(R.string.stores_refresh)) { StoresState.open(ctx, store, force = true) } val name = StoresState.accounts[store] ?: "" SignOutButton(store, name) @@ -932,15 +813,10 @@ private fun Storefront( } } -/** - * What the store is showing (Store, Library, Installed), a magnifier beside it that opens into the - * search field across the row, and [trailing] (the account) at the right, over a rule. - */ @Composable internal fun ModeHeader(title: String, query: String, onQuery: (String) -> Unit, placeholder: String, trailing: @Composable () -> Unit) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current - // Open while it holds a query; an empty field closes again once focus leaves it. var searching by remember { mutableStateOf(query.isNotEmpty()) } val open by animateFloatAsState(if (searching) 1f else 0f, Motion.sp(0.75f, 420f), label = "searchOpen") Column(Modifier.fillMaxWidth()) { @@ -951,7 +827,6 @@ internal fun ModeHeader(title: String, query: String, onQuery: (String) -> Unit, ) BoxWithConstraints(Modifier.weight(1f), contentAlignment = Alignment.CenterStart) { if (searching || open > 0.01f) { - // The field grows out of the magnifier, the row's width at most. val w = androidx.compose.ui.unit.lerp(44.dp, maxWidth.coerceAtMost(520.dp), open) Box(Modifier.width(w).graphicsLayer { alpha = (open * 1.6f).coerceAtMost(1f) }) { AdbTextField( @@ -968,7 +843,6 @@ internal fun ModeHeader(title: String, query: String, onQuery: (String) -> Unit, } } -/** A store chip's face: its dot and short name, for what steps out of it to carry. */ @Composable private fun ChipLabel(store: Store, ink: Color) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { @@ -977,7 +851,6 @@ private fun ChipLabel(store: Store, ink: Color) { } } -/** The page's accent in [store]'s own colour (GOG purple, Epic white, Amazon orange); [base] for none. */ internal fun storePalette(base: Palette, store: Store?): Palette { val (accent, deep) = when (store) { Store.GOG -> Color(0xFFC25BC8) to Color(0xFF9A3FA1) @@ -993,14 +866,12 @@ internal fun storePalette(base: Palette, store: Store?): Palette { ) } -/** The Store section's accent: Flathub's teal, with its dark ink on it. */ internal fun flathubPalette(base: Palette): Palette = Palette( base.id, base.label, base.detail, base.background, base.surface, base.surfaceVariant, base.line, base.line2, base.onBackground, base.onSurfaceVariant, primary = FlathubTeal, primary2 = Color(0xFF2E958D), onPrimary = FlathubInk, signal = FlathubTeal, good = base.good, error = base.error, ) -/** [content] with the focus ring, buttons, step-outs and the chip glide in Flathub's teal. */ @Composable internal fun FlathubTheme(content: @Composable () -> Unit) { val base = LocalPalette.current @@ -1009,12 +880,10 @@ internal fun FlathubTheme(content: @Composable () -> Unit) { CompositionLocalProvider(LocalPalette provides p) { MaterialTheme(colorScheme = scheme, typography = MaterialTheme.typography, content = content) } } -/** [content] with the focus ring, buttons and step-outs in [store]'s colour. */ @Composable internal fun StoreTheme(store: Store?, content: @Composable () -> Unit) { val base = LocalPalette.current val p = remember(base, store) { storePalette(base, store) } - // Downloads uses the base palette, but the provider stays in the same focus tree. val scheme = MaterialTheme.colorScheme.copy(primary = p.primary, onPrimary = p.onPrimary) CompositionLocalProvider(LocalPalette provides p) { MaterialTheme(colorScheme = scheme, typography = MaterialTheme.typography, content = content) } } @@ -1033,10 +902,6 @@ private fun SmallTextButton(text: String, key: String = text, onClick: () -> Uni ) } -/** - * "<account> · sign out", which asks first: are you sure steps out of it (End session's shape, in - * red), Stay signed in first, so a stray A costs nothing. - */ @Composable private fun SignOutButton(store: Store, name: String) { val ctx = LocalContext.current @@ -1066,7 +931,6 @@ private fun SignOutButton(store: Store, name: String) { } } -/** The header's magnifier: a round button the search field opens out of. */ @Composable internal fun SearchIcon(description: String, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme @@ -1082,7 +946,6 @@ internal fun SearchIcon(description: String, onClick: () -> Unit) { ) { Icon(Icons.Filled.Search, description, tint = if (hot) pal.signal else colors.onSurfaceVariant, modifier = Modifier.size(20.dp)) } } -/** A horizontal row of cards under a title; nothing at all when the list is empty or not here yet. */ @Composable private fun Shelf(title: String, items: List<CatalogItem>?, card: @Composable (CatalogItem) -> Unit) { if (items.isNullOrEmpty()) return @@ -1093,7 +956,6 @@ private fun Shelf(title: String, items: List<CatalogItem>?, card: @Composable (C } } -/** Only visible cards (and the grid's prefetch) compose; [empty] spans the row when there are none. */ private fun LazyGridScope.storeGrid(games: List<CatalogItem>, empty: String, card: @Composable (CatalogItem) -> Unit) { if (games.isEmpty()) item(key = "empty", span = { GridItemSpan(maxLineSpan) }) { Box(Modifier.padding(vertical = 32.dp).fillMaxWidth(), contentAlignment = Alignment.Center) { @@ -1103,19 +965,8 @@ private fun LazyGridScope.storeGrid(games: List<CatalogItem>, empty: String, car else items(games, key = { it.key }, contentType = { "game" }) { card(it) } } -/** - * A card's least width: shelf cards are this wide and a grid fits as many columns as the width - * allows at it (the columns then share the leftover), so a display-density setting changes the - * column count rather than the cards' size. 25% under the first 150 dp: five columns where there - * were four on the Pocket FIT. - */ private val CardWidth = 112.dp -/** - * One title: its wide art, the title, a price or status line and one button that says the one - * thing to do next - Install with its size, Play, the download's progress, Get for free, or View - * on the store. Tapping the card itself opens the game's page. - */ @Composable private fun GameCard(item: CatalogItem, store: Store, s: FrontEndState, a: FrontEndActions, installedKeys: Set<String>, onOpen: (String) -> Unit, first: Boolean = false, navigation: Modifier = Modifier) { val ctx = LocalContext.current @@ -1127,14 +978,12 @@ private fun GameCard(item: CatalogItem, store: Store, s: FrontEndState, a: Front val scale by animateFloatAsState(if (pressed) 0.985f else 1f, Motion.sp(0.5f, Spring.StiffnessMedium), label = "cardScale") val installed = item.id in installedKeys val download = StoresState.download("${store.id}:${item.id}")?.takeIf { it.isActive } - // Where it sits, so the game page can flood out of it (and draw back into it). val placed = remember { arrayOfNulls<androidx.compose.ui.layout.LayoutCoordinates>(1) } val corner = with(androidx.compose.ui.platform.LocalDensity.current) { 10.dp.toPx() } val open = { placed[0]?.takeIf { it.isAttached }?.let { StoresMotion.markCard(CardMark(item.key, it.boundsInRoot(), corner, item.imageUrl ?: item.tallImageUrl)) } onOpen(item.key) } - // A size the library did not give is looked up once the card is on screen or close to it. val screen = with(androidx.compose.ui.platform.LocalDensity.current) { androidx.compose.ui.platform.LocalConfiguration.current.screenHeightDp.dp.toPx() } val ahead = screen / 2 Column( @@ -1154,7 +1003,6 @@ private fun GameCard(item: CatalogItem, store: Store, s: FrontEndState, a: Front } } -/** A store's sync as a thin bar and its count ("25/55"); a moving bar when it has no count yet. */ @Composable private fun SyncBar(line: String) { val count = Regex("(\\d+)\\s*/\\s*(\\d+)").find(line) @@ -1169,14 +1017,8 @@ private fun SyncBar(line: String) { } } -/** How tall the card's one line is, over the blurred foot of its art. */ private val CardStrip = 26.dp -/** - * The card is its art: sharp above, and under the one line a blurred, darkened copy of the same - * picture, placed so the two meet without a seam. Blur needs API 31; below it the copy is only - * darkened, more strongly. - */ @OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class) @Composable private fun CardFace(item: CatalogItem, download: com.droiddeck.launcher.stores.download.DownloadEntry?, hot: Boolean, marker: @Composable () -> Unit) { @@ -1196,7 +1038,6 @@ private fun CardFace(item: CatalogItem, download: com.droiddeck.launcher.stores. verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.matchParentSize().padding(horizontal = 8.dp), ) { - // A title too long for the strip scrolls through while the card has focus. Text( item.title, fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White, maxLines = 1, overflow = if (hot) TextOverflow.Clip else TextOverflow.Ellipsis, @@ -1205,7 +1046,6 @@ private fun CardFace(item: CatalogItem, download: com.droiddeck.launcher.stores. marker() } } - // A soft edge where the sharp art meets the strip. Spacer(Modifier.align(Alignment.BottomCenter).padding(bottom = CardStrip).fillMaxWidth().height(14.dp).background(Brush.verticalGradient(0f to Color.Transparent, 1f to Color.Black.copy(alpha = 0.35f)))) if (download != null) Box(Modifier.align(Alignment.BottomCenter).padding(start = 6.dp, end = 6.dp, bottom = CardStrip + 5.dp)) { ProgressBarThin(download.fraction, paused = download.state == DownloadState.PAUSED, verify = download.stage == DownloadStage.VERIFY) @@ -1214,14 +1054,12 @@ private fun CardFace(item: CatalogItem, download: com.droiddeck.launcher.stores. } } -/** The card's right-hand value: done, resume, progress, size, or the price. */ @Composable private fun CardMarker(item: CatalogItem, installed: Boolean, download: com.droiddeck.launcher.stores.download.DownloadEntry?) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current val small = 10.sp when { - // Installed: its size on disk and a small check after it. installed -> Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(3.dp)) { val ctx = LocalContext.current val game = StoresState.installedGame(item.store, item.id) @@ -1231,7 +1069,6 @@ private fun CardMarker(item: CatalogItem, installed: Boolean, download: com.droi } download != null -> Text("${download.percent}%", fontSize = small, fontWeight = FontWeight.Bold, color = pal.signal, maxLines = 1) item.owned && StoresState.isUnfinished(item) -> Text(stringResource(R.string.stores_resume), fontSize = small, fontWeight = FontWeight.Bold, color = pal.signal, maxLines = 1) - // The size in the title's own type; nothing until it is known. item.owned -> com.droiddeck.launcher.stores.StoreSizes.size(item).takeIf { it > 0 }?.let { Text(formatBytes(it), fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White, maxLines = 1) } item.isFree -> Text(stringResource(R.string.stores_free), fontSize = small, fontWeight = FontWeight.Bold, color = pal.good, maxLines = 1) item.isDiscounted -> Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { @@ -1246,8 +1083,6 @@ private fun CardMarker(item: CatalogItem, installed: Boolean, download: com.droi internal fun CardArt(item: CatalogItem, modifier: Modifier) { val url = item.imageUrl ?: item.tallImageUrl val colors = MaterialTheme.colorScheme - // A dark ground under the image, so art with transparency never ends in white, and a quiet - // fade into the card's surface at the foot. Box(modifier.background(colors.surfaceVariant).then(if (url == null) Modifier.background(artBrush(hueOf(item.title))) else Modifier)) { if (url != null) AsyncImage(model = url, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize()) else Text(item.title, fontSize = 10.sp, fontWeight = FontWeight.Black, color = Color.White, maxLines = 3, overflow = TextOverflow.Ellipsis, modifier = Modifier.align(Alignment.BottomStart).padding(6.dp)) @@ -1260,8 +1095,6 @@ internal fun ProgressBarThin(fraction: Float, paused: Boolean = false, verify: B val pal = LocalPalette.current val colors = MaterialTheme.colorScheme val fill = when { done -> pal.good; paused -> Color(0xFFFFC24D); verify -> Color(0xFF9B6DFF); else -> pal.signal } - // The fill glides to each new value over a quarter second - progress arrives a few times a second - // and in steps - and is read only while drawing, so the bar animates without relayout. val shown = androidx.compose.animation.core.animateFloatAsState( fraction.coerceIn(0f, 1f), androidx.compose.animation.core.tween(250, easing = androidx.compose.animation.core.LinearEasing), label = "bar", ) @@ -1278,7 +1111,6 @@ internal fun downloadLabel(d: com.droiddeck.launcher.stores.download.DownloadEnt else -> if (d.stageFraction >= 0f) stringResource(R.string.stores_dl_active_percent, activeStageLabel(d), d.percent) else activeStageLabel(d) } -/** The active stage as the row and the page name it: Checking (files already there), Downloading, Verifying, Installing. */ @Composable internal fun activeStageLabel(d: com.droiddeck.launcher.stores.download.DownloadEntry): String = when (d.stage) { DownloadStage.MANIFEST -> if (d.stageItemsTotal > 0) stringResource(R.string.stores_stage_checking) else stringResource(R.string.stores_stage_manifest) @@ -1288,7 +1120,6 @@ internal fun activeStageLabel(d: com.droiddeck.launcher.stores.download.Download DownloadStage.DONE -> stringResource(R.string.stores_stage_done) } -/** Two taps: the first arms it ([confirm] shows), the second acts; it disarms itself after a few seconds. */ @Composable internal fun ConfirmButton(label: String, confirm: String, compact: Boolean = false, onConfirm: () -> Unit) { var armed by remember { mutableStateOf(false) } @@ -1305,19 +1136,13 @@ internal fun stageLabel(stage: DownloadStage): String = when (stage) { DownloadStage.DONE -> stringResource(R.string.stores_stage_done) } -/** The game's web page, in the device's browser: this app buys nothing and claims nothing itself. */ internal fun openStoreUrl(ctx: android.content.Context, item: CatalogItem) = openUrl(ctx, item.storeUrl) -/** A web page in the browser; nothing when the URL is blank or no app can open it. */ internal fun openUrl(ctx: android.content.Context, url: String) { if (url.isBlank()) return runCatching { ctx.startActivity(android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(url)).addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)) } } -/** - * Launches an installed store game the way the Games tab does: through its Steam shortcut. The - * shortcut exists once the client has (re)started since the install; until then the user is told. - */ internal fun launchStoreGame(ctx: android.content.Context, store: Store, id: String, s: FrontEndState, a: FrontEndActions) { val game = s.steamGames.firstOrNull { it.source == store.id && it.storeId == id } if (game == null) { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/StoresWidgets.kt b/app/src/main/java/com/droiddeck/launcher/ui/StoresWidgets.kt index b4e6e5443..c2580a329 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/StoresWidgets.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/StoresWidgets.kt @@ -5,9 +5,7 @@ import androidx.compose.ui.res.stringResource import com.droiddeck.launcher.R import com.droiddeck.launcher.stores.StoresNative -// Controls the Stores section and its Setup group share. -/** Whether the native download engine is in this build, said where the stores are turned on. */ @Composable internal fun StoresEngineRow() { val version = StoresNative.version diff --git a/app/src/main/java/com/droiddeck/launcher/ui/TestTags.kt b/app/src/main/java/com/droiddeck/launcher/ui/TestTags.kt index 12c5ee390..4e69a2264 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/TestTags.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/TestTags.kt @@ -5,10 +5,5 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId -/** - * Compose test tags as Android resource ids below this node, so UI Automator (`uiautomator dump`, - * droiddeckctl ui-dump) sees stable ids such as `rail-steam` or `setting-logs` rather than only - * labels that change with the language. Tags are named in docs/agent-control.md. - */ @OptIn(ExperimentalComposeUiApi::class) fun Modifier.exposeTestTags(): Modifier = semantics { testTagsAsResourceId = true } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/Theme.kt b/app/src/main/java/com/droiddeck/launcher/ui/Theme.kt index d8c44bae0..86980362f 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/Theme.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/Theme.kt @@ -66,10 +66,6 @@ object Themes { val LocalPalette = staticCompositionLocalOf { Themes.byId(Themes.GRAPHITE) } -/** - * Text and icons drawn on a [Palette.signal] fill: near-black where the blue is light enough that - * white would not read (Graphite's #1A9FFF gives white 2.8:1), white where it is deep. - */ val Palette.onSignal: Color get() = if (signal.luminance() > 0.18f) Color(0xFF03111F) else Color.White @Composable diff --git a/app/src/main/java/com/droiddeck/launcher/ui/Throbber.kt b/app/src/main/java/com/droiddeck/launcher/ui/Throbber.kt index c08188bd8..9e77f1504 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/Throbber.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/Throbber.kt @@ -20,17 +20,12 @@ import kotlin.math.cos import kotlin.math.sin import kotlin.math.sqrt -/** - * The DroidDeck mark as the loading indicator. Every loop the D's square half bends into an arc, - * the ring they make turns once around the ball, and the half straightens back into the D. - */ @Composable internal fun LogoThrobber(modifier: Modifier = Modifier, running: Boolean = true) { val ink = MaterialTheme.colorScheme.onBackground val ball = LocalPalette.current.signal val loop = rememberInfiniteTransition(label = "throbber") .animateFloat(0f, 1f, infiniteRepeatable(tween(LOOP_MS, easing = LinearEasing)), label = "loop") - // Animations off in the system settings, or nothing running: the plain D. val still = !running || Motion.scale == 0f val left = remember { Path() } val right = remember { Path() } @@ -64,7 +59,6 @@ internal fun LogoThrobber(modifier: Modifier = Modifier, running: Boolean = true } } -/** The ball of a throbber laid out in [bounds]: its centre and its radius, at rest. */ internal fun throbberBall(bounds: androidx.compose.ui.geometry.Rect): Pair<Offset, Float> { val s = bounds.width / LogoShape.WIDTH return Offset(bounds.left + (LogoShape.CX - LogoShape.LEFT) * s, bounds.top + LogoShape.CY * s) to LogoShape.BALL * s @@ -74,12 +68,6 @@ private const val LOOP_MS = 1900 private val Settle = CubicBezierEasing(0.5f, 0f, 0.3f, 1f) private val Turn = CubicBezierEasing(0.65f, 0f, 0.35f, 1f) -/** - * The logo's geometry in its SVG's own units (viewBox -55.98 0 239.92 223.72): a D of a square - * left side and a round right side, cut by a ring around the ball and split top and bottom. - * The left half is sampled along rays from the centre so it can bend, point for point, into the - * mirror of the right half's arc. - */ internal object LogoShape { const val LEFT = -55.98f const val WIDTH = 239.92f @@ -97,7 +85,6 @@ internal object LogoShape { private fun at(a: Double, r: Float) = Pt((CX + r * cos(a)).toFloat(), (CY + r * sin(a)).toFloat()) private fun angle(x: Float, y: Float) = atan2((y - CY).toDouble(), (x - CX).toDouble()) - // Angles taken the long way round, through the left: from about -94 degrees down to -266. private fun leftward(a: Double) = if (a < 0) a else a - 2 * Math.PI private val rays: List<Double> = run { @@ -105,7 +92,6 @@ internal object LogoShape { val topCorner = angle(SQUARE_X, 0f) val bottomCorner = leftward(angle(SQUARE_X, HEIGHT)) val bottom = leftward(angle(SPLIT_LEFT, HEIGHT)) - // The corners are rays of their own, so the square half keeps sharp corners. fun span(a: Double, b: Double, n: Int) = List(n) { i -> a + (b - a) * i / (n - 1) } span(top, topCorner, 9).dropLast(1) + span(topCorner, bottomCorner, 25).dropLast(1) + span(bottomCorner, bottom, 9) } @@ -148,7 +134,6 @@ internal object LogoShape { List(40) { i -> at(innerFrom + (innerTo - innerFrom) * i / 39, INNER) } } - /** The left half, [bend] of the way from the square D side (0) to the arc (1). */ fun left(path: Path, bend: Float, px: (Float, Float) -> Offset) { path.reset() square.forEachIndexed { i, q -> diff --git a/app/src/main/java/com/droiddeck/launcher/ui/UpdatesPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/UpdatesPage.kt index ff6613365..a44f5ca19 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/UpdatesPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/UpdatesPage.kt @@ -68,16 +68,13 @@ import com.droiddeck.launcher.update.AppUpdates.Follow import com.droiddeck.launcher.update.AppUpdates.Offer import com.droiddeck.launcher.update.AppUpdates.Release -/** The Updates page: the latest build of each channel, the one followed, and an update in progress. */ class UpdatesState( val catalog: AppUpdates.Catalog? = null, val follow: Follow = Follow(Channel.STABLE), val checking: Boolean = false, val error: String? = null, - /** "Downloading", "Installing", ... while an update is under way; [percent] is -1 when unknown. */ val stage: String? = null, val percent: Int = -1, - /** Explain "Install unknown apps" before sending the user to it. */ val askPermission: Boolean = false, ) { val hasUpdate: Boolean get() = AppUpdates.hasUpdate(catalog, follow) @@ -99,14 +96,11 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie val colors = MaterialTheme.colorScheme Column(modifier = modifier) { PageHeader(stringResource(R.string.upd_title)) { - // The build that is running, beside the title where it is always in view. Text( stringResource(R.string.upd_build, me.version, s.buildLabel), fontSize = 12.5.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f).padding(top = 6.dp), ) val checked = u.catalog?.checkedAt?.takeIf { it > 0 } - // The relative time comes capitalized to stand alone (Spanish "Hace 5 minutos"); here it - // ends a sentence. if (!LocalNarrowPane.current) Text( if (u.checking) stringResource(R.string.common_checking) else if (checked != null) stringResource(R.string.upd_checked, ago(checked).replaceFirstChar { it.lowercase() }) else stringResource(R.string.upd_not_checked), fontSize = 13.sp, color = colors.onSurfaceVariant, @@ -114,9 +108,6 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie ToolIcon(Icons.Outlined.Refresh, stringResource(R.string.store_check_updates), busy = u.checking, enabled = !u.checking && u.stage == null, onClick = ua.onCheck) } BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) { - // Landscape has the width for channels and status side by side; a narrow pane, or a small - // phone's (where each channel's name and date would be cut to a word), stacks them. The - // Thor's top screen leaves about 690dp here and keeps them side by side. val stacked = LocalNarrowPane.current || maxWidth < 600.dp Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(bottom = 8.dp)) { if (stacked) { @@ -125,7 +116,6 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie ChannelLabel() ChannelPicker(u, ua) } else { - // The label sits over both columns, so the status card lines up with the first channel. ChannelLabel() Row(horizontalArrangement = Arrangement.spacedBy(18.dp)) { Box(Modifier.weight(1f)) { ChannelPicker(u, ua) } @@ -148,7 +138,6 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie ) } -/** Where things stand, as a system updater says it: a status line, what you'd get, and one button. */ @Composable private fun StatusPanel(s: FrontEndState, u: UpdatesState, ua: UpdatesActions, me: AppUpdates.Installed) { val colors = MaterialTheme.colorScheme @@ -216,7 +205,6 @@ private fun StatusPanel(s: FrontEndState, u: UpdatesState, ua: UpdatesActions, m } } -/** The change and its notes, two lines each; More opens the rest when either is cut short. */ @Composable private fun ReleaseNotes(title: String, notes: String, key: String) { val colors = MaterialTheme.colorScheme @@ -246,7 +234,6 @@ private fun ReleaseNotes(title: String, notes: String, key: String) { } } -/** The headline for a build on offer: "DroidDeck 0.3.0", "New Preview build", "PR #93 test build". */ @Composable private fun newBuild(f: Follow, r: Release) = when (f.channel) { Channel.STABLE -> stringResource(R.string.upd_new_stable, r.version ?: r.tag) @@ -254,14 +241,12 @@ private fun newBuild(f: Follow, r: Release) = when (f.channel) { Channel.TEST -> stringResource(R.string.upd_new_test, r.pr) } -/** A PR title as a sentence: "fix(steam): keep the client alive" -> "Keep the client alive". */ private fun changeTitle(t: String): String = t.replace(Regex("""^[a-z]+(\([^)]*\))?!?:\s*"""), "").replaceFirstChar { it.uppercase() } @Composable private fun megabytes(bytes: Long) = stringResource(R.string.upd_mb, ((bytes + 524_288) / 1_048_576).toInt()) -/** The three channels as cards to pick from; Test builds opens its list of PRs under it. */ @Composable private fun ChannelLabel() { Text( @@ -297,7 +282,6 @@ private fun ChannelPicker(u: UpdatesState, ua: UpdatesActions) { } } -/** Read-only Preview history below the status card action; only the latest build is installable. */ @Composable private fun PreviewHistory(catalog: AppUpdates.Catalog?, me: AppUpdates.Installed) { val history = catalog?.let { AppUpdates.previewHistory(it, me) } ?: return @@ -363,8 +347,6 @@ private fun ChannelCard( val pal = LocalPalette.current val src = remember { MutableInteractionSource() } val hot = rememberHot(src) - // Focus shows at once, as on the Components page: no ripple, no fade. Selection is the radio - // dot, never a ring of its own: the ring is only ever focus. val fill = if (hot) pal.signal.copy(alpha = 0.14f) else Color.Transparent Row( verticalAlignment = Alignment.CenterVertically, @@ -391,7 +373,6 @@ private fun ChannelCard( } } -/** The one mark of a chosen option: a filled dot in a ring, beside it. */ @Composable private fun RadioDot(selected: Boolean, size: androidx.compose.ui.unit.Dp = 22.dp) { val pal = LocalPalette.current @@ -400,7 +381,6 @@ private fun RadioDot(selected: Boolean, size: androidx.compose.ui.unit.Dp = 22.d } } -/** One test build under Test builds: its PR, what it fixes, and how fresh it is. */ @Composable private fun TestRow(t: Release, selected: Boolean, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme diff --git a/app/src/main/java/com/droiddeck/launcher/ui/WinComponentsEditor.kt b/app/src/main/java/com/droiddeck/launcher/ui/WinComponentsEditor.kt index bfd227952..155c6e9b2 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/WinComponentsEditor.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/WinComponentsEditor.kt @@ -52,15 +52,9 @@ import androidx.compose.runtime.withFrameNanos import kotlinx.coroutines.withContext import java.io.File -// Windows components for one added game, Bannerlator's list: what the game's folder suggests first, -// then everything that installs here, then the ones that need a Windows installer (not yet). Each -// one is a switch, downloaded the first time it is turned on for any game; the launch puts the -// picked ones into the game's prefix (droiddeck-wincomponents). -/** Bannerlator's detector names installers; here the DLL-copy twin is the one that installs. */ private fun installable(name: String, all: Map<String, WinComponents.Component>): String = WinComponents.installable(name, all) -/** A store as a list's owner names it in a reason line. */ @Composable private fun originName(origin: String): String = stringResource( when (origin) { @@ -71,7 +65,6 @@ private fun originName(origin: String): String = stringResource( }, ) -/** Why a component was turned on by itself, from [WinComponents.AutoReason] (args: what the list called it, then the origin's own). */ @Composable private fun autoReason(r: WinComponents.AutoReason): String = when (r.kind) { WinCompSources.Origin.STEAM.name -> stringResource(R.string.wincomp_auto_steam, r.args.getOrElse(1) { "" }, r.args.getOrElse(2) { "" }, r.args.getOrElse(3) { "" }) @@ -81,7 +74,6 @@ private fun autoReason(r: WinComponents.AutoReason): String = when (r.kind) { else -> "" } -/** The reason line of a finding that is only recommended. */ @Composable private fun findingReason(f: WinCompSources.Finding): String = when (f.origin) { WinCompSources.Origin.FOLDER -> stringResource( @@ -96,9 +88,7 @@ private fun findingReason(f: WinCompSources.Finding): String = when (f.origin) { @Composable internal fun WinComponentsDialog( appKey: String, gameName: String, gameDir: File?, byPad: Boolean, - /** compatdata/<id>: what the prefix already has. [steamAppId]: a Steam title, whose appmanifest lists Steam's own redists. */ compat: File? = null, steamAppId: Int? = null, - /** The game as the Games tab has it: its source decides whose list is trusted ([WinCompSources]). */ game: com.droiddeck.launcher.frontend.Library.SteamGame? = null, onClose: () -> Unit, ) { @@ -106,33 +96,22 @@ internal fun WinComponentsDialog( val coroutine = rememberCoroutineScope() val shown = rememberShown(onClose) val close = { shown.targetState = false } - // null while the catalog loads; empty when it could not be had. var catalog by remember { mutableStateOf<Map<String, WinComponents.Component>?>(null) } var offline by remember { mutableStateOf(false) } var findings by remember { mutableStateOf(emptyList<WinCompSources.Finding>()) } - // Whether the game's list has been worked out at least once (kept from an earlier time counts). var findingsKnown by remember { mutableStateOf(game == null) } - // "Loading the components" only when the catalog takes a while. var slowCatalog by remember { mutableStateOf(false) } LaunchedEffect(Unit) { kotlinx.coroutines.delay(300); slowCatalog = true } var installed by remember { mutableStateOf(emptySet<String>()) } - // Detector names (oalinst, physx...) already in the prefix from elsewhere. var present by remember { mutableStateOf(emptySet<String>()) } var selection by remember { mutableStateOf(WinComponents.selection(context, appKey)) } val picks = selection.effective - // The row being worked on: its id, the latest progress (a dependency's while that installs - // first), and whether it is turning off rather than on. The row greys out and shows the bar - // the moment the switch is pressed, and only that row; the rest of the page stays as it is. var busyId by remember { mutableStateOf<String?>(null) } var progress by remember { mutableStateOf<WinComponents.Progress?>(null) } var reverting by remember { mutableStateOf(false) } var error by remember { mutableStateOf<String?>(null) } - // The component the error is about, so it shows on its own row and not only at the bottom. var failed by remember { mutableStateOf<String?>(null) } var showWaiting by remember { mutableStateOf(false) } - // Done holds focus while the list loads, then the top switch takes it - opened by touch or by - // pad, so the d-pad always has somewhere to start; LB and RB jump between the sections, since - // the full list runs past sixty switches. val recFocus = remember { FocusRequester() } val recFocusAfterAuto = remember { FocusRequester() } val activeFocus = remember { FocusRequester() } @@ -145,10 +124,7 @@ internal fun WinComponentsDialog( LaunchedEffect(Unit) { val (entries, found) = withContext(Dispatchers.IO) { val list = WinComponents.fetch(context) - // The source's list first (and what it turns on by itself, already installed), then the folder scan. if (game != null) { - // The list kept from last time, at once; it is worked out again (and its components - // queued) on the background worker, which this page then follows. com.droiddeck.launcher.frontend.AutoComponents.queueGame(context, game, asked = true) val kept = com.droiddeck.launcher.frontend.AutoComponents.cachedFindings(context, appKey) if (kept != null) findingsKnown = true @@ -166,11 +142,7 @@ internal fun WinComponentsDialog( present = withContext(Dispatchers.IO) { PrefixInstalledDetector.detect(compat) } installed = withContext(Dispatchers.IO) { WinComponents.installedIds(context).toSet() } } - /** The user's own switch for [id]: kept apart from what was turned on by itself, and over it. */ - // Automatic picks still downloading show on their rows; when one lands the page reads the picks again. val autoDownloading by com.droiddeck.launcher.frontend.AutoComponents.downloading.collectAsState() - // Without a network a component not downloaded yet cannot be turned on; the rows say so and - // come back the moment the network does. val online = rememberOnline() val autoChanges by com.droiddeck.launcher.frontend.AutoComponents.changes.collectAsState() val pendingGames by com.droiddeck.launcher.frontend.AutoComponents.pendingGames.collectAsState() @@ -186,14 +158,10 @@ internal fun WinComponentsDialog( coroutine.launch(Dispatchers.IO) { WinComponents.setUser(context, appKey, id, on) } } fun toggle(id: String, on: Boolean) { - // Switches stay enabled during a download - a disabled one drops the pad's focus - so a press - // then is ignored here; the busy row is greyed instead. if (busyId != null) return error = null failed = null if (!on) { - // Turning off is a saved pick, quick, but the row greys out until it is written so the - // press is seen to land; the files leave the prefix at the game's next launch. busyId = id; reverting = true; progress = null selection = WinComponents.Selection(selection.auto, selection.user + (id to false)) coroutine.launch { @@ -219,21 +187,12 @@ internal fun WinComponentsDialog( } val all = catalog - // The first data is in: rows that show up from now on ease in. var arrived by remember { mutableStateOf(false) } LaunchedEffect(all != null && findingsKnown) { if (all != null && findingsKnown) { withFrameNanos { }; arrived = true } } val here = if (all == null) emptySet() else present.map { installable(it, all) }.toSet() fun supportOf(id: String): Support = all?.get(id)?.let { WinComponents.support(it, all) } ?: if (id in installed) Support.READY else Support.UNSUPPORTED - // Turned on by itself for this game, each with its reason; then what is only recommended (the - // folder scan, a name match, an auto pick that cannot install here); then what the lists name - // that is no component here, by its own name. - // Still being worked out on the background worker: the page says so instead of "nothing found". - // Only before the game's list has ever been worked out: a list kept from before shows at once and - // is checked again quietly, its changes easing in. val checking = game != null && !findingsKnown - // Automatic picks on their way (queued or downloading) sit in their section already, with - // their progress, and settle there when they land. val turningOn = if (all == null) emptyMap() else findings.filter { it.auto && it.component != null } .associateBy { installable(it.component!!, all) } .filterKeys { it !in selection.auto && selection.user[it] != false && (it in pendingComponents || it in autoDownloading) } @@ -244,10 +203,6 @@ internal fun WinComponentsDialog( val suggestions = findings.filter { it.component == null }.distinctBy { it.origin to it.original } val ready = all.orEmpty().values.filter { WinComponents.support(it, all.orEmpty()) == Support.READY }.map { it.name } val extra = (installed + picks).filter { all?.containsKey(it) != true } - // What is turned on for this game gets its own section under the recommendations, which stay - // where they are; the rest of the list is everything else, so a component turned off goes back - // to its place in the alphabet. A component is listed once: the recommendations are left out - // of the full list too. val recSet = recIds.map { installable(it.component!!, all.orEmpty()) }.toSet() + autoIds val active = picks.filter { it !in recSet && (all?.containsKey(it) == true || it in installed) }.distinct() .sortedBy { WinComponentNames.of(it).lowercase() } @@ -263,12 +218,10 @@ internal fun WinComponentsDialog( runCatching { sections[section].requestFocus() } } LaunchedEffect(all != null) { - // The dialog's window starts in touch mode, where a switch cannot hold focus at all. inputMode.requestInputMode(InputMode.Keyboard) section = 0 val target = if (all == null) doneFocus else sections.first() switchFocused = false - // It animates in and the list composes over a few frames: ask until the switch has it. repeat(60) { withFrameNanos { } runCatching { target.requestFocus() } @@ -296,7 +249,6 @@ internal fun WinComponentsDialog( support == Support.UNSUPPORTED -> stringResource(R.string.wincomp_unsupported) else -> null } - // The catalog key stays in the detail: it is what a log or a bug report names. val detail = listOfNotNull(reason, c?.description?.takeIf { it.isNotEmpty() }, status, id).joinToString(" · ") val offlineHere = !online && id !in installed val usable = support == Support.READY && !offlineHere @@ -323,13 +275,11 @@ internal fun WinComponentsDialog( .onFocusChanged { if (it.isFocused) switchFocused = true }, ) { on -> toggle(id, on) } } - /** The section's requester goes to its first switch that can take focus. */ fun firstUsable(ids: List<String>, focus: FocusRequester): (String) -> FocusRequester? { val first = ids.firstOrNull { supportOf(it) == Support.READY } return { id -> focus.takeIf { id == first } } } - // The lists only once the catalog is in, faded in once: no half list counted before it. androidx.compose.animation.AnimatedVisibility( visible = all != null, enter = androidx.compose.animation.fadeIn(Motion.tw(220)), @@ -429,7 +379,6 @@ internal fun WinComponentsDialog( } } -/** Once the page has its first data ([arrived]), a row that shows up eases in; the first ones are simply there. */ @Composable private fun Arrive(arrived: Boolean, content: @Composable () -> Unit) { val state = remember { androidx.compose.animation.core.MutableTransitionState(!arrived) }.apply { targetState = true } @@ -442,7 +391,6 @@ private fun Arrive(arrived: Boolean, content: @Composable () -> Unit) { ) { content() } } -/** Whether the default network is there and validated, followed live. */ @Composable private fun rememberOnline(): Boolean { val context = LocalContext.current @@ -474,12 +422,6 @@ private fun Panel(content: @Composable androidx.compose.foundation.layout.Column private fun Divider() = Box(Modifier.fillMaxWidth().padding(horizontal = 14.dp).heightIn(min = 1.dp, max = 1.dp).background(LocalPalette.current.line)) -/** - * One component: name, detail and its switch; while [busy] the row is greyed and a thin bar runs - * across it with [caption] above (the download, then the install, each 0..100 as [percent], or - * indeterminate at -1). The switch stays enabled while busy so a pad's focus does not drop; the - * press is ignored by the caller instead. - */ @Composable private fun ComponentRow( name: String, detail: String, checked: Boolean, enabled: Boolean, dim: Boolean, busy: Boolean = false, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/WirelessAdbFixPage.kt b/app/src/main/java/com/droiddeck/launcher/ui/WirelessAdbFixPage.kt index f934ac8ba..8117c3ee5 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/WirelessAdbFixPage.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/WirelessAdbFixPage.kt @@ -358,7 +358,6 @@ internal fun AdbTextField( onFocusChange: (Boolean) -> Unit = {}, onNext: (() -> Unit)? = null, onDone: (() -> Unit)? = null, - /** Takes focus (and the keyboard) once, as soon as it is shown: a field that opened on request. */ focusOnShow: Boolean = false, ) { val context = LocalContext.current diff --git a/app/src/main/java/com/droiddeck/launcher/ui/Wordmark.kt b/app/src/main/java/com/droiddeck/launcher/ui/Wordmark.kt index 48af1ea26..c0aefad25 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/Wordmark.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/Wordmark.kt @@ -17,10 +17,6 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import com.droiddeck.launcher.R -/** - * The DroidDeck wordmark from the README banner (artwork/droiddeck-banner-*.svg): the mark - * standing in as the D, then "roidDeck". Letters and D in the theme's ink, the ball in its blue. - */ @Composable internal fun Wordmark(height: Dp, modifier: Modifier = Modifier) { val ink = MaterialTheme.colorScheme.onBackground @@ -34,7 +30,6 @@ internal fun Wordmark(height: Dp, modifier: Modifier = Modifier) { scale(k, k, Offset.Zero) { translate(-BOX_X, -BOX_Y) { drawPath(letters, ink) - // The mark's placement in the banner: translate(239.135 123.36) scale(0.527445) translate(31.98 0). translate(239.135f, 123.36f) { scale(MARK_SCALE, MARK_SCALE, Offset.Zero) { translate(31.98f, 0f) { @@ -49,7 +44,6 @@ internal fun Wordmark(height: Dp, modifier: Modifier = Modifier) { } } -// The banner's own units: the wordmark spans x 239.135..1040.7 and y 120.02..243.2. private const val BOX_X = 239.135f private const val BOX_Y = 120.02f private const val BOX_W = 1040.7f - 239.135f diff --git a/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt b/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt index fc6a4f02c..22f0cc22f 100644 --- a/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt +++ b/app/src/main/java/com/droiddeck/launcher/update/AppUpdates.kt @@ -12,13 +12,6 @@ import java.net.HttpURLConnection import java.net.URL import java.security.MessageDigest -/** - * DroidDeck's published builds. CI turns GitHub Releases into one static catalog in DroidDeck-CI; - * the app consumes that contract instead of inferring channels from tags and release-note prose. - * - * [Channel.NIGHTLY] is intentionally kept as the internal enum/pref value for upgrades from older - * DroidDeck versions. The channel is presented to users as Preview. - */ object AppUpdates { private const val REPO = "Droid-Deck/DroidDeck" private const val CI_REPO = "Droid-Deck/DroidDeck-CI" @@ -164,7 +157,6 @@ object AppUpdates { r.commit.startsWith(me.commit, ignoreCase = true) } - /** Recent Preview changes relative to a CI-installed commit, when its position is known. */ fun previewHistory(catalog: Catalog, me: Installed = installed()): PreviewHistory? { if (!me.ciBuild || catalog.recentPreviews.isEmpty()) return null val stable = catalog.stable @@ -195,14 +187,12 @@ object AppUpdates { (installed.length < 40 && published.startsWith(installed, ignoreCase = true)) } - /** Why Android cannot install this published build over [me], before downloading anything. */ fun installBlock(r: Release, me: Installed = installed()): InstallBlock? = when { r.apk == null -> InstallBlock(R.string.upd_block_no_apk) r.apk.versionCode < me.versionCode -> InstallBlock(R.string.upd_block_version_code, r.apk.versionCode, me.versionCode) else -> null } - /** Why Android would refuse a build, as a line for the user. */ class InstallBlock(@androidx.annotation.StringRes val text: Int, private vararg val args: Any) { fun message(context: Context): String = context.getString(text, *args) } @@ -222,7 +212,6 @@ object AppUpdates { } fun setFollow(context: Context, follow: Follow) { - // Keep "nightly" on disk so an older DroidDeck still understands the preference after rollback. val value = if (follow.channel == Channel.TEST) "test:${follow.pr}" else follow.channel.name.lowercase() prefs(context).edit().putString(KEY_CHANNEL, value).apply() @@ -245,11 +234,9 @@ object AppUpdates { return runCatching { readCachedCatalog(JSONObject(raw)) }.getOrNull() } - /** One static request, rather than several unauthenticated GitHub API requests. */ fun refresh(context: Context): Catalog { val checkedAt = System.currentTimeMillis() val published = JSONObject(get(context, "$CATALOG_URL?checked=$checkedAt")) - // Its checks say why in English; the user gets that inside a sentence in the app's language. val catalog = try { readPublishedCatalog(published, context.packageName, checkedAt) } catch (e: IOException) { diff --git a/app/src/main/java/com/droiddeck/launcher/update/SelfInstaller.kt b/app/src/main/java/com/droiddeck/launcher/update/SelfInstaller.kt index 4700302e2..959dcde2a 100644 --- a/app/src/main/java/com/droiddeck/launcher/update/SelfInstaller.kt +++ b/app/src/main/java/com/droiddeck/launcher/update/SelfInstaller.kt @@ -19,23 +19,15 @@ import java.net.HttpURLConnection import java.net.URL import java.security.MessageDigest -/** - * Installs one of DroidDeck's own builds over itself: download into the cache, check the file - * against GitHub's sha256, then hand it to PackageInstaller. Android shows its own "Update this - * app?" prompt, and the app is restarted on the new build. - */ object SelfInstaller { - /** The broadcast PackageInstaller answers on; the launcher registers for it while it is open. */ const val ACTION_STATUS = "com.droiddeck.launcher.update.INSTALL_STATUS" - /** Android lets an app install packages only once the user allows it in "Install unknown apps". */ fun canInstall(context: Context): Boolean = Build.VERSION.SDK_INT < 26 || context.packageManager.canRequestPackageInstalls() fun permissionIntent(context: Context): Intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:${context.packageName}")) - /** Downloads [apk] into the cache and checks it; a download left from before is replaced. */ fun download(context: Context, apk: AppUpdates.Apk, progress: (Int) -> Unit): File { val dir = File(context.cacheDir, "updates") FileUtils.delete(dir) @@ -80,7 +72,6 @@ object SelfInstaller { } } - /** Inspect the actual downloaded APK before giving it to Android's installer. */ private fun validateDownloaded(context: Context, file: File, apk: AppUpdates.Apk) { if (apk.packageName != context.packageName) { throw IOException(context.getString(R.string.selfinst_wrong_package, apk.packageName, context.packageName)) @@ -108,8 +99,6 @@ object SelfInstaller { throw IOException(context.getString(R.string.selfinst_downgrade, code, BuildConfig.VERSION_CODE)) } - // Before Android 9 this lineage intentionally presents the public legacy test key. Android - // 9+ understands the v3 hand-over and must see DroidDeck's private release key as current. if (Build.VERSION.SDK_INT >= 28) { val signing = info.signingInfo ?: throw IOException(context.getString(R.string.selfinst_no_signing)) val digests = signing.apkContentsSigners.orEmpty().map { cert -> @@ -122,13 +111,11 @@ object SelfInstaller { } } - /** Writes [apk] into an install session and commits it; the answer comes as an [ACTION_STATUS] broadcast. */ fun install(context: Context, apk: File) { val installer = context.packageManager.packageInstaller val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply { setAppPackageName(context.packageName) setSize(apk.length()) - // Skips Android's prompt where it allows that (the app installed its current build itself). if (Build.VERSION.SDK_INT >= 31) setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED) } val id = installer.createSession(params) @@ -145,7 +132,6 @@ object SelfInstaller { } } - /** What went wrong, in words for the user, from PackageInstaller's status and message. */ fun describeFailure(context: Context, status: Int, message: String?): String = when { message?.contains("VERSION_DOWNGRADE") == true -> context.getString(R.string.upd_fail_downgrade) status == PackageInstaller.STATUS_FAILURE_CONFLICT || message?.contains("UPDATE_INCOMPATIBLE") == true -> diff --git a/app/src/main/java/com/droiddeck/launcher/wayland/CompositorHost.kt b/app/src/main/java/com/droiddeck/launcher/wayland/CompositorHost.kt index 797ae8469..9df4d4dfd 100644 --- a/app/src/main/java/com/droiddeck/launcher/wayland/CompositorHost.kt +++ b/app/src/main/java/com/droiddeck/launcher/wayland/CompositorHost.kt @@ -4,12 +4,6 @@ import android.view.Choreographer import android.view.Surface import com.droiddeck.launcher.session.SessionState -/** - * The compositor is a process-wide thing, not an activity's: it is started once, keeps running - * while the session does, and merely swaps the Surface it presents into as the activity comes and - * goes. An activity that started it again on re-creation would be starting a second compositor on - * a socket the first one owns. - */ object CompositorHost { @Volatile private var started = false @@ -22,7 +16,6 @@ object CompositorHost { } } - /** True when this call was the one that started the compositor. */ @Synchronized fun startOrAttach( surface: Surface, @@ -36,10 +29,6 @@ object CompositorHost { fpsLimit: Int, ): Boolean { pace(refreshHz, fpsLimit) - // The compositor outlives sessions, and the next one may be on the other screen of a dual-screen - // device: gamescope reads the size from the toplevel configure and the wl_output mode when it - // connects, and kept the first session's (the Thor's 1920x1080 top screen letterboxed the bottom - // one's session to 16:9). A running session keeps the size it was started at. if (!SessionState.running) WaylandCompositor.nativeSetOutputSize(outputWidth, outputHeight) if (started) { attached = surface @@ -57,34 +46,14 @@ object CompositorHost { return true } - /** - * The session's frame cap, on every attach: the compositor outlives sessions, and the next one - * may have a different cap. The release pacer hands a game one buffer back per capped frame - * (per refresh without a cap), and the zero-copy layer votes for the same cadence. Left at 0 - * the vote is inferred from the rate already being achieved, and on a phone whose vendor picks - * clocks from that a session that has been slowed reads as one that wants to be. (WinNative, - * WaylandSession.) - */ private fun pace(refreshHz: Float, fpsLimit: Int) { WaylandCompositor.nativeSetFpsLimit(fpsLimit) WaylandCompositor.nativeSetLayerFrameRate(if (fpsLimit > 0) fpsLimit.toFloat() else refreshHz) } - /** - * The Surface changed size under the compositor (a foldable opening or closing). Android does - * not always recreate the Surface for that, and a swapchain built for the old size keeps - * presenting: the system then scales those buffers onto the new window, which on a Fold showed - * a 16:9 picture stretched to the square panel. Rebinding the window rebuilds the swapchain at - * the new size, and the scale mode letterboxes as it should. - */ @Synchronized fun resize(surface: Surface, rearm: () -> Unit) { if (!started) return - // Frame generation does not live through a resize: its ring, its effects chain and its - // multi-present pacing were all built against the old extent, and on a Fold the picture - // came back stretched with an engine on and right with it off. Disarmed, the window is - // rebound and the swapchain rebuilt; the caller re-arms once that has settled, and every - // engine-side object is made again for the new size. WaylandCompositor.nativeSetFrameGenArmed(false, 0, 0) WaylandCompositor.nativeLog("screen", "surface resized: rebinding the window, frame generation re-armed after") WaylandCompositor.nativeSetSurface(null) @@ -93,12 +62,6 @@ object CompositorHost { android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(rearm, 600) } - /** - * The window the guest presents through has changed (the Steam menu opening or closing over a - * fullscreen game swaps gamescope between forwarding the game's own buffers and its composited - * output). The engine's ring and history straddle that switch and come back wrong-sized - a - * game shown as a strip in a corner - so it is disarmed and re-armed against the new window. - */ fun rearmFrameGen(rearm: () -> Unit) { if (!started) return WaylandCompositor.nativeSetFrameGenArmed(false, 0, 0) @@ -106,19 +69,8 @@ object CompositorHost { android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(rearm, 600) } - /** - * The activity is going away. The compositor keeps running with nothing to present into - - * the guest carries on, and its next frames land on the Surface the next activity brings. - */ - /** The Surface the compositor presents on; only its own owner may take it away. */ private var attached: Surface? = null - /** - * Only for the Surface that is attached. When one session hands over to the next (the - * desktop's Steam launchers), the new activity attaches its Surface before the old one is - * destroyed - and the old one's surfaceDestroyed then arrived here and took the new Surface - * away: gamescope's frames came in by the thousand and nothing reached the screen. - */ @Synchronized fun detach(surface: Surface?) { if (!started) return @@ -128,7 +80,6 @@ object CompositorHost { WaylandCompositor.nativeSetSurface(null) } - /** Frames are only worth drawing while a Surface is attached. */ private fun resumeVsync() { if (vsyncRunning) return vsyncRunning = true @@ -139,10 +90,6 @@ object CompositorHost { vsyncRunning = false } - /** - * A new session behind a compositor that has already presented: the first-frame notice is - * one-shot in native code, so it is re-armed here or the loading panel would never leave. - */ fun newSession() { if (started) WaylandCompositor.nativeResetFirstFrame() } diff --git a/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt b/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt index 714746d20..605a1579d 100644 --- a/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt +++ b/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt @@ -6,12 +6,6 @@ import android.view.Display import android.view.WindowManager import com.droiddeck.launcher.R -/** - * What the panel says about HDR, read from android.view.Display: the gate the compositor's HDR10 - * output sits behind (droiddeck_color.h). A property of the display, not the device - and the - * settings screen greys the HDR switch out with the reason when it is not there, rather than - * offering a switch that a session would then silently ignore. - */ class HdrProbe( val hdr10: Boolean, val formats: String, @@ -22,7 +16,6 @@ class HdrProbe( val ratio: Float, val displayId: Int, val name: String, - /** Why HDR cannot be offered, or null when it can. */ val reason: String?, ) diff --git a/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java b/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java index 14016b29f..97cfcf3c4 100644 --- a/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java +++ b/app/src/main/java/com/droiddeck/launcher/wayland/WaylandCompositor.java @@ -2,15 +2,6 @@ package com.droiddeck.launcher.wayland; import android.view.Surface; -/** - * Embedded Wayland compositor (experimental parallel display runtime). - * - * Brings up a libwayland-server compositor in-process so games launched through - * Wine's winewayland.drv can present into it (companion to the winewayland Proton - * build). Committed frames (dmabufs from winewayland's Vulkan WSI) are composited - * onto the given Surface via the native Vulkan present backend. Not on the default - * X11 path. - */ public final class WaylandCompositor { static { System.loadLibrary("droiddeckwayland"); @@ -20,35 +11,21 @@ public final class WaylandCompositor { private static volatile Runnable firstFrameListener; - /** Register a callback fired once, when the compositor presents the first client - * frame to the output Surface. Used to dismiss the launch overlay in wayland mode - * (there is no XServer window-content hook). Runs on the compositor thread - the - * listener must marshal to the UI thread itself. */ public static void setFirstFrameListener(Runnable r) { firstFrameListener = r; } public static synchronized void clearFirstFrameListener(Runnable r) { if (firstFrameListener == r) firstFrameListener = null; } - /** Invoked from native (droiddeck_on_first_frame) on the first present. */ @SuppressWarnings("unused") static void onFirstFramePresented() { Runnable r = firstFrameListener; if (r != null) r.run(); } - /** The in-game performance HUD's feed in wayland mode (X11 binds the HUD to the window - * carrying _MESA_DRV and counts X presents; there is no X server here). */ public interface GameListener { - /** A window started presenting GPU frames ({@code window} describes it; {@code gpuName} is the - * compositor's GPU), or {@code window == null} when that window closed. Compositor thread. */ void onGameSurface(String window, String gpuName); - /** One GPU frame from that window. Compositor thread - keep it cheap. */ void onGameFrame(); - /** The program behind that window, right after {@link #onGameSurface}: its Linux pid (the - * Wayland client's credentials) and executable name ({@code ""} when unknown). The app arms its - * launch-time CPU affinity on it - on X11 that comes from window events, which a Wayland session - * has none of. Compositor thread - marshal to the UI thread. */ default void onGameProgram(int pid, String program) {} } @@ -60,7 +37,6 @@ public final class WaylandCompositor { if (gameListener == l) gameListener = null; } - /** Invoked from native (droiddeck_on_game_surface). */ @SuppressWarnings("unused") static void onGameSurface(String window, String gpuName) { GameListener l = gameListener; @@ -70,17 +46,12 @@ public final class WaylandCompositor { private static volatile long lastFrameNanos; private static volatile long frameIntervalNanos; - /** - * How long frames are taking to arrive right now, in ms, or -1 before the first: the smoothed - * interval, or the time since the last frame when that is longer (a session that has stalled). - */ public static long recentFrameIntervalMs() { long last = lastFrameNanos; if (last == 0) return -1; return Math.max(frameIntervalNanos, System.nanoTime() - last) / 1_000_000; } - /** Invoked from native (droiddeck_on_game_frame) for every frame of the HUD's window. */ @SuppressWarnings("unused") static void onGameFrame() { long now = System.nanoTime(); @@ -95,19 +66,13 @@ public final class WaylandCompositor { if (l != null) l.onGameFrame(); } - /** Invoked from native (droiddeck_on_game_program) once per game window that starts presenting. */ @SuppressWarnings("unused") static void onGameProgram(int pid, String program) { GameListener l = gameListener; if (l != null) l.onGameProgram(pid, program != null ? program : ""); } - /** Pointer lock (zwp_pointer_constraints_v1) state, for the app's input path. */ public interface PointerLockListener { - /** A program locked the pointer ({@code locked}): the app must feed the compositor deltas - * (scene input type 6) instead of absolute positions. When the lock ends, {@code x,y} is - * where the pointer now is (scene = virtual-desktop coordinates) for the app to re-sync - * its own pointer to. Compositor thread. */ void onPointerLock(boolean locked, int x, int y); } @@ -115,21 +80,17 @@ public final class WaylandCompositor { public static void setPointerLockListener(PointerLockListener l) { pointerLockListener = l; } - /** Unhooks {@code l} only if it is still the listener, so a finishing activity never unhooks its successor's. */ public static void clearPointerLockListener(PointerLockListener l) { if (pointerLockListener == l) pointerLockListener = null; } - /** Invoked from native (droiddeck_on_pointer_lock). */ @SuppressWarnings("unused") static void onPointerLock(boolean locked, int x, int y) { PointerLockListener l = pointerLockListener; if (l != null) l.onPointerLock(locked, x, y); } - /** Clipboard text a program in the guest copied (wl_data_device / zwlr_data_control). */ public interface ClipboardListener { - /** {@code text} is what the guest put on its clipboard. Compositor thread. */ void onGuestClipboardText(String text); } @@ -139,23 +100,17 @@ public final class WaylandCompositor { public static boolean isClipboardListener(ClipboardListener l) { return clipboardListener == l; } - /** A replaced activity must not unhook the clipboard listener of its successor. */ public static void clearClipboardListener(ClipboardListener l) { if (clipboardListener == l) clipboardListener = null; } - /** Invoked from native (droiddeck_on_clipboard_text) with UTF-8 bytes. */ @SuppressWarnings("unused") static void onClipboardText(byte[] utf8) { ClipboardListener l = clipboardListener; if (l != null && utf8 != null) l.onGuestClipboardText(new String(utf8, java.nio.charset.StandardCharsets.UTF_8)); } - /** Text input (zwp_text_input_v3) state: a program accepts IME text, or stopped. */ public interface TextInputListener { - /** {@code enabled}: {@code program} (its exe name) accepts IME text; {@code x,y,w,h} is its caret - * rectangle in scene (virtual desktop) pixels, all 0 until the program positions one. Compositor - * thread. */ void onTextInput(boolean enabled, String program, int x, int y, int w, int h); } @@ -163,7 +118,6 @@ public final class WaylandCompositor { public static void setTextInputListener(TextInputListener l) { textInputListener = l; } - /** Invoked from native (droiddeck_on_text_input). */ @SuppressWarnings("unused") static void onTextInput(boolean enabled, String program, int x, int y, int w, int h) { TextInputListener l = textInputListener; @@ -174,16 +128,12 @@ public final class WaylandCompositor { return s == null ? new byte[0] : s.getBytes(java.nio.charset.StandardCharsets.UTF_8); } - /** Android's clipboard text becomes the guest's selection ({@code null}/empty clears it). Any thread. */ public static void setClipboardText(String text) { nativeSetClipboardText(utf8(text)); } - /** Commit soft-keyboard text to the program accepting text input. Any thread. */ public static void textInputCommit(String text) { if (text != null && !text.isEmpty()) nativeTextInputCommit(utf8(text)); } - /** Composing (pre-edit) text with the caret at character index {@code cursor} (-1 = end); empty clears. */ public static void textInputPreedit(String text, int cursor) { nativeTextInputPreedit(utf8(text), cursor, cursor); } - /** The IME deleted {@code before} characters before and {@code after} after the caret. */ public static void textInputDelete(int before, int after) { nativeTextInputDelete(before, after); } private static native void nativeSetClipboardText(byte[] utf8); @@ -191,104 +141,46 @@ public final class WaylandCompositor { private static native void nativeTextInputPreedit(byte[] utf8, int cursorBegin, int cursorEnd); private static native void nativeTextInputDelete(int before, int after); - /** Start the compositor headless (no output window) - bring-up tests only. */ public static native void nativeStart(String xdgRuntimeDir); - /** Start the compositor rendering to {@code surface}. XDG_RUNTIME_DIR = an - * app-writable dir for the wayland socket (e.g. context.getFilesDir()). - * driverPath/libraryName/nativeLibDir select the Turnip driver via adrenotools - * (all null -> system libvulkan, which can't do dmabuf import). */ public static native void nativeStartWithSurface(Surface surface, String xdgRuntimeDir, String driverPath, String libraryName, String nativeLibDir); - /** Replace/clear the output window when the SurfaceView is (re)created/destroyed. */ public static native void nativeSetSurface(Surface surface); - /** Inject a pointer event into the compositor (from the SurfaceView touch listener). - * action: 0=down, 1=move, 2=up. x/y in compositor output space (0..1919, 0..1079). */ public static native void nativeSendPointer(int action, int x, int y); public static native void nativeSendTouch(int action, int pointerId, int x, int y); - /** - * The client's pointer image, straight from wl_pointer.set_cursor (labwc's resize arrows, a text - * field's I-beam). out = [serial, hidden, w, h, hotspotX, hotspotY, ARGB8888 pixels...]; returns - * the number of ints written, or 0 if {@code out} is too small. Serial 0 = the client has not - * set one yet; [1] = 1 means it asked for NO pointer. - */ public static native int nativeCursorSnapshot(int[] out); - /** The serial {@link #nativeCursorSnapshot} would report, without copying the image. */ public static native int nativeCursorSerial(); - /** Enough for the header plus a 256x256 cursor, the largest the compositor snapshots. */ public static final int CURSOR_BUF_INTS = 6 + 256 * 256; - /** Inject a key event. evdev = Linux input keycode (KEY_A=30…); state: 1=down, 0=up. */ public static native void nativeSendKey(int evdev, int state); - /** One screen refresh (Choreographer frame callback). The compositor draws the newest state - * once per tick, so games run unthrottled and the screen shows their latest frame. */ public static native void nativeVsync(long frameTimeNanos); - /** The compositor thread's Linux tid, or 0 before it has started. */ public static native int nativeCompositorTid(); - /** Shortcut launches: don't draw explorer's windows (desktop, taskbar, Start menu), matching the - * X11 renderer's unviewable "explorer.exe". Set before the compositor starts. */ public static native void nativeSetHideShell(boolean hide); - /** The panel's refresh rate in Hz, advertised on the Wayland output so Wine's display modes - * carry the real rate (games that insist on their saved 144 Hz mode find it, as on X11). - * Set before the compositor starts. */ public static native void nativeSetOutputRefreshRate(float hz); - /** Show a single fullscreen window on its own Android layer (SurfaceControl) instead of blitting - * it into the compositor's swapchain. DROIDDECK_WAYLAND_ZERO_COPY=1 in the container's environment - * variables is the launch default; see waylandcomp/ZERO_COPY_SPIKE.md. - * - * Live and thread-safe: before the compositor starts this is the initial state, and afterwards - * the call is marshalled onto the compositor thread, which flips the state, tells every bound - * game over banner_ahb_v1.mode to rebuild its swapchain for it, and redraws. The switch is one - * frame's worth of latency; the picture never goes black, because the old swapchain's buffers - * keep being shown until the game has replaced them. */ public static native void nativeSetZeroCopy(boolean on); - /** Zero-copy frames the compositor presented in its LAST completed 10 s stats window (the - * "| N zero-copy frames" figure of its session-log stats line); 0 while zero-copy is off or - * before the first window closes. Read-only, any thread - the in-game drawer polls it. */ public static native int nativeZeroCopyFrames(); - /** Milliseconds since the compositor last put a game frame on the display layer without a copy; - * -1 if it never has this session. Unlike nativeZeroCopyFrames this updates on every such frame - * rather than once per 10 s window, so the drawer can tell "switching..." from "running" within - * a frame or two of the live toggle. Read-only, any thread. */ public static native int nativeZeroCopyLastFrameAgeMs(); - /** Compressed (UBWC) game buffers: the compositor advertises DRM_FORMAT_MOD_QCOM_COMPRESSED next to - * LINEAR on zwp_linux_dmabuf_v1 for every format its driver can import that way, so the game's - * Turnip allocates compressed swapchain images instead of resolving every frame to a linear copy. - * Default on; DROIDDECK_WAYLAND_UBWC=0 in the container's environment variables turns it off (A/B). - * Set before the compositor starts. */ public static native void nativeSetUbwc(boolean on); - /** Debug: advertise no DRM device (main device 0:0) in the dma-buf feedback, as a phone that - * exposes no /dev/dri node to apps does. DROIDDECK_WAYLAND_NO_RENDER_NODE=1 in the container's or - * shortcut's environment variables. Set before the compositor starts. */ public static native void nativeSetNoRenderNode(boolean on); - /** VRR / refresh-rate matching: mirror the panel refresh-rate vote the activity puts on the - * compositor's SurfaceView onto the game's own SurfaceControl layer. Under zero-copy the game's - * frames go straight onto that layer and never reach the app's surface, so the surface vote alone - * does not tell SurfaceFlinger the game's cadence. Same rate and compatibility as - * {@code Surface.setFrameRate}; {@code 0} clears the vote. Callable any time from any thread; the - * compositor applies it on its next layer transaction and writes one `layer` line per change to - * the session log. A no-op on Android versions without {@code ASurfaceTransaction_setFrameRate}. */ public static native void nativeSetLayerFrameRate(float hz); - /** Where the compositor was told to write its log, for the session collector; null if it was - * left to choose. Set before the compositor starts - it opens the file once, at start. */ private static volatile java.io.File sessionLogFile; public static void setSessionLogFile(java.io.File file) { @@ -299,233 +191,116 @@ public final class WaylandCompositor { return sessionLogFile; } - /** Write one line into the compositor's session log (Download/Wayland-logs) under the "display" - * area, from Java. Used for facts the platform knows and the native side does not - the panel's - * HDR capability, which lives behind android.view.Display. Safe before the compositor thread is - * up (the line then only reaches logcat) and safe after it has gone. */ public static native void nativeLogDisplay(String message); - /** The same under the "perf" area (e.g. the CPU cores Prefer Big Cores picked). Same safety. */ public static native void nativeLogPerf(String message); - /** The container's screen size, advertised as the Wayland output's mode so Wine's display-mode - * list stops at the desktop size, as the X server's does on X11. Set before the compositor starts. */ public static native void nativeSetOutputSize(int width, int height); - /** A new output size for a running compositor ("Follow screen": a foldable opening or closing). - * The compositor thread re-sends the wl_output mode and an xdg_toplevel configure to every - * client, and gamescope / the desktop's KWin resize their output to it. Any thread. */ public static native void nativeResizeOutput(int width, int height); - /** Re-arms the one-shot first-frame notice, for a new session under a compositor that has - * already presented a previous one. */ public static native void nativeResetFirstFrame(); - // ── HDR10 output, round 1 (waylandcomp/src/droiddeck_color.h, wl_color_mgmt.c) ───────────────── - // Opt-in: DROIDDECK_WAYLAND_HDR=1 in the container's or shortcut's env vars. The compositor offers - // games HDR10 (wp_color_manager_v1 + 10-bit buffers, frames tagged BT2020_PQ on the game's own - // display layer) only when the game's display lists HDR10 as well; otherwise nothing changes and - // the session log says why. Not the drawer's "HDR" effect, which is an SDR bloom/contrast filter. - /** HDR_MODE_OFF / HDR_MODE_ON (DROIDDECK_WAYLAND_HDR=1) / HDR_MODE_FORCE (=force: skip the display - * check, for testing the negotiation on an SDR panel). */ public static final int HDR_MODE_OFF = 0, HDR_MODE_ON = 1, HDR_MODE_FORCE = 2; - /** The opt-in: mode, where it came from ("container env" / "shortcut env"), whether DXVK_HDR=1 is - * in the game's environment, and whether the app turned zero-copy on for this session because HDR - * needs it. Set before the compositor starts. */ public static native void nativeSetHdrRequest(int mode, String source, boolean dxvkHdr, boolean zeroCopyForced); - /** The game's display as {@code android.view.Display} reports it. Before the compositor starts (it - * feeds the gate) and again whenever it changes (logged; the gate is decided once per session). */ public static native void nativeSetHdrDisplay(int displayId, String name, String formats, boolean hdr10, float maxLuminance, float maxAverageLuminance, float minLuminance, boolean hdrSdrRatioAvailable, float hdrSdrRatio, int apiLevel); - /** One {@code Display.getHdrSdrRatio()} reading ({@code < 0} = not available); {@code listener} = it - * came from the display's ratio listener rather than the periodic sampler. Any thread. */ public static native void nativeHdrSdrRatioSample(float ratio, boolean listener); - /** Milliseconds since an HDR frame last reached a display layer; -1 = none this session. Any thread. */ public static native int nativeHdrLastFrameAgeMs(); - /** -1 = the compositor has not decided the HDR gate yet, 0 = closed, 1 = open. Any thread. */ public static native int nativeHdrGateState(); - /** The session is ending: the compositor writes its "HDR on screen: …" summary line. Once. */ public static native void nativeHdrSessionEnd(); - /** True while HDR frames are really on screen: frames tagged BT2020_PQ reached the display in the - * last 1.5 s and, where the display reports an HDR/SDR ratio, it is above 1 (the HUD's badge). */ public static native boolean nativeHdrOnScreen(); - /** One "color" line in the session log, from Java (the HDR environment, the DXVK warning). */ public static native void nativeLogColor(String message); - /** One line in the session log under {@code area} (the log's 9-character column: "gpu", "nvapi"), - * from Java: launch facts the app decides (the GPU name spoof, the Unreal Engine HDR mode). Like - * {@link #nativeLogDisplay}, safe before the session file exists (the line is written when it - * opens) and after it has gone. */ public static native void nativeLog(String area, String message); - /** Nits at which SDR content is placed when the compositor composes an HDR picture (a window over - * an HDR game, the desktop around a windowed one). Default 203 (BT.2408). Before the start. */ public static native void nativeSetHdrSdrWhite(float nits); - /** The in-game drawer's HDR output switch (only meaningful while the HDR gate is open; per session, - * starts on). On = the game's HDR frames go to the display as HDR; off = the same frames are shown - * tone-mapped to SDR. Live, any thread (queued to the compositor, which logs the flip); the game is - * told nothing - DXVK_HDR and the colour-manager offer were decided at launch. */ public static native void nativeSetHdrOutput(boolean on); - /** The HDR output switch's current state (any thread). */ public static native boolean nativeHdrOutput(); - /** True while an HDR game's frames are being shown tone-mapped to SDR (the last one under 1.5 s ago). */ public static native boolean nativeHdrToneMappedOnScreen(); - /** 0 = no HDR frames on screen (or not confirmed yet), 1 = HDR frames on screen with HDR headroom, - * 2 = HDR frames on screen but the display has given them no headroom (HDR/SDR ratio 1.00) for 5 s - * or more - the brightness slider at maximum, or a screen recording (Android turns HDR headroom off - * while the screen is recorded). The HUD badge and the drawer's HDR row. */ public static native int nativeHdrState(); - /** Device evidence beside the HDR/SDR headroom, HDR sessions only (any thread): PowerManager thermal - * status (0..6, -1 unknown) and getThermalHeadroom(10) (NaN / negative = not available), and - * Settings.System SCREEN_BRIGHTNESS (0..255, -1 unknown) + SCREEN_BRIGHTNESS_MODE (1 auto, 0 manual, - * -1 unknown). The compositor logs changes of status and brightness and puts the values on every - * no-headroom line, the 10 s HDR line and the verdict. */ public static native void nativeHdrEnvSample(int thermalStatus, float thermalHeadroom, int brightness, int brightnessMode); - /** Display.getHighestHdrSdrRatio() (Android 16+), <= 0 = not reported: the display's own ceiling, capped - * into the headroom request and logged in the verdict. Any thread. */ public static native void nativeSetHdrHighestRatio(float ratio); - /** The HDR headroom the SCREEN surface should ask for while HDR frames go through the HDR10 swapchain - * (frame generation), 0 = none; the app applies it (SurfaceView.setDesiredHdrHeadroom, API 35). */ public static native float nativeHdrScreenHeadroom(); - /** Why that value ("content peak 1207 nits (max CLL) / SDR 203 ..."), for the log line. */ public static native String nativeHdrScreenHeadroomWhy(); - /** What the app asked for on the screen surface: > 0 the ratio, 0 cleared, -1 not possible (< Android 15). */ public static native void nativeHdrNoteHeadroomRequest(float ratio); - /** Fullscreen mode ({@code Container.FULLSCREEN_OFF/FIT/STRETCH/FILL/INTEGER}) and screen alignment - * ({@code Container.ALIGN_CENTER/TOP/BOTTOM}): how the compositor fits the desktop onto the screen, - * with the same arithmetic as {@code ViewTransformation} (which maps touch input), so the picture and - * the pointer agree. OFF and FIT both letterbox. Callable any time; the next frame uses it. */ public static native void nativeSetScaleMode(int fullscreenMode, int screenAlignment); - // ---- Screen effects: the X11 Vulkan renderer's post chain, run by the compositor between the - // composited scene and the output blit (waylandcomp/src/effects_chain.c). Same modes, ranges and - // pass order as VulkanRenderer's setters, so a saved preset looks the same on both backends. - // Callable any time from any thread; the compositor applies them on its next frame and writes - // one `effects` line to the session log per change. While any effect is on, a zero-copy - // (layer-mode) game is presented through the compositor pass instead. - /** Scaling mode: 0=None 1=Linear 2=Nearest 3=SGSR 4=FSR 5=FSR Fit 6=Sharpen 7=NIS 8=SGSR HQ. - * Resizes the scene to its mapped output size (there is no render scale on Wayland). */ public static native void nativeSetUpscaler(int mode); - /** The upscaler's own sharpness slider 0..100 (RCAS lobe scale, SGSR edge 0.5..4.5, NIS). */ public static native void nativeSetUpscaleSharpness(int sharpness); - /** AMD CAS sharpen toggle + level 0..100 (0 = the pass is off). */ public static native void nativeSetCas(boolean enabled, int sharpness); - /** Fake-HDR toggle. */ public static native void nativeSetHdr(boolean enabled); - /** Terminal debanding toggle + strength 0..200 (100 = 1 LSB). */ public static native void nativeSetDeband(boolean enabled, int strength); - /** Colour grade in the drawer's slider units (brightness/contrast -100..100, gamma 0.5..3.0, - * saturation 0..200 percent; 0/0/1/100 = neutral, pass off) plus the FXAA / Toon / CRT / NTSC - * toggles - the same signature as {@code VulkanRenderer.setScreenEffects}. */ public static native void nativeSetScreenEffects(float brightness, float contrast, float gamma, float saturation, boolean fxaa, boolean toon, boolean crt, boolean ntsc); - /** The Look the controls currently match ({@code null} = Custom); only named in the log line. */ public static native void nativeSetLookName(String name); - /** Cardboard mode (waylandcomp/src/cardboard.h): two lens-distorted eye views side by side. - * {@code source} 0 = the frame as a virtual screen, 1 = the frame is already a side-by-side - * stereo pair. Distances in metres, field of view in degrees. */ public static native void nativeSetCardboard(boolean enabled, int source, float ipdM, float screenDistM, float screenWidthM, float k1, float k2, float fovDeg, float lensInset, float chroma); - /** The head pose for Cardboard mode: head -> world rotation, row-major 3x3 (any thread). {@code live} - * = a sensor produced it. */ public static native void nativeSetCardboardPose(float[] rotation, boolean live); - /** Where the head pose is shared with the guest (waylandcomp/src/headpose_shm.h); set once. */ public static native void nativeSetCardboardPoseFile(String path); - /** The in-game FPS limiter: frames per second, 0 = unlimited. Paces when replaced buffers go - * back to the game, like the X11 IdleNotify pacer, so the game itself slows to the cap. */ public static native void nativeSetFpsLimit(int fps); - /** KGSL power control off (true: the GPU held at its top clock) or back on. Device-wide and - * outlives the process; see {@code GpuClockPin}. A no-op without /dev/kgsl-3d0. */ public static native void nativeSetGpuTurbo(boolean on); - /** setpriority() on one of our own processes or threads, only ever raising it (no cgroup move, - * unlike android.os.Process.setThreadPriority). Returns the nice it is left at; 100 = unknown. */ public static native int nativeRaisePriority(int tid, int nice); - /** Inject the app's X-server input in scene (virtual desktop) pixels. type 2 = move to a,b; - * 3 = evdev button a (BTN_LEFT=0x110…) pressed (b=1) or released (b=0); 4 = a wheel steps, - * negative = up. */ public static native void nativeSendSceneInput(int type, int a, int b); - // ── Frame generation (waylandcomp/src/framegen_bridge.c) ───────────────────────────────── - // LSFG Native and Win-FG Native run inside the compositor's Turnip device, the same engines - // the X11 VulkanRenderer hosts. Every setter only stores a value; the compositor thread applies - // it on its next frame, so they are safe from any thread and before the compositor starts. - // bionic-fg (the guest-side win-fg layer) is X11-only and has no Wayland counterpart. public static final int FG_ENGINE_LSFG = 0, FG_ENGINE_WINFG = 1; - /** Which native engine generates: {@link #FG_ENGINE_LSFG} or {@link #FG_ENGINE_WINFG}. */ public static native void nativeSetFrameGenEngine(int kind); - /** Arm (multiplier 2..4: one real frame plus multiplier-1 interpolated ones per game frame) or - * disarm. A targetFps above 0 makes LSFG adaptive instead: it generates 0..3 frames per game - * frame, whatever reaches that rate. Generated frames are presented ahead of the real frame - * on consecutive vblanks. */ public static native void nativeSetFrameGenArmed(boolean armed, int multiplier, int targetFps); - /** LSFG Native: the SPIR-V cache built from the user's Lossless.dll ({@code Lossless.cacheFile}). */ public static native void nativeSetLsfgCachePath(String path); - /** Flow scale (0.25-1.0) and the panel's real refresh rate (the pacer never generates above it). */ public static native void nativeSetFrameGenTuning(float flowScale, float refreshHz); - /** Win-FG Native only: interpolation model (3/4) and performance preset (0..2). */ public static native void nativeSetWinFgTuning(int model, int perfPreset); - /** Same codes as {@code VulkanRenderer.getFrameGenProblem()}: -1 not known yet (the compositor's - * device is not up), 0 fine, 1 the driver lacks what the selected engine needs - * ({@link #nativeFrameGenCapsReason()} says what), 2 the engine failed to start. */ public static native int nativeFrameGenProblem(); public static native String nativeFrameGenCapsReason(); - /** Same shape as {@code VulkanRenderer.getFrameGenStats()}: {generations trusted, generations - * planned, real fps, presented fps (generated frames included), thermal (-1 = none), - * GPU ms per generated frame (-1 = unknown)}. */ public static native float[] nativeFrameGenStats(); - /** Relative pointer motion by dx,dy scene pixels (the Relative Mouse / captured-mouse path): - * while a program holds a pointer lock this is what it receives as relative_motion; otherwise - * the compositor moves its pointer by the delta. */ public static void sendPointerDelta(int dx, int dy) { if (dx == 0 && dy == 0) return; nativeSendSceneInput(6, dx * 256, dy * 256); } - /** As {@link #sendPointerDelta(int, int)}, keeping a captured mouse's fractions of a pixel. */ public static void sendPointerDelta(float dx, float dy) { int x = Math.round(dx * 256f), y = Math.round(dy * 256f); if (x == 0 && y == 0) return; diff --git a/app/src/main/res/anim/session_hold.xml b/app/src/main/res/anim/session_hold.xml index 60e4638f6..8a828450e 100644 --- a/app/src/main/res/anim/session_hold.xml +++ b/app/src/main/res/anim/session_hold.xml @@ -1,3 +1,2 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- The front end stays put while the session slides over or off it. --> <alpha xmlns:android="http://schemas.android.com/apk/res/android" android:fromAlpha="1" android:toAlpha="1" android:duration="380" /> diff --git a/app/src/main/res/anim/session_rise.xml b/app/src/main/res/anim/session_rise.xml index 7aa0b7074..57f4afd23 100644 --- a/app/src/main/res/anim/session_rise.xml +++ b/app/src/main/res/anim/session_rise.xml @@ -1,5 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- The session surface rises over the front end. --> <set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:interpolator/decelerate_cubic"> <translate android:fromYDelta="100%p" android:toYDelta="0" android:duration="380" /> <alpha android:fromAlpha="0.6" android:toAlpha="1" android:duration="380" /> diff --git a/app/src/main/res/anim/session_sink.xml b/app/src/main/res/anim/session_sink.xml index 6854de13d..e5f80c5aa 100644 --- a/app/src/main/res/anim/session_sink.xml +++ b/app/src/main/res/anim/session_sink.xml @@ -1,5 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Leaving a session: the surface sinks back down to reveal the front end. --> <set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:interpolator/accelerate_cubic"> <translate android:fromYDelta="0" android:toYDelta="100%p" android:duration="320" /> </set> diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5f2842f42..224591154 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1153,16 +1153,12 @@ <string name="game_file_exported">Archivos de juegos guardados en %1$s</string> <string name="game_frontend_files">Archivos para frontends</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">Espera a que termine la operación del runtime</string> <string name="main_wait_proton_install">Espera a que termine la instalación de la herramienta de compatibilidad</string> <string name="main_wait_desktop_install">Espera a que termine la instalación de la app de escritorio</string> <string name="main_wait_install_game">Espera a que termine la instalación antes de iniciar un juego</string> <string name="main_saves_close_session">Cierra primero la sesión de Steam para que el juego no guarde encima de lo importado</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">Importando en %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="one">Se importó %1$d archivo del %2$s en %3$s</item> <item quantity="many">Se importaron %1$d archivos del %2$s en %3$s</item> @@ -1174,17 +1170,13 @@ <item quantity="other">Se importaron %1$d archivos del %2$s en %3$s. Copia de las partidas anteriores en Download/DroidDeck/Saves/backups</item> </plurals> <string name="main_saves_pick_zip">Elige un zip de partidas guardadas para %1$s</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">Exportando %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="one">Se exportó %1$d archivo como %2$s: %3$s</item> <item quantity="many">Se exportaron %1$d archivos como %2$s: %3$s</item> <item quantity="other">Se exportaron %1$d archivos como %2$s: %3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">Elige dónde guardar %1$s (%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">%1$s falló: %2$s</string> <string name="main_no_logs_yet">Aún no hay registros de sesión: inicia una sesión primero.</string> <string name="main_no_logs_to_clear">No hay registros de sesión que borrar.</string> @@ -1199,68 +1191,55 @@ <string name="main_adb_copied">Comando ADB copiado</string> <string name="main_pick_roms">Elige la carpeta de ROMs</string> <string name="main_non_adreno_title">La GPU no es Adreno</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">Turnip es compatible con GPU Adreno. En %1$s, Steam puede mostrar una pantalla negra. Descarga: %2$.0f MB.</string> <string name="main_install_anyway">Instalar de todos modos</string> <string name="main_remove_runtime_title">Eliminar el runtime de Linux</string> <string name="main_remove_runtime_text">Esto elimina el runtime, el cliente de Steam que contiene y todos los juegos instalados en él.</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">Aplicado: %1$s</string> <string name="android_app_open_failed_primary">No se pudo abrir %1$s en la pantalla principal</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">No se pudo abrir %1$s en %2$s</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">No se pudo abrir %1$s en la pantalla %2$d</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">Importando</string> <string name="comp_busy_swapping">Reemplazando</string> <string name="comp_busy_restoring">Restaurando</string> <string name="comp_busy_cancelling">Cancelando</string> <string name="comp_busy_deleting">Eliminando</string> <string name="comp_cannot_read">no se puede leer el archivo</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">%1$s importado</string> <string name="comp_swap_cancelled">Se canceló el reemplazo pendiente.</string> <string name="comp_pick_wcp">Elige un paquete de componentes (-linux .wcp)</string> <string name="comp_nightlies_unreachable">No se pudo acceder a las Nightlies</string> - <!-- %1$s is the package version --> <string name="comp_stored">%1$s guardado</string> <string name="comp_download_failed">La descarga falló: %1$s</string> <string name="drivers_pick_linux">Elige un controlador de runtime de Linux (zip -Linux o paquete Android + Linux)</string> <string name="drivers_pick_android">Elige un controlador de pantalla (zip de AdrenoTools o paquete Android + Linux)</string> <string name="drivers_pick_any">Elige un zip de controlador o un paquete Android + Linux</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s: %2$s</string> <string name="main_removing_named">Quitando %1$s…</string> <string name="main_forgetting_named">Olvidando %1$s…</string> <string name="main_pick_game_storage">Elige la carpeta de almacenamiento de juegos</string> <string name="main_pick_decky_zip">Elige un ZIP de plugin de Decky</string> <string name="main_storage_not_usable">No se puede usar: %1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">Programa</string> <string name="main_removing_runtime">Eliminando el runtime de Linux</string> <string name="main_settings_main_screen">No se pudieron abrir los Ajustes en la pantalla inferior; se abrieron en la pantalla principal.</string> <string name="main_open_dev_options">Abre Opciones de desarrollador en Ajustes</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">No se pudo abrir el ajuste \"Instalar apps desconocidas\" de Android</string> <string name="upd_couldnt_check">No se pudo comprobar: %1$s</string> <string name="upd_no_download">Esta versión no tiene descarga para esta copia de DroidDeck</string> <string name="upd_stop_first">Detén primero la sesión en ejecución y después actualiza</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">Descargando</string> <string name="upd_stage_installing">Instalando</string> <string name="upd_stage_confirm">Esperando tu confirmación</string> <string name="upd_failed">La actualización falló</string> <string name="upd_not_offered">Android no ofreció instalar la actualización</string> - <!-- Decky --> <string name="decky_install_failed">Falló la instalación de Decky: %1$s</string> <string name="decky_plugin_starting">Iniciando la importación del plugin…</string> <string name="decky_plugin_failed">Falló la importación del plugin: %1$s</string> <string name="decky_plugin_installed">Plugin instalado. Reinicia Steam para cargarlo.</string> - <!-- GPU drivers --> <string name="drivers_not_checked">Aún sin comprobar - toca actualizar para buscar controladores nuevos</string> <string name="drivers_bundle_sets_display">Android + Linux: define también el controlador de pantalla</string> <string name="drivers_bundle_sets_runtime">Android + Linux: define también el controlador de runtime</string> @@ -1268,16 +1247,13 @@ <string name="drivers_system_vulkan">Vulkan del sistema: no hay versión incluida para esta GPU</string> <string name="drivers_bundled">%1$s (incluido)</string> <string name="drivers_manual">Manual</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">Por ahora solo se ha publicado una de las mitades</string> <string name="drivers_pair_size">%1$.0f MB para ambos</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">No hay controladores que definir: %1$s</string> <string name="drivers_checking_latest">Buscando los controladores más recientes…</string> <string name="drivers_up_to_date_check_failed">Al día (no se pudo comprobar: %1$s)</string> <string name="drivers_recommended_unpublished">Los controladores recomendados no están publicados ahora mismo</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">Descargando %1$s %2$s…</string> <string name="drivers_updated_to">Actualizado a %1$s %2$s.</string> <string name="drivers_updated_to_restart">Actualizado a %1$s %2$s. El controlador de pantalla se aplica al reiniciar DroidDeck.</string> @@ -1292,9 +1268,7 @@ <string name="drivers_using_bundle_restart">Usando %1$s como controlador de runtime y de pantalla. El controlador de pantalla se aplica al reiniciar DroidDeck.</string> <string name="drivers_deleted_bundle">%1$s eliminado (ambos controladores)</string> <string name="drivers_deleted">%1$s eliminado</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">Últimas: %1$s · comprobado %2$s</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">Últimas: %1$s · sin acceso a %2$s · comprobado %3$s</string> <plurals name="drivers_ago_minutes"> <item quantity="one">hace %1$d min</item> @@ -1313,33 +1287,25 @@ </plurals> <string name="drivers_checking_sources">Comprobando Banners-Turnip, WinNative y DroidDeck…</string> <string name="drivers_check_failed">falló la comprobación</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">%1$s instalado - elígelo en cualquiera de las listas para definir ambos controladores</string> <string name="drivers_installed_pick">%1$s instalado - elígelo en el menú</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">Espera a que termine la eliminación del runtime</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">Juego</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">touchpad</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">directo</string> <string name="session_swap_failed">El reemplazo falló: %1$s</string> <string name="session_profile_save_failed">No se pudo guardar el perfil: %1$s</string> <string name="session_desktop_paused">El escritorio está en pausa</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$s está en pausa</string> <string name="session_program_paused">El programa está en pausa</string> <string name="session_starting_desktop">Iniciando el escritorio</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">Iniciando %1$s</string> <string name="session_starting_program">Iniciando el programa</string> <string name="session_no_logs">No hay registros de esta sesión.</string> <string name="session_logs_archive_failed">No se pudo crear el archivo de registros de la sesión.</string> <string name="session_logs_share_failed">No se pudieron compartir los registros de la sesión.</string> <string name="session_starting_session">Iniciando la sesión…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">descargando el runtime de Linux</string> <string name="session_runtime_downloading_mb">descargando el runtime de Linux · %1$d de %2$d MB</string> <string name="session_runtime_checking">comprobando el runtime de Linux</string> @@ -1354,29 +1320,22 @@ <string name="session_proton_unpacking">descomprimiendo Proton Experimental (ARM64)</string> <string name="session_runtime_catalog_unreachable">No se pudo acceder al catálogo del runtime. Revisa la conexión y vuelve a pulsar Jugar.</string> <string name="session_runtime_install_failed">El runtime de Linux no se instaló. Revisa el runtime en Configuración y vuelve a pulsar Jugar.</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">El escritorio no se instaló (%1$s). Revisa la conexión y pulsa Escritorio de nuevo.</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$s se detuvo inesperadamente. Comparte los registros en un informe de error o inténtalo de nuevo.</string> <string name="session_ended_program">El programa se detuvo inesperadamente. Comparte los registros en un informe de error o inténtalo de nuevo.</string> <string name="session_ended_desktop">El escritorio se detuvo inesperadamente. Comparte los registros en un informe de error o inténtalo de nuevo.</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">Código de salida %1$d · %2$s</string> <string name="session_ended_no_log">sin registro</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="one">El registro muestra %1$d error \"Function not implemented\": la aceleración seccomp de proot hace fallar un proceso auxiliar en este dispositivo. Prueba %2$s → \"%3$s\" (o \"%4$s\") y vuelve a iniciar.</item> <item quantity="many">El registro muestra %1$d errores \"Function not implemented\": la aceleración seccomp de proot hace fallar un proceso auxiliar en este dispositivo. Prueba %2$s → \"%3$s\" (o \"%4$s\") y vuelve a iniciar.</item> <item quantity="other">El registro muestra %1$d errores \"Function not implemented\": la aceleración seccomp de proot hace fallar un proceso auxiliar en este dispositivo. Prueba %2$s → \"%3$s\" (o \"%4$s\") y vuelve a iniciar.</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">Plugins</string> <string name="mode_decky_plugins_hint">Instala un ZIP de plugin para la próxima sesión de Steam.</string> <string name="mode_decky_plugin_downloading">Descargando…</string> <string name="mode_decky_plugin_install_zip">Instalar desde ZIP</string> - <!-- Store --> <string name="store_cat_games">Juegos</string> <string name="store_cat_audio_video">Audio y vídeo</string> <string name="store_cat_graphics">Gráficos</string> @@ -1389,21 +1348,14 @@ <string name="store_cat_utilities">Utilidades</string> <string name="store_section_popular">Populares</string> <string name="store_section_trending">En tendencia</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">Nuevas</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">Actualizadas</string> <string name="store_section_loading">cargando…</string> <string name="store_flatpak_setup">Configuración de Flatpak</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s: %2$s</string> <string name="store_updates_check_failed">No se pudieron buscar actualizaciones en Flathub</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">instalado</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">carpeta</string> - <!-- Launcher themes --> <string name="theme_graphite">Grafito</string> <string name="theme_graphite_detail">Superficies grafito, con el azul del orbe del logotipo para todo lo seleccionado, enfocado o pulsado.</string> <string name="theme_paper">Papel sobre negro</string> @@ -1412,7 +1364,6 @@ <string name="theme_icon_blue_detail">Fondo negro, texto blanco y el azul para todo lo seleccionado, enfocado o pulsado.</string> <string name="theme_electric">Azul marino eléctrico</string> <string name="theme_electric_detail">Un azul más vivo, sobre superficies que tiran a azul marino en vez de gris.</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Insert</string> <string name="pckb_key_del">Supr</string> @@ -1430,22 +1381,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">Espacio</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">Instala primero el runtime</string> <string name="game_sync_err_steam_library">La biblioteca de Steam no está disponible</string> <string name="game_sync_err_previous_sd">La biblioteca anterior de la SD no está disponible</string> <string name="game_sync_err_game_storage">El almacenamiento de juegos no está disponible</string> <string name="game_sync_err_added_games">La carpeta de juegos añadidos no está disponible</string> <string name="game_sync_err_manifests">Los manifiestos de Steam no están disponibles</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">Varios sistemas</string> - <!-- File manager --> <string name="fm_drive_internal">Interno</string> <string name="fm_drive_linux">Sistema Linux</string> <string name="fm_drive_sd">Tarjeta SD</string> @@ -1463,7 +1409,6 @@ <string name="fm_no_favorites">Aún no hay favoritos - fija una carpeta con su menú ⋮ para volver aquí rápidamente.</string> <string name="fm_unpin_favorite">Quitar de favoritos</string> <string name="fm_size_zero">0 bytes</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">bytes</string> <string name="fm_prop_location">Ubicación</string> <string name="fm_prop_type">Tipo</string> @@ -1471,7 +1416,6 @@ <string name="fm_prop_modified">Modificado</string> <string name="fm_type_folder">Carpeta</string> <string name="fm_type_file">Archivo</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">Archivo %1$s</string> <string name="fm_read_only">Solo lectura</string> <string name="fm_read_only_hint">Impide que los juegos (y Wine) sobrescriban o eliminen este archivo.</string> @@ -1485,15 +1429,11 @@ <string name="fm_delete_failed">No se pudo eliminar</string> <string name="fm_op_moving">Moviendo...</string> <string name="fm_op_copying">Copiando...</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">Moviendo - %1$s</string> <string name="fm_op_copying_item">Copiando - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">Moviendo %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">Copiando %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d hechos, %2$d con error</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d hechos, %2$d omitidos</string> <plurals name="fm_paste_moved"> <item quantity="one">%1$d elemento movido</item> @@ -1505,7 +1445,6 @@ <item quantity="many">%1$d elementos copiados</item> <item quantity="other">%1$d elementos copiados</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">\"%1$s\" ya existe</string> <string name="fm_op_renaming">Renombrando...</string> <string name="fm_rename_failed">No se pudo renombrar</string> @@ -1516,7 +1455,6 @@ <string name="fm_create">Crear</string> <string name="fm_new_name">Nuevo nombre</string> <string name="fm_delete_title">¿Eliminar?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">¿Eliminar \"%1$s\" de forma permanente?</string> <plurals name="fm_delete_items_title"> <item quantity="one">¿Eliminar %1$d elemento?</item> @@ -1524,13 +1462,11 @@ <item quantity="other">¿Eliminar %1$d elementos?</item> </plurals> <string name="fm_cannot_undo">Esta acción no se puede deshacer.</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="one">…y %1$d más</item> <item quantity="many">…y %1$d más</item> <item quantity="other">…y %1$d más</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">Eliminando %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="one">No se pudo eliminar %1$d</item> @@ -1546,7 +1482,6 @@ <string name="fm_conflict_skip">Omitir</string> <string name="fm_select_this_folder">Seleccionar esta carpeta</string> <string name="fm_back">Atrás</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$s está montado, pero ahora mismo no se puede leer</string> <string name="fm_list_view">Vista de lista</string> <string name="fm_grid_view">Vista de cuadrícula</string> @@ -1561,12 +1496,9 @@ <string name="fm_hide_favorites">Ocultar favoritos</string> <string name="fm_show_favorites">Mostrar favoritos</string> <string name="fm_filter_folder">Filtrar esta carpeta</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">%1$s libres</string> <string name="fm_selected_count">%1$d seleccionados</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">Ninguno</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">Todo</string> <string name="fm_paste">Pegar</string> <plurals name="fm_items"> @@ -1574,30 +1506,23 @@ <item quantity="many">%1$d elementos</item> <item quantity="other">%1$d elementos</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">Pegar %1$s aquí</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">Pegar %1$s aquí (mover)</string> <string name="fm_rail_title">Archivos</string> <string name="fm_rail_storage">ALMACENAMIENTO</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">ATAJOS</string> <string name="fm_rail_favorites">FAVORITOS</string> <string name="fm_rail_downloads">Descargas</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">Descargas</string> <string name="fm_rail_roms">ROMs</string> <string name="fm_rail_session_logs">Registros de sesión</string> <string name="fm_rail_pictures">Imágenes</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">Se añadió \"%1$s\" a Favoritos</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">Se quitó \"%1$s\" de Favoritos</string> <string name="fm_empty_directory">Carpeta vacía</string> <string name="fm_up_one_level">Subir un nivel</string> <string name="fm_choose_folder">Elige una carpeta</string> <string name="fm_choose_file">Elige un archivo</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Azul Steam</string> <string name="ctrl_tint_sky">Celeste</string> <string name="ctrl_tint_white">Blanco</string> @@ -1606,7 +1531,6 @@ <string name="ctrl_tint_amber">Ámbar</string> <string name="ctrl_tint_red">Rojo</string> <string name="ctrl_tint_pink">Rosa</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">Botón A</string> <string name="ctrl_button_b">Botón B</string> <string name="ctrl_button_x">Botón X</string> @@ -1617,28 +1541,21 @@ <string name="ctrl_button_rt">Gatillo derecho</string> <string name="ctrl_button_view">Botón Vista</string> <string name="ctrl_button_menu">Botón Menú</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> - <!-- Controller View button --> <string name="ctrl_target_view">Vista</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">Menú</string> <string name="ctrl_target_up">Cruceta arriba</string> <string name="ctrl_target_down">Cruceta abajo</string> <string name="ctrl_target_left">Cruceta izquierda</string> <string name="ctrl_target_right">Cruceta derecha</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">Oculto</string> <string name="ctrl_editor_drag">Arrastra para mover</string> <string name="ctrl_editor_snap">Ajustar</string> <string name="ctrl_editor_save">Guardar</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">Nada</string> <string name="second_mode_keyboard_trackpad">Teclado + trackpad</string> <string name="second_mode_terminal">Terminal</string> <string name="second_mode_deck_controls">Agarres de Deck + trackpads</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">Eliminando el runtime de Linux</string> <plurals name="rtinst_removing_entries"> <item quantity="one">Eliminando el runtime de Linux · %1$d elemento</item> @@ -1655,63 +1572,47 @@ <string name="rtinst_title_installing">Instalando el runtime de Linux</string> <string name="rtinst_title_removing">Eliminando el runtime de Linux</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">El runtime de Linux no está instalado</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">El catálogo no tiene suma de verificación para %1$s</string> <string name="deskpkg_checksum_mismatch">La suma de verificación no coincide - no se cambió nada</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">Instalando %1$s</string> <string name="deskpkg_place_failed">No se pudo colocar el AppImage</string> <string name="deskpkg_extract_failed">La extracción falló</string> <string name="deskpkg_install_failed">La instalación falló</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">La configuración de Flatpak falló (salida %1$d)</string> <string name="flatpakmgr_setup_unfinished">La configuración de Flatpak no terminó</string> <string name="flatpakmgr_busy">Hay otra tarea de la tienda en curso</string> <string name="flatpakmgr_not_ready">Configura Flatpak primero</string> <string name="flatpakmgr_starting">Iniciando</string> <string name="flatpakmgr_exit_status">Flatpak terminó con el código %1$d</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">Instalando %1$s</string> <string name="flatpakmgr_updating">Actualizando %1$s</string> <string name="flatpakmgr_removing">Quitando %1$s</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">Instalando %1$s (%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">Actualizando %1$s (%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">Quitando %1$s (%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">traducciones</string> <string name="flatpakmgr_ref_graphics_drivers">controladores gráficos</string> <string name="flatpakmgr_ref_media_codecs">códecs multimedia</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">runtime de %1$s</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">runtime de %1$s %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">El archivo es demasiado pequeño para ser un AppImage</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">No se puede leer el archivo: %1$s</string> <string name="appimg_not_linux">Esto no es un AppImage (no es un programa de Linux)</string> <string name="appimg_wrong_arch">Este AppImage no está hecho ni para ARM64 (aarch64) ni para PC x86</string> <string name="appimg_not_appimage">Es un programa de Linux, pero no un AppImage</string> <string name="appimg_wrong_type">Solo se pueden importar AppImages de tipo 2 (este es de tipo %1$d)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">No hay espacio libre suficiente: importarlo necesita unos %1$s</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">No hay espacio libre suficiente: actualizarlo necesita unos %1$s</string> <string name="appimg_import_failed">La importación falló</string> <string name="appimg_gone">Ese AppImage ya no está</string> <string name="appimg_update_failed">La actualización falló</string> - <!-- %1$s: file name --> <string name="appimg_extracting">Extrayendo %1$s</string> <string name="appimg_extract_failed">No se pudo extraer el AppImage</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">No se pudo extraer el AppImage: %1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">Detén la sesión activa antes de instalar Decky Loader</string> <string name="deckymgr_download_failed">Falló la descarga de Decky Loader</string> <string name="deckymgr_size_mismatch">El tamaño de Decky Loader no coincide con los metadatos de la versión</string> @@ -1747,20 +1648,13 @@ <string name="deckymgr_remote_entry_invalid">Entrada remote_binary no válida</string> <string name="deckymgr_remote_name_invalid">Nombre de binario remoto no válido</string> <string name="deckymgr_remote_url_https">La URL del binario remoto debe usar HTTPS</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">El binario remoto %1$s tiene un hash SHA-256 no válido</string> <string name="deckymgr_bin_dir_failed">No se pudo crear la carpeta bin del plugin</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">Descargando el binario del plugin · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">No se pudo descargar el binario del plugin %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">La suma de verificación del binario del plugin no coincide · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">No se pudo marcar como ejecutable el binario del plugin · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">No se pudo reemplazar el binario del plugin · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">No se pudo instalar el binario del plugin · %1$s</string> <string name="deckymgr_folder_taken">La carpeta del plugin pertenece a otro plugin</string> <string name="deckymgr_preserve_failed">No se pudo conservar el plugin existente</string> @@ -1768,20 +1662,14 @@ <string name="deckymgr_settings_failed">No se pudieron actualizar los ajustes de plugins de Decky</string> <string name="deckymgr_restore_failed">No se pudo restaurar el plugin existente</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">No se pudo guardar la identidad de la Depuración inalámbrica</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">Falló el comando ADB (%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Android no confirmó el cambio del límite de procesos secundarios: %1$s</string> <string name="adbfix_no_pairing">No hay ninguna vinculación de Depuración inalámbrica guardada</string> <string name="adbfix_enter_address">Introduce la dirección IP y el puerto actuales de la Depuración inalámbrica</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">Se agotó el tiempo de espera de la ventana de vinculación. Inténtalo de nuevo desde DroidDeck.</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">No se pudo buscar la ventana de vinculación (%1$d). Comprueba que el Wi-Fi esté activado.</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">No se pudo buscar la ventana de vinculación: %1$s</string> <string name="adbpair_six_digits">Introduce los 6 dígitos del código</string> <string name="adbpair_pairing">Vinculando…</string> @@ -1795,27 +1683,22 @@ <string name="adbpair_title_pair">Vincular Depuración inalámbrica</string> <string name="adbpair_waiting_text">Activa la Depuración inalámbrica y toca “Vincular dispositivo con código de vinculación”.</string> <string name="adbpair_waiting_long">Activa la Depuración inalámbrica y toca “Vincular dispositivo con código de vinculación”. Quédate en Ajustes: el código se introduce en esta notificación.</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">Código de vinculación de 6 dígitos</string> <string name="adbpair_code_hint">Toca Introducir código y escribe los 6 dígitos que muestra Ajustes.</string> <string name="adbpair_title_code">Introduce el código de vinculación Wi-Fi</string> <string name="adbpair_title_retry">Vuelve a probar el código</string> - <!-- Notification action button --> <string name="adbpair_enter_code">Introducir código</string> <string name="adbpair_title_working">Preparando Steam</string> <string name="adbpair_title_done">Steam está listo</string> <string name="adbpair_done_text">Toca para volver a DroidDeck. Ya puedes desactivar la Depuración inalámbrica.</string> <string name="adbpair_title_failed">Se detuvo la configuración de la Depuración inalámbrica</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">La descarga respondió HTTP %1$d</string> <string name="selfinst_size_mismatch">El tamaño de la descarga no coincidía con el APK publicado y se descartó - inténtalo de nuevo</string> <string name="selfinst_checksum_mismatch">La descarga no coincidía con su suma de verificación y se descartó - inténtalo de nuevo</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">La actualización es para el paquete de Android %1$s, no %2$s</string> <string name="selfinst_metadata_signer">Los metadatos de la actualización no indican la clave de firma de publicación de DroidDeck</string> <string name="selfinst_unreadable">Android no pudo leer el APK descargado</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">El APK descargado es para %1$s, no %2$s</string> <string name="selfinst_version_mismatch">El APK descargado indica versionCode %1$d, no el %2$d publicado</string> <string name="selfinst_downgrade">Android no instalará versionCode %1$d sobre el %2$d instalado</string> @@ -1823,16 +1706,11 @@ <string name="selfinst_wrong_signer">El APK descargado no está firmado con la clave de publicación de DroidDeck</string> <string name="appupd_http">El catálogo de actualizaciones respondió HTTP %1$d</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">Iniciando la sesión…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">%1$d:%2$02d transcurridos · sigue en marcha</string> <string name="loadstate_steam_update">Descargando la actualización del cliente de Steam · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">Descargando el cliente de Steam · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">Ya se está instalando una herramienta de compatibilidad</string> <string name="pextra_stop_session">Detén la sesión activa antes de instalar herramientas de compatibilidad</string> <string name="pextra_no_release">No se encontró una versión ARM64 de %1$s</string> @@ -1849,48 +1727,38 @@ <string name="pextra_checksum_mismatch">La suma de verificación no coincide; se eliminó la descarga</string> <string name="pextra_session_started">Se inició una sesión durante la descarga; detenla antes de instalar herramientas de compatibilidad</string> <string name="pextra_installing">Instalando %1$s</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">Falló la instalación de %1$s (salida %2$d)</string> <string name="pextra_could_not_install">No se pudo instalar %1$s</string> <string name="esync_fetching">Descargando el paquete droiddeck-esync</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">con auxiliares de Linux</string> <string name="cmgr_fex_dlls_only">Solo DLL</string> <string name="cmgr_original_proton_updated">Original (Proton se actualizó desde el último reemplazo)</string> <string name="cmgr_changed_outside">Cambiado fuera de Componentes</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">no presente</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">Original %1$s</string> <string name="cmgr_original_from">Original de %1$s</string> <string name="cmgr_swap_queued">%1$s %2$s entrará en %3$s cuando se cierre el juego que lo está usando.</string> <string name="cmgr_swap_done">%1$s %2$s ya está en %3$s. Se aplica la próxima vez que se inicie un juego.</string> <string name="cmgr_original_missing">Ese paquete original no está guardado</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%1$s de %2$s volverá a su original cuando se cierre el juego que lo está usando.</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">%1$s de %2$s restaurado desde su original de %3$s.</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%1$s está en uso en %2$s. Cambia antes ese Proton a otra cosa.</string> <string name="cmgr_package_not_stored">%1$s no está guardado</string> <string name="cmgr_deleted">%1$s eliminado.</string> <string name="cmgr_original_kept">El original de la versión instalada se conserva: es el camino de vuelta.</string> <string name="cmgr_not_stored">No guardado</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">Se eliminó el original de %1$s de %2$s.</string> <string name="cmgr_nightlies_only">Las descargas solo proceden de las versiones Nightlies</string> <string name="cmgr_list_predates_checksums">Esta lista de paquetes es anterior a las sumas de verificación - actualízala e inténtalo de nuevo</string> <string name="cmgr_checksum_mismatch">La suma de verificación no coincide</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">Lineal</string> <string name="sprefs_upscaler_nearest">Más cercano</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (calidad)</string> <string name="sprefs_upscaler_sharpen">Solo nitidez</string> <string name="sprefs_lod_bias_auto">Auto (según el escalado)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">Sin efectos.</string> <string name="look_game_clarity">Claridad</string> <string name="look_game_clarity_desc">Nitidez con un toque de contraste: el de todos los días.</string> @@ -1915,27 +1783,23 @@ <string name="look_anime_edge">Bordes anime</string> <string name="look_anime_edge_desc">Contornos toon más nitidez.</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">La GPU no es Adreno</string> <string name="gpuinfo_supported">Compatible</string> <string name="gpuinfo_experimental">Experimental: sus controladores son versiones de prueba</string> <string name="gpuinfo_below_tested">Por debajo del hardware probado (Adreno 725 y posteriores): puede que no funcione</string> <string name="gpuinfo_unsupported">No compatible: DroidDeck necesita una GPU Adreno (Snapdragon)</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">Se alcanzó el límite de solicitudes de GitHub - inténtalo más tarde</string> <string name="turnip_github_http">GitHub respondió HTTP %1$d</string> <string name="turnip_download_http">la descarga respondió HTTP %1$d</string> <string name="turnip_list_predates_checksums">Esta lista de controladores es anterior a las sumas de verificación - actualízala e inténtalo de nuevo</string> <string name="turnip_checksum_mismatch">La suma de verificación no coincide - se descartó la descarga</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">No es un controlador de AdrenoTools: %1$s</string> <string name="turnip_reject_no_meta">no hay meta.json</string> <string name="turnip_reject_meta_unreadable">no se puede leer meta.json (%1$s)</string> <string name="turnip_reject_linux_driver">es un controlador de runtime de Linux (impórtalo en \"Controlador de runtime\")</string> <string name="turnip_reject_wayland">es un controlador de juegos para Wayland, que esta app no usa</string> <string name="turnip_reject_no_library_name">meta.json no indica ningún libraryName, así que no se pudo pasar nada a AdrenoTools</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.json indica %1$s, que no está en el zip</string> <string name="turnip_reject_not_aarch64">%1$s no es una biblioteca compartida ELF AArch64 de 64 bits</string> <string name="turnip_reject_glibc">%1$s enlaza glibc - es un Turnip \"-Linux\", que va en \"Controlador de runtime\"</string> @@ -1946,90 +1810,61 @@ <string name="bundle_entry_outside">No es un paquete de controladores: %1$s apunta fuera de él</string> <string name="bundle_too_large">No es un paquete de controladores: ocupa más de %1$d MB al descomprimirse</string> - <!-- Game storage --> <string name="gstore_sd_card">Tarjeta SD</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">Carpeta</string> <string name="logshare_chooser">Compartir registros de sesión</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">requiere Android 10 o posterior (capas de pantalla con espacio de color)</string> <string name="hdr_none">esta pantalla no indica compatibilidad con HDR</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">esta pantalla admite %1$s, no HDR10</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">esta GPU</string> <string name="phantom_title_on">El límite de procesos secundarios está activado</string> <string name="phantom_title_unset">El límite de procesos secundarios no está definido</string> <string name="phantom_title_unreadable">No se pudo comprobar el límite de procesos secundarios</string> <string name="phantom_title_off">El límite de procesos secundarios está desactivado</string> <string name="phantom_title_not_required">El límite de procesos secundarios no es necesario</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s Si no es posible, conecta este dispositivo a un ordenador con ADB y ejecuta el comando de abajo. Después vuelve aquí y comprueba de nuevo.</string> <string name="phantom_instructions_off">Ya se pueden iniciar sesiones de Steam.</string> <string name="phantom_instructions_not_applicable">Esta versión de Android no usa este límite.</string> <string name="phantom_fix_toggle">Activa “Desactivar restricciones de procesos secundarios” en Opciones de desarrollador y deja activadas las Opciones de desarrollador después.</string> <string name="phantom_fix_no_toggle">Esta versión de Android no tiene un interruptor para ello. Usa la Depuración inalámbrica para cambiarlo desde este dispositivo.</string> <string name="upd_block_no_apk">Esta versión no tiene descarga para esta copia de DroidDeck.</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">Android no la instalará sobre esta versión porque su versionCode %1$d es inferior al %2$d instalado.</string> <string name="upd_fail_downgrade">Android no instala una versión anterior sobre una más reciente. Quédate en esta versión hasta que el canal la alcance.</string> <string name="upd_fail_signature">Esta copia de DroidDeck está firmada de otro modo (¿una compilación local?), así que Android no la actualizará sobre la instalada.</string> <string name="upd_fail_storage">No hay espacio libre suficiente para la actualización.</string> <string name="upd_fail_aborted">Se canceló la actualización.</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">La actualización no se instaló: %1$s</string> <string name="upd_fail_generic">La actualización no se instaló.</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">zip de %1$s</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">zip</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$s aún no se ha iniciado</string> <string name="saves_none_found">No se encontraron partidas guardadas de %1$s</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$s no tiene carpeta drive_c, así que no es un zip de partidas de GameHub ni de Winlator</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">no se pueden crear carpetas en %1$s</string> <string name="gstore_cannot_write">no se puede escribir en %1$s</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">no se puede escribir en %1$s (%2$s)</string> <string name="gfiles_folder_unavailable">La carpeta no está disponible</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">Otro archivo ya usa %1$s</string> <string name="gfiles_art_unavailable">La carpeta de imágenes no está disponible</string> <string name="gfiles_unrelated_inventory">La carpeta tiene un inventario de sincronización ajeno</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">No se pudo quitar %1$s</string> <string name="gfiles_outside_folder">El archivo sale de la carpeta de exportación</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX (compilación sin nombre de versión)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">no hay profile.json en %1$s</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$s no es un paquete de componentes de Linux (tipo %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · %2$s libres</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">Automático</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">El catálogo de actualizaciones no superó las comprobaciones (%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">Ajuste de procesos secundarios de DroidDeck</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">Sesión iniciada · modo sin conexión</string> <string name="setup_signed_in_unnamed">Sesión iniciada</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">INCLUIDO</string> <string name="driver_tag_downloaded">DESCARGADO</string> <string name="driver_tag_imported">IMPORTADO</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">ANDROID + LINUX</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> <string name="display_session">Pantalla de la sesión</string> <string name="display_resolution">Resolución</string> diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1b9c0d2dd..3506af5f1 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1141,16 +1141,12 @@ <string name="game_file_exported">ゲームファイルを %1$s に保存しました</string> <string name="game_frontend_files">フロントエンドファイル</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">実行環境の処理が完了するまでお待ちください</string> <string name="main_wait_proton_install">互換性ツールのインストールが完了するまでお待ちください</string> <string name="main_wait_desktop_install">デスクトップアプリのインストールが完了するまでお待ちください</string> <string name="main_wait_install_game">インストールが完了してからゲームを起動してください</string> <string name="main_saves_close_session">インポートしたデータがゲームに上書き保存されないよう、先にSteamセッションを終了してください</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">%1$sにインポート中</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="other">%2$sから%1$d個のファイルを%3$sにインポートしました</item> </plurals> @@ -1158,15 +1154,11 @@ <item quantity="other">%2$sから%1$d個のファイルを%3$sにインポートしました。以前のセーブデータはDownload/DroidDeck/Saves/backupsにバックアップされました</item> </plurals> <string name="main_saves_pick_zip">%1$sのセーブデータzipを選択</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">%1$sをエクスポート中</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="other">%1$d個のファイルを%2$sとしてエクスポートしました:%3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">%1$sの保存先を選択(%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">%1$sに失敗しました:%2$s</string> <string name="main_no_logs_yet">セッションログはまだありません。先にセッションを実行してください。</string> <string name="main_no_logs_to_clear">消去するセッションログはありません。</string> @@ -1179,68 +1171,55 @@ <string name="main_adb_copied">ADBコマンドをコピーしました</string> <string name="main_pick_roms">ROMフォルダを選択</string> <string name="main_non_adreno_title">Adreno GPUではありません</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">TurnipはAdreno GPUをサポートしています。%1$sでは、Steamの画面が真っ暗になる可能性があります。ダウンロード:%2$.0f MB。</string> <string name="main_install_anyway">インストールする</string> <string name="main_remove_runtime_title">Linux実行環境を削除</string> <string name="main_remove_runtime_text">実行環境、その中のSteamクライアント、そこにインストールされたすべてのゲームが削除されます。</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">適用しました:%1$s</string> <string name="android_app_open_failed_primary">メイン画面で%1$sを開けませんでした</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">%2$sで%1$sを開けませんでした</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">ディスプレイ%2$dで%1$sを開けませんでした</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">インポート中</string> <string name="comp_busy_swapping">置き換え中</string> <string name="comp_busy_restoring">復元中</string> <string name="comp_busy_cancelling">キャンセル中</string> <string name="comp_busy_deleting">削除中</string> <string name="comp_cannot_read">ファイルを読み取れません</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">%1$sをインポートしました</string> <string name="comp_swap_cancelled">待機中の置き換えをキャンセルしました。</string> <string name="comp_pick_wcp">コンポーネントパッケージ(-linux .wcp)を選択</string> <string name="comp_nightlies_unreachable">Nightliesに接続できませんでした</string> - <!-- %1$s is the package version --> <string name="comp_stored">%1$sを保存しました</string> <string name="comp_download_failed">ダウンロードに失敗しました:%1$s</string> <string name="drivers_pick_linux">Linuxランタイムドライバー(-Linux zipまたはAndroid + Linuxバンドル)を選択</string> <string name="drivers_pick_android">ディスプレイドライバー(AdrenoTools zipまたはAndroid + Linuxバンドル)を選択</string> <string name="drivers_pick_any">ドライバーzipまたはAndroid + Linuxバンドルを選択</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s:%2$s</string> <string name="main_removing_named">%1$sを削除中…</string> <string name="main_forgetting_named">%1$sを登録解除中…</string> <string name="main_pick_game_storage">ゲームの保存先フォルダを選択</string> <string name="main_pick_decky_zip">Deckyプラグインのzipを選択</string> <string name="main_storage_not_usable">使用できません:%1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">プログラム</string> <string name="main_removing_runtime">Linux実行環境を削除中</string> <string name="main_settings_main_screen">下部画面で設定を開けなかったため、メイン画面で開きました。</string> <string name="main_open_dev_options">設定で開発者向けオプションを開いてください</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">Androidの「不明なアプリのインストール」設定を開けませんでした</string> <string name="upd_couldnt_check">確認できませんでした:%1$s</string> <string name="upd_no_download">このビルドには、このDroidDeckコピー用のダウンロードがありません</string> <string name="upd_stop_first">実行中のセッションを停止してから更新してください</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">ダウンロード中</string> <string name="upd_stage_installing">インストール中</string> <string name="upd_stage_confirm">確認を待機中</string> <string name="upd_failed">アップデートに失敗しました</string> <string name="upd_not_offered">Androidがアップデートのインストールを表示しませんでした</string> - <!-- Decky --> <string name="decky_install_failed">Deckyのインストールに失敗しました:%1$s</string> <string name="decky_plugin_starting">プラグインのインポートを開始中…</string> <string name="decky_plugin_failed">プラグインのインポートに失敗しました:%1$s</string> <string name="decky_plugin_installed">プラグインをインストールしました。読み込むにはSteamを再起動してください。</string> - <!-- GPU drivers --> <string name="drivers_not_checked">未確認 - 更新をタップして新しいドライバーを検索</string> <string name="drivers_bundle_sets_display">Android + Linux:ディスプレイドライバーも一緒に設定されます</string> <string name="drivers_bundle_sets_runtime">Android + Linux:ランタイムドライバーも一緒に設定されます</string> @@ -1248,16 +1227,13 @@ <string name="drivers_system_vulkan">システムのVulkan:このGPU用の同梱ビルドはありません</string> <string name="drivers_bundled">%1$s(同梱)</string> <string name="drivers_manual">手動</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">現在、片方のみが公開されています</string> <string name="drivers_pair_size">両方で%1$.0f MB</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">設定できるドライバーがありません:%1$s</string> <string name="drivers_checking_latest">最新のドライバーを確認中…</string> <string name="drivers_up_to_date_check_failed">最新です(確認できませんでした:%1$s)</string> <string name="drivers_recommended_unpublished">推奨ドライバーは現在公開されていません</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">%1$s %2$sをダウンロード中…</string> <string name="drivers_updated_to">%1$s %2$sに更新しました。</string> <string name="drivers_updated_to_restart">%1$s %2$sに更新しました。ディスプレイドライバーはDroidDeckの再起動後に有効になります。</string> @@ -1272,9 +1248,7 @@ <string name="drivers_using_bundle_restart">%1$sをランタイムドライバーとディスプレイドライバーの両方として使用中。ディスプレイドライバーはDroidDeckの再起動後に有効になります。</string> <string name="drivers_deleted_bundle">%1$sを削除しました(両方のドライバー)</string> <string name="drivers_deleted">%1$sを削除しました</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">最新:%1$s · 確認 %2$s</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">最新:%1$s · %2$sに接続できません · 確認 %3$s</string> <plurals name="drivers_ago_minutes"> <item quantity="other">%1$d分前</item> @@ -1287,33 +1261,25 @@ </plurals> <string name="drivers_checking_sources">Banners-Turnip、WinNative、DroidDeckを確認中…</string> <string name="drivers_check_failed">確認に失敗</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">%1$sをインストールしました - どちらかの一覧で選択すると両方のドライバーが設定されます</string> <string name="drivers_installed_pick">%1$sをインストールしました - メニューで選択してください</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">実行環境の削除が完了するまでお待ちください</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">ゲーム</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">トラックパッド</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">ダイレクト</string> <string name="session_swap_failed">置き換えに失敗しました:%1$s</string> <string name="session_profile_save_failed">プロファイルの保存に失敗しました:%1$s</string> <string name="session_desktop_paused">デスクトップは一時停止中です</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$sは一時停止中です</string> <string name="session_program_paused">プログラムは一時停止中です</string> <string name="session_starting_desktop">デスクトップを起動中</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">%1$sを起動中</string> <string name="session_starting_program">プログラムを起動中</string> <string name="session_no_logs">このセッションのログはありません。</string> <string name="session_logs_archive_failed">セッションログのアーカイブを作成できませんでした。</string> <string name="session_logs_share_failed">セッションログを共有できませんでした。</string> <string name="session_starting_session">セッションを開始中…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">Linux実行環境をダウンロード中</string> <string name="session_runtime_downloading_mb">Linux実行環境をダウンロード中 · %1$d / %2$d MB</string> <string name="session_runtime_checking">Linux実行環境を確認中</string> @@ -1328,27 +1294,20 @@ <string name="session_proton_unpacking">Proton Experimental(ARM64)を展開中</string> <string name="session_runtime_catalog_unreachable">実行環境のカタログに接続できませんでした。接続を確認して、もう一度Playをタップしてください。</string> <string name="session_runtime_install_failed">Linux実行環境をインストールできませんでした。「設定」で実行環境を確認して、もう一度Playをタップしてください。</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">デスクトップをインストールできませんでした(%1$s)。接続を確認して、もう一度「デスクトップ」をタップしてください。</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$sが予期せず停止しました。バグレポートにログを添付して共有するか、もう一度お試しください。</string> <string name="session_ended_program">プログラムが予期せず停止しました。バグレポートにログを添付して共有するか、もう一度お試しください。</string> <string name="session_ended_desktop">デスクトップが予期せず停止しました。バグレポートにログを添付して共有するか、もう一度お試しください。</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">終了ステータス %1$d · %2$s</string> <string name="session_ended_no_log">ログなし</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="other">ログに「Function not implemented」エラーが%1$d件あります。このデバイスでは、prootのseccomp高速化が補助プロセスの処理に失敗しています。%2$s →「%3$s」(または「%4$s」)を試してから、もう一度起動してください。</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">プラグイン</string> <string name="mode_decky_plugins_hint">次回のSteamセッション用にプラグインのzipをインストールします。</string> <string name="mode_decky_plugin_downloading">ダウンロード中…</string> <string name="mode_decky_plugin_install_zip">zipからインストール</string> - <!-- Store --> <string name="store_cat_games">ゲーム</string> <string name="store_cat_audio_video">オーディオ・ビデオ</string> <string name="store_cat_graphics">グラフィック</string> @@ -1361,21 +1320,14 @@ <string name="store_cat_utilities">ユーティリティ</string> <string name="store_section_popular">人気</string> <string name="store_section_trending">トレンド</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">新着</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">最近の更新</string> <string name="store_section_loading">読み込み中…</string> <string name="store_flatpak_setup">Flatpakのセットアップ</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s:%2$s</string> <string name="store_updates_check_failed">Flathubでアップデートを確認できませんでした</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">インストール済み</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">フォルダ</string> - <!-- Launcher themes --> <string name="theme_graphite">グラファイト</string> <string name="theme_graphite_detail">グラファイト調の画面に、選択・フォーカス・押下時にはロゴのオーブと同じ青を使用します。</string> <string name="theme_paper">ペーパー・オン・ブラック</string> @@ -1384,7 +1336,6 @@ <string name="theme_icon_blue_detail">黒の背景に白の文字、選択・フォーカス・押下時には青を使用します。</string> <string name="theme_electric">エレクトリックネイビー</string> <string name="theme_electric_detail">より明るい青を、グレーではなくネイビー寄りの画面に使用します。</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Ins</string> <string name="pckb_key_del">Del</string> @@ -1402,22 +1353,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">スペース</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">先に実行環境をインストールしてください</string> <string name="game_sync_err_steam_library">Steamライブラリを利用できません</string> <string name="game_sync_err_previous_sd">以前のSDライブラリを利用できません</string> <string name="game_sync_err_game_storage">ゲームストレージを利用できません</string> <string name="game_sync_err_added_games">追加したゲームのフォルダを利用できません</string> <string name="game_sync_err_manifests">Steamのマニフェストを利用できません</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">多数のシステム</string> - <!-- File manager --> <string name="fm_drive_internal">内部</string> <string name="fm_drive_linux">Linux システム</string> <string name="fm_drive_sd">SDカード</string> @@ -1435,7 +1381,6 @@ <string name="fm_no_favorites">お気に入りはまだありません。フォルダの⋮メニューからピン留めすると、すぐにここへ戻れます。</string> <string name="fm_unpin_favorite">お気に入りから削除</string> <string name="fm_size_zero">0バイト</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">バイト</string> <string name="fm_prop_location">場所</string> <string name="fm_prop_type">種類</string> @@ -1443,7 +1388,6 @@ <string name="fm_prop_modified">更新日時</string> <string name="fm_type_folder">フォルダ</string> <string name="fm_type_file">ファイル</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">%1$sファイル</string> <string name="fm_read_only">読み取り専用</string> <string name="fm_read_only_hint">ゲーム(およびWine)がこのファイルを上書き・削除できないようにします。</string> @@ -1457,15 +1401,11 @@ <string name="fm_delete_failed">削除に失敗しました</string> <string name="fm_op_moving">移動中...</string> <string name="fm_op_copying">コピー中...</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">移動中 - %1$s</string> <string name="fm_op_copying_item">コピー中 - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">移動中 %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">コピー中 %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d件完了、%2$d件失敗</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d件完了、%2$d件スキップ</string> <plurals name="fm_paste_moved"> <item quantity="other">%1$d個の項目を移動しました</item> @@ -1473,7 +1413,6 @@ <plurals name="fm_paste_copied"> <item quantity="other">%1$d個の項目をコピーしました</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">「%1$s」はすでに存在します</string> <string name="fm_op_renaming">名前を変更中...</string> <string name="fm_rename_failed">名前を変更できませんでした</string> @@ -1484,17 +1423,14 @@ <string name="fm_create">作成</string> <string name="fm_new_name">新しい名前</string> <string name="fm_delete_title">削除しますか?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">「%1$s」を完全に削除しますか?</string> <plurals name="fm_delete_items_title"> <item quantity="other">%1$d個の項目を削除しますか?</item> </plurals> <string name="fm_cannot_undo">この操作は元に戻せません。</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="other">…ほか%1$d件</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">削除中 %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="other">%1$d件を削除できませんでした</item> @@ -1508,7 +1444,6 @@ <string name="fm_conflict_skip">スキップ</string> <string name="fm_select_this_folder">このフォルダを選択</string> <string name="fm_back">戻る</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$sはマウントされていますが、現在読み取れません</string> <string name="fm_list_view">リスト表示</string> <string name="fm_grid_view">グリッド表示</string> @@ -1523,41 +1458,31 @@ <string name="fm_hide_favorites">お気に入りを非表示</string> <string name="fm_show_favorites">お気に入りを表示</string> <string name="fm_filter_folder">このフォルダを絞り込み</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">空き %1$s</string> <string name="fm_selected_count">%1$d個選択中</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">解除</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">すべて</string> <string name="fm_paste">貼り付け</string> <plurals name="fm_items"> <item quantity="other">%1$d個の項目</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">%1$sをここに貼り付け</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">%1$sをここに貼り付け(移動)</string> <string name="fm_rail_title">ファイル</string> <string name="fm_rail_storage">ストレージ</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">クイック</string> <string name="fm_rail_favorites">お気に入り</string> <string name="fm_rail_downloads">ダウンロード</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">DL</string> <string name="fm_rail_roms">ROM</string> <string name="fm_rail_session_logs">セッションログ</string> <string name="fm_rail_pictures">画像</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">「%1$s」をお気に入りに追加しました</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">「%1$s」をお気に入りから削除しました</string> <string name="fm_empty_directory">空のフォルダ</string> <string name="fm_up_one_level">1つ上の階層へ</string> <string name="fm_choose_folder">フォルダを選択</string> <string name="fm_choose_file">ファイルを選択</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Steamブルー</string> <string name="ctrl_tint_sky">スカイ</string> <string name="ctrl_tint_white">ホワイト</string> @@ -1566,7 +1491,6 @@ <string name="ctrl_tint_amber">アンバー</string> <string name="ctrl_tint_red">レッド</string> <string name="ctrl_tint_pink">ピンク</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">Aボタン</string> <string name="ctrl_button_b">Bボタン</string> <string name="ctrl_button_x">Xボタン</string> @@ -1577,28 +1501,21 @@ <string name="ctrl_button_rt">右トリガー</string> <string name="ctrl_button_view">ビューボタン</string> <string name="ctrl_button_menu">メニューボタン</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> - <!-- Controller View button --> <string name="ctrl_target_view">ビュー</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">メニュー</string> <string name="ctrl_target_up">十字キー上</string> <string name="ctrl_target_down">十字キー下</string> <string name="ctrl_target_left">十字キー左</string> <string name="ctrl_target_right">十字キー右</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">非表示</string> <string name="ctrl_editor_drag">ドラッグして移動</string> <string name="ctrl_editor_snap">スナップ</string> <string name="ctrl_editor_save">保存</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">なし</string> <string name="second_mode_keyboard_trackpad">キーボード + トラックパッド</string> <string name="second_mode_terminal">ターミナル</string> <string name="second_mode_deck_controls">Deckグリップ + トラックパッド</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">Linux実行環境を削除中</string> <plurals name="rtinst_removing_entries"> <item quantity="other">Linux実行環境を削除中 · %1$d項目</item> @@ -1613,63 +1530,47 @@ <string name="rtinst_title_installing">Linux実行環境をインストール中</string> <string name="rtinst_title_removing">Linux実行環境を削除中</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">Linux実行環境がインストールされていません</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">カタログに%1$sのチェックサムがありません</string> <string name="deskpkg_checksum_mismatch">チェックサムが一致しません - 何も変更されていません</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">%1$sをインストール中</string> <string name="deskpkg_place_failed">AppImageを配置できませんでした</string> <string name="deskpkg_extract_failed">展開に失敗しました</string> <string name="deskpkg_install_failed">インストールに失敗しました</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">Flatpakのセットアップに失敗しました(終了コード %1$d)</string> <string name="flatpakmgr_setup_unfinished">Flatpakのセットアップが完了しませんでした</string> <string name="flatpakmgr_busy">別のストアのタスクを実行中です</string> <string name="flatpakmgr_not_ready">先にFlatpakをセットアップしてください</string> <string name="flatpakmgr_starting">開始中</string> <string name="flatpakmgr_exit_status">Flatpakがステータス%1$dで終了しました</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">%1$sをインストール中</string> <string name="flatpakmgr_updating">%1$sを更新中</string> <string name="flatpakmgr_removing">%1$sを削除中</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">%1$sをインストール中(%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">%1$sを更新中(%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">%1$sを削除中(%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">翻訳</string> <string name="flatpakmgr_ref_graphics_drivers">グラフィックドライバー</string> <string name="flatpakmgr_ref_media_codecs">メディアコーデック</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">%1$sランタイム</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">%1$sランタイム %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">ファイルが小さすぎるため、AppImageではありません</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">ファイルを読み取れません:%1$s</string> <string name="appimg_not_linux">AppImageではありません(Linuxプログラムではありません)</string> <string name="appimg_wrong_arch">このAppImageはARM64(aarch64)向けでもx86 PC向けでもありません</string> <string name="appimg_not_appimage">Linuxプログラムですが、AppImageではありません</string> <string name="appimg_wrong_type">インポートできるのはタイプ2のAppImageのみです(このファイルはタイプ%1$d)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">空き容量不足:インポートには約%1$sが必要です</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">空き容量不足:更新には約%1$sが必要です</string> <string name="appimg_import_failed">インポートに失敗しました</string> <string name="appimg_gone">このAppImageは存在しません</string> <string name="appimg_update_failed">更新に失敗しました</string> - <!-- %1$s: file name --> <string name="appimg_extracting">%1$sを展開中</string> <string name="appimg_extract_failed">AppImageを展開できませんでした</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">AppImageを展開できませんでした:%1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">Decky Loaderをインストールする前に、実行中のセッションを停止してください</string> <string name="deckymgr_download_failed">Decky Loaderのダウンロードに失敗しました</string> <string name="deckymgr_size_mismatch">Decky Loaderのサイズがリリース情報と一致しません</string> @@ -1705,20 +1606,13 @@ <string name="deckymgr_remote_entry_invalid">remote_binaryの項目が無効です</string> <string name="deckymgr_remote_name_invalid">リモートバイナリ名が無効です</string> <string name="deckymgr_remote_url_https">リモートバイナリのURLにはHTTPSを使用する必要があります</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">リモートバイナリ%1$sのSHA-256ハッシュが無効です</string> <string name="deckymgr_bin_dir_failed">プラグインのbinフォルダを作成できませんでした</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">プラグインバイナリをダウンロード中 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">プラグインバイナリ%1$sをダウンロードできませんでした</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">プラグインバイナリのチェックサムが一致しません · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">プラグインバイナリを実行可能にできませんでした · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">プラグインバイナリを置き換えられませんでした · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">プラグインバイナリをインストールできませんでした · %1$s</string> <string name="deckymgr_folder_taken">このプラグインフォルダは別のプラグインが使用しています</string> <string name="deckymgr_preserve_failed">既存のプラグインを保持できませんでした</string> @@ -1726,20 +1620,14 @@ <string name="deckymgr_settings_failed">Deckyのプラグイン設定を更新できませんでした</string> <string name="deckymgr_restore_failed">既存のプラグインを復元できませんでした</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">ワイヤレスデバッグの識別情報を保存できませんでした</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">ADBコマンドに失敗しました(%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Androidは子プロセス制限の変更を確認しませんでした:%1$s</string> <string name="adbfix_no_pairing">ワイヤレスデバッグのペアリング情報が保存されていません</string> <string name="adbfix_enter_address">現在のワイヤレスデバッグのIPアドレスとポートを入力してください</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">ペアリングのポップアップを待機中にタイムアウトしました。DroidDeckからもう一度お試しください。</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">ペアリングのポップアップを検索できませんでした(%1$d)。Wi-Fiがオンになっているか確認してください。</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">ペアリングのポップアップを検索できませんでした:%1$s</string> <string name="adbpair_six_digits">6桁のコードをすべて入力してください</string> <string name="adbpair_pairing">ペアリング中…</string> @@ -1753,27 +1641,22 @@ <string name="adbpair_title_pair">ワイヤレスデバッグのペアリング</string> <string name="adbpair_waiting_text">ワイヤレスデバッグをオンにして、「ペアリングコードでデバイスをペア設定」をタップしてください。</string> <string name="adbpair_waiting_long">ワイヤレスデバッグをオンにして、「ペアリングコードでデバイスをペア設定」をタップしてください。設定画面のままにしてください:コードはこの通知に入力します。</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">6桁のペアリングコード</string> <string name="adbpair_code_hint">「コードを入力」をタップし、設定に表示された6桁の数字を入力してください。</string> <string name="adbpair_title_code">Wi-Fiペアリングコードを入力</string> <string name="adbpair_title_retry">コードをもう一度入力</string> - <!-- Notification action button --> <string name="adbpair_enter_code">コードを入力</string> <string name="adbpair_title_working">Steamをセットアップ中</string> <string name="adbpair_title_done">Steamの準備ができました</string> <string name="adbpair_done_text">タップしてDroidDeckに戻ります。ワイヤレスデバッグはオフにしてかまいません。</string> <string name="adbpair_title_failed">ワイヤレスデバッグのセットアップが中断しました</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">ダウンロード先がHTTP %1$dを返しました</string> <string name="selfinst_size_mismatch">ダウンロードしたファイルのサイズが公開されているAPKと一致しないため、破棄しました。もう一度お試しください</string> <string name="selfinst_checksum_mismatch">ダウンロードしたファイルがチェックサムと一致しないため、破棄しました。もう一度お試しください</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">このアップデートはAndroidパッケージ%1$s用で、%2$s用ではありません</string> <string name="selfinst_metadata_signer">アップデート情報にDroidDeckのリリース署名キーが記載されていません</string> <string name="selfinst_unreadable">AndroidがダウンロードしたAPKを読み取れませんでした</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">ダウンロードしたAPKは%1$s用で、%2$s用ではありません</string> <string name="selfinst_version_mismatch">ダウンロードしたAPKのversionCodeは%1$dで、公開されている%2$dと一致しません</string> <string name="selfinst_downgrade">AndroidはインストールされているversionCode %2$dの上に%1$dをインストールしません</string> @@ -1781,16 +1664,11 @@ <string name="selfinst_wrong_signer">ダウンロードしたAPKはDroidDeckのリリースキーで署名されていません</string> <string name="appupd_http">アップデートカタログがHTTP %1$dを返しました</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">セッションを開始中…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">%1$d:%2$02d 経過 · 処理中</string> <string name="loadstate_steam_update">Steamクライアントのアップデートをダウンロード中 · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">Steamクライアントをダウンロード中 · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">互換性ツールのインストールはすでに実行中です</string> <string name="pextra_stop_session">互換性ツールをインストールする前に、実行中のセッションを停止してください</string> <string name="pextra_no_release">%1$sのARM64リリースが見つかりませんでした</string> @@ -1807,48 +1685,38 @@ <string name="pextra_checksum_mismatch">チェックサムが一致しないため、ダウンロードを削除しました</string> <string name="pextra_session_started">ダウンロード中にセッションが開始されました。停止してから互換性ツールをインストールしてください</string> <string name="pextra_installing">%1$sをインストール中</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">%1$sのインストールに失敗しました(終了コード %2$d)</string> <string name="pextra_could_not_install">%1$sをインストールできませんでした</string> <string name="esync_fetching">droiddeck-esyncパックを取得中</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">Linux補助プログラム付き</string> <string name="cmgr_fex_dlls_only">DLLのみ</string> <string name="cmgr_original_proton_updated">元(前回の置き換え以降にProtonが更新されました)</string> <string name="cmgr_changed_outside">「コンポーネント」以外で変更されました</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">なし</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">元 %1$s</string> <string name="cmgr_original_from">%1$sの元のファイル</string> <string name="cmgr_swap_queued">%3$sで実行中のゲームが終了すると、%1$s %2$sがそこに入ります。</string> <string name="cmgr_swap_done">%1$s %2$sが%3$sに入りました。次回のゲーム起動時に有効になります。</string> <string name="cmgr_original_missing">その元のバンドルは保存されていません</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%2$sで実行中のゲームが終了すると、その%1$sが元に戻ります。</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">%2$sの%1$sを%3$sの元のファイルから復元しました。</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%1$sは%2$sで使用中です。先にそのProtonを別のものに置き換えてください。</string> <string name="cmgr_package_not_stored">%1$sは保存されていません</string> <string name="cmgr_deleted">%1$sを削除しました。</string> <string name="cmgr_original_kept">インストール済みビルドの元のファイルは、元に戻すために保持されます。</string> <string name="cmgr_not_stored">保存されていません</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">%2$sの%1$s版の元のファイルを削除しました。</string> <string name="cmgr_nightlies_only">ダウンロードはNightliesのリリースからのみ行われます</string> <string name="cmgr_list_predates_checksums">このパッケージ一覧はチェックサム導入前のものです。更新してからもう一度お試しください</string> <string name="cmgr_checksum_mismatch">チェックサムが一致しません</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">リニア</string> <string name="sprefs_upscaler_nearest">ニアレスト</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR(品質)</string> <string name="sprefs_upscaler_sharpen">シャープのみ</string> <string name="sprefs_lod_bias_auto">自動(スケーリングに合わせる)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">何も適用されません。</string> <string name="look_game_clarity">ゲームクラリティ</string> <string name="look_game_clarity_desc">シャープ化に少しコントラストを加えた、普段使い向け。</string> @@ -1873,27 +1741,23 @@ <string name="look_anime_edge">アニメエッジ</string> <string name="look_anime_edge_desc">トゥーン輪郭線とシャープ化。</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">Adreno GPUではありません</string> <string name="gpuinfo_supported">サポート対象</string> <string name="gpuinfo_experimental">実験的:ドライバーはテストビルドです</string> <string name="gpuinfo_below_tested">テスト済みハードウェア(Adreno 725以降)未満:動作しない可能性があります</string> <string name="gpuinfo_unsupported">サポート対象外:DroidDeckにはAdreno(Snapdragon)GPUが必要です</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">GitHubのレート制限に達しました。後でもう一度お試しください</string> <string name="turnip_github_http">GitHubがHTTP %1$dを返しました</string> <string name="turnip_download_http">ダウンロード先がHTTP %1$dを返しました</string> <string name="turnip_list_predates_checksums">このドライバー一覧はチェックサム導入前のものです。更新してからもう一度お試しください</string> <string name="turnip_checksum_mismatch">チェックサムが一致しないため、ダウンロードを破棄しました</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">AdrenoToolsドライバーではありません:%1$s</string> <string name="turnip_reject_no_meta">meta.jsonがありません</string> <string name="turnip_reject_meta_unreadable">meta.jsonを読み取れません(%1$s)</string> <string name="turnip_reject_linux_driver">これはLinuxランタイムドライバーです(「ランタイムドライバー」でインポートしてください)</string> <string name="turnip_reject_wayland">これはWaylandゲーム用ドライバーで、このアプリでは使用しません</string> <string name="turnip_reject_no_library_name">meta.jsonにlibraryNameが指定されていないため、AdrenoToolsに渡せるものがありません</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.jsonで指定された%1$sがzipに含まれていません</string> <string name="turnip_reject_not_aarch64">%1$sは64ビットAArch64 ELF共有ライブラリではありません</string> <string name="turnip_reject_glibc">%1$sはglibcにリンクしています。これは「-Linux」Turnipなので、「ランタイムドライバー」でインポートしてください</string> @@ -1904,90 +1768,61 @@ <string name="bundle_entry_outside">ドライバーバンドルではありません:%1$sがバンドルの外を指しています</string> <string name="bundle_too_large">ドライバーバンドルではありません:展開後のサイズが%1$d MBを超えます</string> - <!-- Game storage --> <string name="gstore_sd_card">SDカード</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">フォルダ</string> <string name="logshare_chooser">セッションログを共有</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">Android 10以降が必要です(色空間付きのディスプレイレイヤー)</string> <string name="hdr_none">このディスプレイはHDRに対応していません</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">このディスプレイは%1$sに対応していますが、HDR10には対応していません</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">このGPU</string> <string name="phantom_title_on">子プロセス制限がオンです</string> <string name="phantom_title_unset">子プロセス制限が未設定です</string> <string name="phantom_title_unreadable">子プロセス制限を確認できませんでした</string> <string name="phantom_title_off">子プロセス制限はオフです</string> <string name="phantom_title_not_required">子プロセス制限の変更は不要です</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s それができない場合は、このデバイスをADBが使えるPCに接続し、以下のコマンドを実行してください。その後ここに戻って再確認してください。</string> <string name="phantom_instructions_off">Steamセッションを開始できます。</string> <string name="phantom_instructions_not_applicable">このAndroidバージョンではこの制限は使用されません。</string> <string name="phantom_fix_toggle">開発者向けオプションで「子プロセス制限を無効にする」をオンにし、その後も開発者向けオプションをオンのままにしてください。</string> <string name="phantom_fix_no_toggle">このAndroidバージョンにはこのスイッチがありません。ワイヤレスデバッグを使って、このデバイスから変更してください。</string> <string name="upd_block_no_apk">このビルドには、このDroidDeckコピー用のダウンロードがありません。</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">このビルドのversionCode %1$dはインストール済みの%2$dより小さいため、Androidは上書きインストールしません。</string> <string name="upd_fail_downgrade">Androidは新しいバージョンの上に古いバージョンをインストールしません。チャンネルが追いつくまで、このビルドを使い続けてください。</string> <string name="upd_fail_signature">このDroidDeckコピーは署名が異なる(ローカルビルド?)ため、Androidは上書き更新しません。</string> <string name="upd_fail_storage">アップデートに必要な空き容量が不足しています。</string> <string name="upd_fail_aborted">アップデートはキャンセルされました。</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">アップデートをインストールできませんでした:%1$s</string> <string name="upd_fail_generic">アップデートをインストールできませんでした。</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">%1$s zip</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">zip</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$sはまだ起動されていません</string> <string name="saves_none_found">%1$sのセーブファイルが見つかりません</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$sにはdrive_cフォルダがないため、GameHubまたはWinlatorのセーブデータzipではありません</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">%1$sにフォルダを作成できません</string> <string name="gstore_cannot_write">%1$sに書き込めません</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">%1$sに書き込めません(%2$s)</string> <string name="gfiles_folder_unavailable">フォルダを利用できません</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">%1$sは別のファイルがすでに使用しています</string> <string name="gfiles_art_unavailable">画像フォルダを利用できません</string> <string name="gfiles_unrelated_inventory">フォルダに無関係な同期情報があります</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">%1$sを削除できませんでした</string> <string name="gfiles_outside_folder">ファイルがエクスポートフォルダの外を指しています</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX(バージョン名のないビルド)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">%1$sにprofile.jsonがありません</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$sはLinuxコンポーネントパッケージではありません(タイプ %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · 空き %2$s</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">自動</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">アップデートカタログの検証に失敗しました(%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">DroidDeckの子プロセス設定</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">ログイン中 · オフラインモード</string> <string name="setup_signed_in_unnamed">ログイン中</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">同梱</string> <string name="driver_tag_downloaded">ダウンロード</string> <string name="driver_tag_imported">インポート</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">ANDROID + LINUX</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s。%2$s\n\n%3$s</string> <string name="display_session">セッションの表示</string> <string name="display_resolution">解像度</string> diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f0672279d..f47ca0874 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1141,16 +1141,12 @@ <string name="game_file_exported">게임 파일이 %1$s에 저장되었습니다</string> <string name="game_frontend_files">프론트엔드 파일</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">런타임 작업이 끝날 때까지 기다려 주세요</string> <string name="main_wait_proton_install">호환성 도구 설치가 끝날 때까지 기다려 주세요</string> <string name="main_wait_desktop_install">데스크톱 앱 설치가 끝날 때까지 기다려 주세요</string> <string name="main_wait_install_game">설치가 끝난 후 게임을 실행하세요</string> <string name="main_saves_close_session">게임이 가져온 데이터를 덮어쓰지 않도록 먼저 Steam 세션을 종료하세요</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">%1$s(으)로 가져오는 중</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="other">%2$s에서 %3$s(으)로 파일 %1$d개를 가져왔습니다</item> </plurals> @@ -1158,15 +1154,11 @@ <item quantity="other">%2$s에서 %3$s(으)로 파일 %1$d개를 가져왔습니다. 이전 저장 데이터는 Download/DroidDeck/Saves/backups에 백업되었습니다</item> </plurals> <string name="main_saves_pick_zip">%1$s의 저장 데이터 zip 선택</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">%1$s 내보내는 중</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="other">파일 %1$d개를 %2$s(으)로 내보냈습니다: %3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">%1$s을(를) 저장할 위치 선택(%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">%1$s 실패: %2$s</string> <string name="main_no_logs_yet">아직 세션 로그가 없습니다. 먼저 세션을 실행하세요.</string> <string name="main_no_logs_to_clear">지울 세션 로그가 없습니다.</string> @@ -1179,68 +1171,55 @@ <string name="main_adb_copied">ADB 명령이 복사되었습니다</string> <string name="main_pick_roms">ROM 폴더 선택</string> <string name="main_non_adreno_title">Adreno GPU가 아님</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">Turnip은 Adreno GPU를 지원합니다. %1$s에서는 Steam 화면이 검게 표시될 수 있습니다. 다운로드: %2$.0f MB.</string> <string name="main_install_anyway">그래도 설치</string> <string name="main_remove_runtime_title">Linux 런타임 제거</string> <string name="main_remove_runtime_text">런타임과 그 안의 Steam 클라이언트, 그리고 런타임에 설치된 모든 게임이 삭제됩니다.</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">적용됨: %1$s</string> <string name="android_app_open_failed_primary">메인 화면에서 %1$s을(를) 열 수 없습니다</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">%2$s에서 %1$s을(를) 열 수 없습니다</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">디스플레이 %2$d에서 %1$s을(를) 열 수 없습니다</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">가져오는 중</string> <string name="comp_busy_swapping">교체하는 중</string> <string name="comp_busy_restoring">복원하는 중</string> <string name="comp_busy_cancelling">취소하는 중</string> <string name="comp_busy_deleting">삭제하는 중</string> <string name="comp_cannot_read">파일을 읽을 수 없음</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">%1$s 가져옴</string> <string name="comp_swap_cancelled">대기 중이던 교체가 취소되었습니다.</string> <string name="comp_pick_wcp">구성 요소 패키지 선택(-linux .wcp)</string> <string name="comp_nightlies_unreachable">Nightlies에 연결할 수 없습니다</string> - <!-- %1$s is the package version --> <string name="comp_stored">%1$s 저장됨</string> <string name="comp_download_failed">다운로드 실패: %1$s</string> <string name="drivers_pick_linux">Linux 런타임 드라이버 선택(-Linux zip 또는 Android + Linux 번들)</string> <string name="drivers_pick_android">디스플레이 드라이버 선택(AdrenoTools zip 또는 Android + Linux 번들)</string> <string name="drivers_pick_any">드라이버 zip 또는 Android + Linux 번들 선택</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s: %2$s</string> <string name="main_removing_named">%1$s 제거 중…</string> <string name="main_forgetting_named">%1$s 잊는 중…</string> <string name="main_pick_game_storage">게임 저장 폴더 선택</string> <string name="main_pick_decky_zip">Decky 플러그인 ZIP 선택</string> <string name="main_storage_not_usable">사용할 수 없음: %1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">프로그램</string> <string name="main_removing_runtime">Linux 런타임 제거 중</string> <string name="main_settings_main_screen">하단 화면에서 설정을 열 수 없어 메인 화면에서 열었습니다.</string> <string name="main_open_dev_options">설정에서 개발자 옵션을 여세요</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">Android의 \"출처를 알 수 없는 앱 설치\" 설정을 열 수 없습니다</string> <string name="upd_couldnt_check">확인할 수 없음: %1$s</string> <string name="upd_no_download">이 빌드에는 이 DroidDeck 복사본용 다운로드가 없습니다</string> <string name="upd_stop_first">실행 중인 세션을 먼저 중지한 후 업데이트하세요</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">다운로드 중</string> <string name="upd_stage_installing">설치 중</string> <string name="upd_stage_confirm">확인 대기 중</string> <string name="upd_failed">업데이트 실패</string> <string name="upd_not_offered">Android에서 업데이트 설치를 진행하지 않았습니다</string> - <!-- Decky --> <string name="decky_install_failed">Decky 설치 실패: %1$s</string> <string name="decky_plugin_starting">플러그인 가져오기 시작 중…</string> <string name="decky_plugin_failed">플러그인 가져오기 실패: %1$s</string> <string name="decky_plugin_installed">플러그인이 설치되었습니다. 불러오려면 Steam을 다시 시작하세요.</string> - <!-- GPU drivers --> <string name="drivers_not_checked">아직 확인하지 않음 - 새로 고침을 탭하여 새 드라이버를 찾으세요</string> <string name="drivers_bundle_sets_display">Android + Linux: 디스플레이 드라이버도 함께 설정</string> <string name="drivers_bundle_sets_runtime">Android + Linux: 런타임 드라이버도 함께 설정</string> @@ -1248,16 +1227,13 @@ <string name="drivers_system_vulkan">시스템 Vulkan: 이 GPU용 내장 빌드 없음</string> <string name="drivers_bundled">%1$s(내장)</string> <string name="drivers_manual">수동</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">현재 한쪽만 배포되어 있습니다</string> <string name="drivers_pair_size">둘 다 %1$.0f MB</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">설정할 드라이버 없음: %1$s</string> <string name="drivers_checking_latest">최신 드라이버 확인 중…</string> <string name="drivers_up_to_date_check_failed">최신 버전(확인 실패: %1$s)</string> <string name="drivers_recommended_unpublished">현재 권장 드라이버가 배포되지 않았습니다</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">%1$s %2$s 다운로드 중…</string> <string name="drivers_updated_to">%1$s %2$s(으)로 업데이트했습니다.</string> <string name="drivers_updated_to_restart">%1$s %2$s(으)로 업데이트했습니다. 디스플레이 드라이버는 DroidDeck을 다시 시작한 후 적용됩니다.</string> @@ -1272,9 +1248,7 @@ <string name="drivers_using_bundle_restart">%1$s을(를) 런타임 및 디스플레이 드라이버로 사용 중. 디스플레이 드라이버는 DroidDeck을 다시 시작한 후 적용됩니다.</string> <string name="drivers_deleted_bundle">%1$s 삭제됨(드라이버 2개)</string> <string name="drivers_deleted">%1$s 삭제됨</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">최신: %1$s · %2$s 확인</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">최신: %1$s · %2$s 연결 불가 · %3$s 확인</string> <plurals name="drivers_ago_minutes"> <item quantity="other">%1$d분 전</item> @@ -1287,33 +1261,25 @@ </plurals> <string name="drivers_checking_sources">Banners-Turnip, WinNative 및 DroidDeck 확인 중…</string> <string name="drivers_check_failed">확인 실패</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">%1$s 설치됨 - 두 목록 중 어디에서든 선택하면 두 드라이버가 모두 설정됩니다</string> <string name="drivers_installed_pick">%1$s 설치됨 - 메뉴에서 선택하세요</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">런타임 제거가 끝날 때까지 기다려 주세요</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">게임</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">터치패드</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">직접 터치</string> <string name="session_swap_failed">교체 실패: %1$s</string> <string name="session_profile_save_failed">프로필 저장 실패: %1$s</string> <string name="session_desktop_paused">데스크톱이 일시 중지됨</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$s 일시 중지됨</string> <string name="session_program_paused">프로그램이 일시 중지됨</string> <string name="session_starting_desktop">데스크톱 시작 중</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">%1$s 시작 중</string> <string name="session_starting_program">프로그램 시작 중</string> <string name="session_no_logs">이 세션의 로그가 없습니다.</string> <string name="session_logs_archive_failed">세션 로그 압축 파일을 만들 수 없습니다.</string> <string name="session_logs_share_failed">세션 로그를 공유할 수 없습니다.</string> <string name="session_starting_session">세션 시작 중…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">Linux 런타임 다운로드 중</string> <string name="session_runtime_downloading_mb">Linux 런타임 다운로드 중 · %1$d/%2$d MB</string> <string name="session_runtime_checking">Linux 런타임 확인 중</string> @@ -1328,27 +1294,20 @@ <string name="session_proton_unpacking">Proton Experimental(ARM64) 압축 해제 중</string> <string name="session_runtime_catalog_unreachable">런타임 카탈로그에 연결할 수 없습니다. 연결을 확인한 후 \"Steam 실행\"을 다시 누르세요.</string> <string name="session_runtime_install_failed">Linux 런타임이 설치되지 않았습니다. \"설정\"에서 런타임을 확인한 후 \"Steam 실행\"을 다시 누르세요.</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">데스크톱이 설치되지 않았습니다(%1$s). 연결을 확인한 후 \"데스크톱\"을 다시 누르세요.</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$s이(가) 예기치 않게 중지되었습니다. 버그 리포트와 함께 로그를 공유하거나 다시 시도하세요.</string> <string name="session_ended_program">프로그램이 예기치 않게 중지되었습니다. 버그 리포트와 함께 로그를 공유하거나 다시 시도하세요.</string> <string name="session_ended_desktop">데스크톱이 예기치 않게 중지되었습니다. 버그 리포트와 함께 로그를 공유하거나 다시 시도하세요.</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">종료 상태 %1$d · %2$s</string> <string name="session_ended_no_log">로그 없음</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="other">로그에 \"Function not implemented\" 오류가 %1$d개 있습니다. 이 기기에서 proot의 seccomp 가속이 보조 프로세스를 실패시키고 있습니다. %2$s → \"%3$s\"(또는 \"%4$s\")를 시도한 후 다시 시작하세요.</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">플러그인</string> <string name="mode_decky_plugins_hint">다음 Steam 세션에 사용할 플러그인 ZIP을 설치합니다.</string> <string name="mode_decky_plugin_downloading">다운로드 중…</string> <string name="mode_decky_plugin_install_zip">ZIP에서 설치</string> - <!-- Store --> <string name="store_cat_games">게임</string> <string name="store_cat_audio_video">오디오 및 비디오</string> <string name="store_cat_graphics">그래픽</string> @@ -1361,21 +1320,14 @@ <string name="store_cat_utilities">유틸리티</string> <string name="store_section_popular">인기</string> <string name="store_section_trending">급상승</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">신규</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">업데이트됨</string> <string name="store_section_loading">불러오는 중…</string> <string name="store_flatpak_setup">Flatpak 설정</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s: %2$s</string> <string name="store_updates_check_failed">Flathub에서 업데이트를 확인할 수 없습니다</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">설치됨</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">폴더</string> - <!-- Launcher themes --> <string name="theme_graphite">그래파이트</string> <string name="theme_graphite_detail">그래파이트 색상의 화면에, 선택·포커스·누름 상태에는 로고의 오브 블루를 사용합니다.</string> <string name="theme_paper">블랙 앤 화이트</string> @@ -1384,7 +1336,6 @@ <string name="theme_icon_blue_detail">검은색 배경에 흰색 글자, 선택·포커스·누름 상태에는 파란색을 사용합니다.</string> <string name="theme_electric">일렉트릭 네이비</string> <string name="theme_electric_detail">회색 대신 네이비 톤의 화면에 더 밝은 파란색을 사용합니다.</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Ins</string> <string name="pckb_key_del">Del</string> @@ -1402,22 +1353,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">Space</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">먼저 런타임을 설치하세요</string> <string name="game_sync_err_steam_library">Steam 라이브러리를 사용할 수 없습니다</string> <string name="game_sync_err_previous_sd">이전 SD 라이브러리를 사용할 수 없습니다</string> <string name="game_sync_err_game_storage">게임 저장 공간을 사용할 수 없습니다</string> <string name="game_sync_err_added_games">추가된 게임 폴더를 사용할 수 없습니다</string> <string name="game_sync_err_manifests">Steam 매니페스트를 사용할 수 없습니다</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">다양한 기종</string> - <!-- File manager --> <string name="fm_drive_internal">내부</string> <string name="fm_drive_linux">Linux 시스템</string> <string name="fm_drive_sd">SD 카드</string> @@ -1435,7 +1381,6 @@ <string name="fm_no_favorites">즐겨찾기가 없습니다. 폴더의 ⋮ 메뉴에서 고정하면 여기로 빠르게 이동할 수 있습니다.</string> <string name="fm_unpin_favorite">즐겨찾기에서 제거</string> <string name="fm_size_zero">0바이트</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">바이트</string> <string name="fm_prop_location">위치</string> <string name="fm_prop_type">유형</string> @@ -1443,7 +1388,6 @@ <string name="fm_prop_modified">수정한 날짜</string> <string name="fm_type_folder">폴더</string> <string name="fm_type_file">파일</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">%1$s 파일</string> <string name="fm_read_only">읽기 전용</string> <string name="fm_read_only_hint">게임(및 Wine)이 이 파일을 덮어쓰거나 삭제하지 못하게 합니다.</string> @@ -1457,15 +1401,11 @@ <string name="fm_delete_failed">삭제 실패</string> <string name="fm_op_moving">이동 중...</string> <string name="fm_op_copying">복사 중...</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">이동 중 - %1$s</string> <string name="fm_op_copying_item">복사 중 - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">이동 중 %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">복사 중 %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d개 완료, %2$d개 실패</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d개 완료, %2$d개 건너뜀</string> <plurals name="fm_paste_moved"> <item quantity="other">항목 %1$d개 이동됨</item> @@ -1473,7 +1413,6 @@ <plurals name="fm_paste_copied"> <item quantity="other">항목 %1$d개 복사됨</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">\"%1$s\"이(가) 이미 있습니다</string> <string name="fm_op_renaming">이름 바꾸는 중...</string> <string name="fm_rename_failed">이름 바꾸기 실패</string> @@ -1484,17 +1423,14 @@ <string name="fm_create">만들기</string> <string name="fm_new_name">새 이름</string> <string name="fm_delete_title">삭제하시겠습니까?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">\"%1$s\"을(를) 영구적으로 삭제하시겠습니까?</string> <plurals name="fm_delete_items_title"> <item quantity="other">항목 %1$d개를 삭제하시겠습니까?</item> </plurals> <string name="fm_cannot_undo">이 작업은 되돌릴 수 없습니다.</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="other">…외 %1$d개</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">삭제 중 %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="other">%1$d개를 삭제할 수 없습니다</item> @@ -1508,7 +1444,6 @@ <string name="fm_conflict_skip">건너뛰기</string> <string name="fm_select_this_folder">이 폴더 선택</string> <string name="fm_back">뒤로</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$s이(가) 마운트되었지만 지금은 읽을 수 없습니다</string> <string name="fm_list_view">목록 보기</string> <string name="fm_grid_view">격자 보기</string> @@ -1523,41 +1458,31 @@ <string name="fm_hide_favorites">즐겨찾기 숨기기</string> <string name="fm_show_favorites">즐겨찾기 표시</string> <string name="fm_filter_folder">이 폴더에서 필터</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">%1$s 사용 가능</string> <string name="fm_selected_count">%1$d개 선택됨</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">선택 해제</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">전체</string> <string name="fm_paste">붙여넣기</string> <plurals name="fm_items"> <item quantity="other">항목 %1$d개</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">여기에 %1$s 붙여넣기</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">여기에 %1$s 붙여넣기(이동)</string> <string name="fm_rail_title">파일</string> <string name="fm_rail_storage">저장소</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">바로 가기</string> <string name="fm_rail_favorites">즐겨찾기</string> <string name="fm_rail_downloads">다운로드</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">다운로드</string> <string name="fm_rail_roms">ROMs</string> <string name="fm_rail_session_logs">세션 로그</string> <string name="fm_rail_pictures">사진</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">\"%1$s\"을(를) 즐겨찾기에 추가했습니다</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">\"%1$s\"을(를) 즐겨찾기에서 제거했습니다</string> <string name="fm_empty_directory">빈 폴더</string> <string name="fm_up_one_level">상위 폴더로</string> <string name="fm_choose_folder">폴더 선택</string> <string name="fm_choose_file">파일 선택</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Steam 블루</string> <string name="ctrl_tint_sky">하늘색</string> <string name="ctrl_tint_white">흰색</string> @@ -1566,7 +1491,6 @@ <string name="ctrl_tint_amber">호박색</string> <string name="ctrl_tint_red">빨간색</string> <string name="ctrl_tint_pink">분홍색</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">A 버튼</string> <string name="ctrl_button_b">B 버튼</string> <string name="ctrl_button_x">X 버튼</string> @@ -1577,28 +1501,21 @@ <string name="ctrl_button_rt">오른쪽 트리거</string> <string name="ctrl_button_view">보기 버튼</string> <string name="ctrl_button_menu">메뉴 버튼</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> - <!-- Controller View button --> <string name="ctrl_target_view">보기</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">메뉴</string> <string name="ctrl_target_up">방향 패드 위</string> <string name="ctrl_target_down">방향 패드 아래</string> <string name="ctrl_target_left">방향 패드 왼쪽</string> <string name="ctrl_target_right">방향 패드 오른쪽</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">숨김</string> <string name="ctrl_editor_drag">드래그하여 이동</string> <string name="ctrl_editor_snap">맞춤</string> <string name="ctrl_editor_save">저장</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">없음</string> <string name="second_mode_keyboard_trackpad">키보드 + 터치패드</string> <string name="second_mode_terminal">터미널</string> <string name="second_mode_deck_controls">Deck 그립 + 터치패드</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">Linux 런타임 제거 중</string> <plurals name="rtinst_removing_entries"> <item quantity="other">Linux 런타임 제거 중 · 항목 %1$d개</item> @@ -1613,63 +1530,47 @@ <string name="rtinst_title_installing">Linux 런타임 설치 중</string> <string name="rtinst_title_removing">Linux 런타임 제거 중</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">Linux 런타임이 설치되지 않았습니다</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">카탈로그에 %1$s의 체크섬이 없습니다</string> <string name="deskpkg_checksum_mismatch">체크섬이 일치하지 않습니다. 변경된 내용은 없습니다</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">%1$s 설치 중</string> <string name="deskpkg_place_failed">AppImage를 배치할 수 없습니다</string> <string name="deskpkg_extract_failed">압축 해제 실패</string> <string name="deskpkg_install_failed">설치 실패</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">Flatpak 설정 실패(종료 코드 %1$d)</string> <string name="flatpakmgr_setup_unfinished">Flatpak 설정이 완료되지 않았습니다</string> <string name="flatpakmgr_busy">다른 스토어 작업이 진행 중입니다</string> <string name="flatpakmgr_not_ready">먼저 Flatpak을 설정하세요</string> <string name="flatpakmgr_starting">시작 중</string> <string name="flatpakmgr_exit_status">Flatpak이 종료되었습니다(상태 %1$d)</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">%1$s 설치 중</string> <string name="flatpakmgr_updating">%1$s 업데이트 중</string> <string name="flatpakmgr_removing">%1$s 제거 중</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">%1$s 설치 중(%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">%1$s 업데이트 중(%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">%1$s 제거 중(%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">번역</string> <string name="flatpakmgr_ref_graphics_drivers">그래픽 드라이버</string> <string name="flatpakmgr_ref_media_codecs">미디어 코덱</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">%1$s 런타임</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">%1$s 런타임 %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">파일이 너무 작아 AppImage가 아닙니다</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">파일을 읽을 수 없습니다: %1$s</string> <string name="appimg_not_linux">AppImage가 아닙니다(Linux 프로그램이 아님)</string> <string name="appimg_wrong_arch">이 AppImage는 ARM64(aarch64)용도, x86 PC용도 아닙니다</string> <string name="appimg_not_appimage">Linux 프로그램이지만 AppImage가 아닙니다</string> <string name="appimg_wrong_type">유형 2 AppImage만 가져올 수 있습니다(이 파일은 유형 %1$d)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">사용 가능한 공간 부족: 가져오려면 약 %1$s이(가) 필요합니다</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">사용 가능한 공간 부족: 업데이트하려면 약 %1$s이(가) 필요합니다</string> <string name="appimg_import_failed">가져오기 실패</string> <string name="appimg_gone">AppImage가 더 이상 존재하지 않습니다</string> <string name="appimg_update_failed">업데이트 실패</string> - <!-- %1$s: file name --> <string name="appimg_extracting">%1$s 압축 해제 중</string> <string name="appimg_extract_failed">AppImage의 압축을 풀 수 없습니다</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">AppImage의 압축을 풀 수 없습니다: %1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">Decky Loader를 설치하기 전에 실행 중인 세션을 중지하세요</string> <string name="deckymgr_download_failed">Decky Loader 다운로드 실패</string> <string name="deckymgr_size_mismatch">Decky Loader 크기가 릴리스 정보와 일치하지 않습니다</string> @@ -1705,20 +1606,13 @@ <string name="deckymgr_remote_entry_invalid">잘못된 remote_binary 항목</string> <string name="deckymgr_remote_name_invalid">잘못된 원격 바이너리 이름</string> <string name="deckymgr_remote_url_https">원격 바이너리 URL은 HTTPS를 사용해야 합니다</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">원격 바이너리 %1$s의 SHA-256 해시가 잘못되었습니다</string> <string name="deckymgr_bin_dir_failed">플러그인 bin 폴더를 만들 수 없습니다</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">플러그인 바이너리 다운로드 중 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">플러그인 바이너리 %1$s을(를) 다운로드할 수 없습니다</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">플러그인 바이너리 체크섬 불일치 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">플러그인 바이너리를 실행 가능으로 설정할 수 없습니다 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">플러그인 바이너리를 교체할 수 없습니다 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">플러그인 바이너리를 설치할 수 없습니다 · %1$s</string> <string name="deckymgr_folder_taken">플러그인 폴더를 다른 플러그인이 사용하고 있습니다</string> <string name="deckymgr_preserve_failed">기존 플러그인을 보존할 수 없습니다</string> @@ -1726,20 +1620,14 @@ <string name="deckymgr_settings_failed">Decky 플러그인 설정을 업데이트할 수 없습니다</string> <string name="deckymgr_restore_failed">기존 플러그인을 복원할 수 없습니다</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">무선 디버깅 ID를 저장할 수 없습니다</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">ADB 명령 실패(%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Android에서 하위 프로세스 제한 변경을 확인하지 않았습니다: %1$s</string> <string name="adbfix_no_pairing">저장된 무선 디버깅 페어링이 없습니다</string> <string name="adbfix_enter_address">현재 무선 디버깅의 IP 주소 및 포트를 입력하세요</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">페어링 팝업을 기다리는 시간이 초과되었습니다. DroidDeck에서 다시 시도하세요.</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">페어링 팝업을 찾을 수 없습니다(%1$d). Wi-Fi가 켜져 있는지 확인하세요.</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">페어링 팝업을 찾을 수 없습니다: %1$s</string> <string name="adbpair_six_digits">코드 6자리를 모두 입력하세요</string> <string name="adbpair_pairing">페어링 중…</string> @@ -1753,27 +1641,22 @@ <string name="adbpair_title_pair">무선 디버깅 페어링</string> <string name="adbpair_waiting_text">무선 디버깅을 켠 다음 “페어링 코드로 기기 페어링”을 탭하세요.</string> <string name="adbpair_waiting_long">무선 디버깅을 켠 다음 “페어링 코드로 기기 페어링”을 탭하세요. 코드는 이 알림에 입력하므로 설정 화면에 머물러 있으세요.</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">6자리 페어링 코드</string> <string name="adbpair_code_hint">\"코드 입력\"을 탭하고 설정에 표시된 6자리 숫자를 입력하세요.</string> <string name="adbpair_title_code">Wi-Fi 페어링 코드 입력</string> <string name="adbpair_title_retry">코드를 다시 입력하세요</string> - <!-- Notification action button --> <string name="adbpair_enter_code">코드 입력</string> <string name="adbpair_title_working">Steam 설정 중</string> <string name="adbpair_title_done">Steam 준비 완료</string> <string name="adbpair_done_text">탭하여 DroidDeck으로 돌아가세요. 이제 무선 디버깅을 꺼도 됩니다.</string> <string name="adbpair_title_failed">무선 디버깅 설정이 중단되었습니다</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">다운로드 응답: HTTP %1$d</string> <string name="selfinst_size_mismatch">다운로드 크기가 배포된 APK와 일치하지 않아 삭제했습니다. 다시 시도하세요</string> <string name="selfinst_checksum_mismatch">다운로드한 파일이 체크섬과 일치하지 않아 삭제했습니다. 다시 시도하세요</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">이 업데이트는 %2$s이(가) 아닌 Android 패키지 %1$s용입니다</string> <string name="selfinst_metadata_signer">업데이트 메타데이터에 DroidDeck 릴리스 서명 키가 지정되어 있지 않습니다</string> <string name="selfinst_unreadable">Android에서 다운로드한 APK를 읽을 수 없습니다</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">다운로드한 APK는 %2$s이(가) 아닌 %1$s용입니다</string> <string name="selfinst_version_mismatch">다운로드한 APK의 versionCode가 배포된 %2$d이(가) 아닌 %1$d입니다</string> <string name="selfinst_downgrade">Android에서 설치된 versionCode %2$d 위에 %1$d을(를) 설치하지 않습니다</string> @@ -1781,16 +1664,11 @@ <string name="selfinst_wrong_signer">다운로드한 APK가 DroidDeck 릴리스 키로 서명되지 않았습니다</string> <string name="appupd_http">업데이트 카탈로그 응답: HTTP %1$d</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">세션 시작 중…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">%1$d:%2$02d 경과 · 아직 진행 중</string> <string name="loadstate_steam_update">Steam 클라이언트 업데이트 다운로드 중 · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">Steam 클라이언트 다운로드 중 · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">호환성 도구를 이미 설치하고 있습니다</string> <string name="pextra_stop_session">호환성 도구를 설치하기 전에 실행 중인 세션을 중지하세요</string> <string name="pextra_no_release">%1$s의 ARM64 릴리스를 찾을 수 없습니다</string> @@ -1807,48 +1685,38 @@ <string name="pextra_checksum_mismatch">체크섬이 일치하지 않아 다운로드를 삭제했습니다</string> <string name="pextra_session_started">다운로드 중에 세션이 시작되었습니다. 호환성 도구를 설치하기 전에 세션을 중지하세요</string> <string name="pextra_installing">%1$s 설치 중</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">%1$s 설치 실패(종료 코드 %2$d)</string> <string name="pextra_could_not_install">%1$s을(를) 설치할 수 없습니다</string> <string name="esync_fetching">droiddeck-esync 팩 받는 중</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">Linux 도우미 포함</string> <string name="cmgr_fex_dlls_only">DLL만</string> <string name="cmgr_original_proton_updated">원본(마지막 교체 이후 Proton 업데이트됨)</string> <string name="cmgr_changed_outside">\"구성 요소\" 외부에서 변경됨</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">없음</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">원본 %1$s</string> <string name="cmgr_original_from">%1$s의 원본</string> <string name="cmgr_swap_queued">%3$s을(를) 사용하는 게임이 종료되면 %1$s %2$s(으)로 교체됩니다.</string> <string name="cmgr_swap_done">이제 %3$s에 %1$s %2$s이(가) 적용되었습니다. 다음 게임 실행 시 적용됩니다.</string> <string name="cmgr_original_missing">해당 원본 번들이 저장되어 있지 않습니다</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%2$s의 %1$s은(는) 해당 Proton을 사용하는 게임이 종료되면 원본으로 복원됩니다.</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">%2$s의 %1$s을(를) %3$s 원본으로 복원했습니다.</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%1$s은(는) %2$s에서 사용 중입니다. 먼저 해당 Proton을 다른 항목으로 교체하세요.</string> <string name="cmgr_package_not_stored">%1$s이(가) 저장되어 있지 않습니다</string> <string name="cmgr_deleted">%1$s을(를) 삭제했습니다.</string> <string name="cmgr_original_kept">설치된 빌드의 원본은 되돌리기 위해 유지됩니다.</string> <string name="cmgr_not_stored">저장되지 않음</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">%2$s의 %1$s 원본을 삭제했습니다.</string> <string name="cmgr_nightlies_only">Nightlies 릴리스에서만 다운로드할 수 있습니다</string> <string name="cmgr_list_predates_checksums">이 패키지 목록은 체크섬 도입 이전의 것입니다. 새로 고친 후 다시 시도하세요</string> <string name="cmgr_checksum_mismatch">체크섬 불일치</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">선형</string> <string name="sprefs_upscaler_nearest">최근접</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR(품질)</string> <string name="sprefs_upscaler_sharpen">샤프닝 전용</string> <string name="sprefs_lod_bias_auto">자동(스케일링에 맞춤)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">적용된 효과 없음.</string> <string name="look_game_clarity">게임 선명도</string> <string name="look_game_clarity_desc">샤프닝에 약간의 대비를 더한 일상용 프리셋.</string> @@ -1873,27 +1741,23 @@ <string name="look_anime_edge">애니메 윤곽</string> <string name="look_anime_edge_desc">툰 윤곽선과 샤프닝.</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">Adreno GPU가 아님</string> <string name="gpuinfo_supported">지원됨</string> <string name="gpuinfo_experimental">실험적: 드라이버가 테스트 빌드입니다</string> <string name="gpuinfo_below_tested">테스트된 하드웨어(Adreno 725 이상)보다 낮음: 실행되지 않을 수 있습니다</string> <string name="gpuinfo_unsupported">지원되지 않음: DroidDeck에는 Adreno(Snapdragon) GPU가 필요합니다</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">GitHub 요청 한도에 도달했습니다. 나중에 다시 시도하세요</string> <string name="turnip_github_http">GitHub 응답: HTTP %1$d</string> <string name="turnip_download_http">다운로드 응답: HTTP %1$d</string> <string name="turnip_list_predates_checksums">이 드라이버 목록은 체크섬 도입 이전의 것입니다. 새로 고친 후 다시 시도하세요</string> <string name="turnip_checksum_mismatch">체크섬이 일치하지 않아 다운로드를 삭제했습니다</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">AdrenoTools 드라이버가 아닙니다: %1$s</string> <string name="turnip_reject_no_meta">meta.json 없음</string> <string name="turnip_reject_meta_unreadable">meta.json을 읽을 수 없음(%1$s)</string> <string name="turnip_reject_linux_driver">Linux 런타임 드라이버입니다(\"Linux 런타임 드라이버\"에서 가져오세요)</string> <string name="turnip_reject_wayland">Wayland 게임 드라이버이며, 이 앱에서는 사용하지 않습니다</string> <string name="turnip_reject_no_library_name">meta.json에 libraryName이 지정되지 않아 AdrenoTools에 전달할 수 없습니다</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.json에 지정된 %1$s이(가) zip에 없습니다</string> <string name="turnip_reject_not_aarch64">%1$s은(는) 64비트 AArch64 ELF 공유 라이브러리가 아닙니다</string> <string name="turnip_reject_glibc">%1$s은(는) glibc에 링크되어 있습니다. \"-Linux\" Turnip이므로 \"Linux 런타임 드라이버\"에서 가져오세요</string> @@ -1904,90 +1768,61 @@ <string name="bundle_entry_outside">드라이버 번들이 아닙니다: %1$s이(가) 번들 외부를 가리킵니다</string> <string name="bundle_too_large">드라이버 번들이 아닙니다: 압축을 풀면 %1$d MB를 초과합니다</string> - <!-- Game storage --> <string name="gstore_sd_card">SD 카드</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">폴더</string> <string name="logshare_chooser">세션 로그 공유</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">Android 10 이상 필요(색 공간을 지원하는 디스플레이 레이어)</string> <string name="hdr_none">이 디스플레이는 HDR을 지원하지 않습니다</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">이 디스플레이는 HDR10이 아닌 %1$s 형식을 지원합니다</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">이 GPU</string> <string name="phantom_title_on">하위 프로세스 제한 켜짐</string> <string name="phantom_title_unset">하위 프로세스 제한 설정되지 않음</string> <string name="phantom_title_unreadable">하위 프로세스 제한을 확인할 수 없음</string> <string name="phantom_title_off">하위 프로세스 제한 꺼짐</string> <string name="phantom_title_not_required">하위 프로세스 제한 해당 없음</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s 이 방법을 사용할 수 없으면 이 기기를 ADB가 설치된 PC에 연결하고 아래 명령을 실행하세요. 그런 다음 여기로 돌아와 다시 확인하세요.</string> <string name="phantom_instructions_off">Steam 세션을 시작할 수 있습니다.</string> <string name="phantom_instructions_not_applicable">이 Android 버전에서는 이 제한을 사용하지 않습니다.</string> <string name="phantom_fix_toggle">개발자 옵션에서 “하위 프로세스 제한 사용 중지”를 켜고, 이후에도 개발자 옵션을 켜 둔 상태로 유지하세요.</string> <string name="phantom_fix_no_toggle">이 Android 버전에는 해당 스위치가 없습니다. 무선 디버깅을 사용하여 이 기기에서 변경하세요.</string> <string name="upd_block_no_apk">이 빌드에는 이 DroidDeck 복사본용 다운로드가 없습니다.</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">versionCode %1$d이(가) 설치된 %2$d보다 낮아 Android에서 이 빌드 위에 설치하지 않습니다.</string> <string name="upd_fail_downgrade">Android에서는 최신 버전 위에 이전 버전을 설치할 수 없습니다. 채널이 따라잡을 때까지 현재 빌드를 유지하세요.</string> <string name="upd_fail_signature">이 DroidDeck 복사본은 다른 키로 서명되어 있어(로컬 빌드일 수 있음) Android에서 덮어쓰기 업데이트를 할 수 없습니다.</string> <string name="upd_fail_storage">업데이트에 필요한 저장 공간이 부족합니다.</string> <string name="upd_fail_aborted">업데이트가 취소되었습니다.</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">업데이트가 설치되지 않았습니다: %1$s</string> <string name="upd_fail_generic">업데이트가 설치되지 않았습니다.</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">%1$s zip</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">zip</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$s을(를) 아직 실행한 적이 없습니다</string> <string name="saves_none_found">%1$s의 저장 파일을 찾을 수 없습니다</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$s에 drive_c 폴더가 없으므로 GameHub 또는 Winlator 저장 데이터 zip이 아닙니다</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">%1$s에 폴더를 만들 수 없음</string> <string name="gstore_cannot_write">%1$s에 쓸 수 없음</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">%1$s에 쓸 수 없음(%2$s)</string> <string name="gfiles_folder_unavailable">폴더를 사용할 수 없습니다</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">다른 파일이 이미 %1$s 이름을 사용하고 있습니다</string> <string name="gfiles_art_unavailable">이미지 폴더를 사용할 수 없습니다</string> <string name="gfiles_unrelated_inventory">폴더에 관련 없는 동기화 목록이 있습니다</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">%1$s을(를) 제거할 수 없습니다</string> <string name="gfiles_outside_folder">파일이 내보내기 폴더 밖을 가리킵니다</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX(버전 이름 없는 빌드)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">%1$s에 profile.json이 없음</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$s은(는) Linux 구성 요소 패키지가 아닙니다(유형 %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · %2$s 사용 가능</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">자동</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">업데이트 카탈로그가 검사를 통과하지 못했습니다(%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">DroidDeck 하위 프로세스 설정</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">로그인됨 · 오프라인 모드</string> <string name="setup_signed_in_unnamed">로그인됨</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">내장</string> <string name="driver_tag_downloaded">다운로드됨</string> <string name="driver_tag_imported">가져옴</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">ANDROID + LINUX</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> <string name="display_session">세션 디스플레이</string> <string name="display_resolution">해상도</string> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f93e48b38..cd85930f9 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1141,16 +1141,12 @@ <string name="game_file_exported">游戏文件已保存到 %1$s</string> <string name="game_frontend_files">前端文件</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">请等待运行时操作完成</string> <string name="main_wait_proton_install">请等待兼容性工具安装完成</string> <string name="main_wait_desktop_install">请等待桌面应用安装完成</string> <string name="main_wait_install_game">请等待安装完成后再启动游戏</string> <string name="main_saves_close_session">请先关闭 Steam 会话,以免游戏存档覆盖导入的内容</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">正在导入到 %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="other">已从 %2$s 导入 %1$d 个文件到 %3$s</item> </plurals> @@ -1158,15 +1154,11 @@ <item quantity="other">已从 %2$s 导入 %1$d 个文件到 %3$s。旧存档已备份到 Download/DroidDeck/Saves/backups</item> </plurals> <string name="main_saves_pick_zip">为 %1$s 选择存档 zip 包</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">正在导出 %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="other">已将 %1$d 个文件导出为 %2$s:%3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">选择 %1$s 的保存位置(%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">%1$s失败:%2$s</string> <string name="main_no_logs_yet">还没有会话日志:请先运行一次会话。</string> <string name="main_no_logs_to_clear">没有可清除的会话日志。</string> @@ -1179,68 +1171,55 @@ <string name="main_adb_copied">已复制 ADB 命令</string> <string name="main_pick_roms">选择 ROM 文件夹</string> <string name="main_non_adreno_title">不是 Adreno GPU</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">Turnip 支持 Adreno GPU。在 %1$s 上,Steam 可能显示黑屏。需下载:%2$.0f MB。</string> <string name="main_install_anyway">仍要安装</string> <string name="main_remove_runtime_title">移除 Linux 运行时</string> <string name="main_remove_runtime_text">这会删除运行时、其中的 Steam 客户端,以及安装在其中的所有游戏。</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">已应用:%1$s</string> <string name="android_app_open_failed_primary">无法在主屏幕上打开 %1$s</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">无法在 %2$s 上打开 %1$s</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">无法在显示器 %2$d 上打开 %1$s</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">正在导入</string> <string name="comp_busy_swapping">正在替换</string> <string name="comp_busy_restoring">正在还原</string> <string name="comp_busy_cancelling">正在取消</string> <string name="comp_busy_deleting">正在删除</string> <string name="comp_cannot_read">无法读取该文件</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">已导入 %1$s</string> <string name="comp_swap_cancelled">等待中的替换已取消。</string> <string name="comp_pick_wcp">选择组件包(-linux .wcp)</string> <string name="comp_nightlies_unreachable">无法连接 Nightlies</string> - <!-- %1$s is the package version --> <string name="comp_stored">已存储 %1$s</string> <string name="comp_download_failed">下载失败:%1$s</string> <string name="drivers_pick_linux">选择 Linux 运行时驱动(-Linux zip 包或 Android + Linux 组合包)</string> <string name="drivers_pick_android">选择显示驱动(AdrenoTools zip 包或 Android + Linux 组合包)</string> <string name="drivers_pick_any">选择驱动 zip 包或 Android + Linux 组合包</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s:%2$s</string> <string name="main_removing_named">正在移除 %1$s…</string> <string name="main_forgetting_named">正在移除 %1$s…</string> <string name="main_pick_game_storage">选择游戏存储文件夹</string> <string name="main_pick_decky_zip">选择 Decky 插件 ZIP 包</string> <string name="main_storage_not_usable">无法使用:%1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">程序</string> <string name="main_removing_runtime">正在移除 Linux 运行时</string> <string name="main_settings_main_screen">无法在底部屏幕上打开设置,已改为在主屏幕上打开。</string> <string name="main_open_dev_options">在设置中打开开发者选项</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">无法打开 Android 的“安装未知应用”设置</string> <string name="upd_couldnt_check">无法检查:%1$s</string> <string name="upd_no_download">此构建没有适用于此 DroidDeck 副本的下载</string> <string name="upd_stop_first">请先停止正在运行的会话,再进行更新</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">正在下载</string> <string name="upd_stage_installing">正在安装</string> <string name="upd_stage_confirm">等待你确认</string> <string name="upd_failed">更新失败</string> <string name="upd_not_offered">Android 未提供安装此更新的选项</string> - <!-- Decky --> <string name="decky_install_failed">Decky 安装失败:%1$s</string> <string name="decky_plugin_starting">正在开始导入插件…</string> <string name="decky_plugin_failed">插件导入失败:%1$s</string> <string name="decky_plugin_installed">插件已安装。重启 Steam 即可加载。</string> - <!-- GPU drivers --> <string name="drivers_not_checked">尚未检查 — 点按刷新以查找新驱动</string> <string name="drivers_bundle_sets_display">Android + Linux:同时设置显示驱动</string> <string name="drivers_bundle_sets_runtime">Android + Linux:同时设置运行时驱动</string> @@ -1248,16 +1227,13 @@ <string name="drivers_system_vulkan">系统 Vulkan:没有适用于此 GPU 的内置构建</string> <string name="drivers_bundled">%1$s(内置)</string> <string name="drivers_manual">手动</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">目前只发布了其中一半</string> <string name="drivers_pair_size">两者共 %1$.0f MB</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">没有可设置的驱动:%1$s</string> <string name="drivers_checking_latest">正在检查最新驱动…</string> <string name="drivers_up_to_date_check_failed">已是最新(无法检查:%1$s)</string> <string name="drivers_recommended_unpublished">推荐的驱动目前尚未发布</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">正在下载 %1$s %2$s…</string> <string name="drivers_updated_to">已更新到 %1$s %2$s。</string> <string name="drivers_updated_to_restart">已更新到 %1$s %2$s。显示驱动将在 DroidDeck 重启后生效。</string> @@ -1272,9 +1248,7 @@ <string name="drivers_using_bundle_restart">正在将 %1$s 同时用作运行时驱动和显示驱动。显示驱动将在 DroidDeck 重启后生效。</string> <string name="drivers_deleted_bundle">已删除 %1$s(两个驱动)</string> <string name="drivers_deleted">已删除 %1$s</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">最新:%1$s · %2$s检查</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">最新:%1$s · 无法连接 %2$s · %3$s检查</string> <plurals name="drivers_ago_minutes"> <item quantity="other">%1$d 分钟前</item> @@ -1287,33 +1261,25 @@ </plurals> <string name="drivers_checking_sources">正在检查 Banners-Turnip、WinNative 和 DroidDeck…</string> <string name="drivers_check_failed">检查失败</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">已安装 %1$s — 在任一列表中选择它即可同时设置两个驱动</string> <string name="drivers_installed_pick">已安装 %1$s — 请在菜单中选择它</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">请等待运行时移除完成</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">游戏</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">触控板</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">直接</string> <string name="session_swap_failed">替换失败:%1$s</string> <string name="session_profile_save_failed">配置档案保存失败:%1$s</string> <string name="session_desktop_paused">桌面已暂停</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$s 已暂停</string> <string name="session_program_paused">程序已暂停</string> <string name="session_starting_desktop">正在启动桌面</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">正在启动 %1$s</string> <string name="session_starting_program">正在启动程序</string> <string name="session_no_logs">此会话没有日志。</string> <string name="session_logs_archive_failed">无法创建会话日志压缩包。</string> <string name="session_logs_share_failed">无法分享会话日志。</string> <string name="session_starting_session">正在启动会话…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">正在下载 Linux 运行时</string> <string name="session_runtime_downloading_mb">正在下载 Linux 运行时 · %1$d / %2$d MB</string> <string name="session_runtime_checking">正在检查 Linux 运行时</string> @@ -1328,27 +1294,20 @@ <string name="session_proton_unpacking">正在解包 Proton Experimental(ARM64)</string> <string name="session_runtime_catalog_unreachable">无法连接运行时目录。请检查网络连接,然后再次点“游玩”。</string> <string name="session_runtime_install_failed">Linux 运行时未能安装。请在“配置”中检查运行时,然后再次点“游玩”。</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">桌面未能安装(%1$s)。请检查网络连接,然后再次点“桌面”。</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$s 意外停止。请将日志随错误报告一起分享,或重试。</string> <string name="session_ended_program">程序意外停止。请将日志随错误报告一起分享,或重试。</string> <string name="session_ended_desktop">桌面意外停止。请将日志随错误报告一起分享,或重试。</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">退出状态 %1$d · %2$s</string> <string name="session_ended_no_log">无日志</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="other">日志中有 %1$d 个“Function not implemented”错误:proot 的 seccomp 加速在此设备上导致某个辅助进程失败。请尝试 %2$s → “%3$s”(或“%4$s”),然后重新启动。</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">插件</string> <string name="mode_decky_plugins_hint">安装插件 ZIP 包,将在下次 Steam 会话中生效。</string> <string name="mode_decky_plugin_downloading">正在下载…</string> <string name="mode_decky_plugin_install_zip">从 ZIP 安装</string> - <!-- Store --> <string name="store_cat_games">游戏</string> <string name="store_cat_audio_video">影音</string> <string name="store_cat_graphics">图形</string> @@ -1361,21 +1320,14 @@ <string name="store_cat_utilities">实用工具</string> <string name="store_section_popular">热门</string> <string name="store_section_trending">趋势</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">新上架</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">最近更新</string> <string name="store_section_loading">加载中…</string> <string name="store_flatpak_setup">Flatpak 配置</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s:%2$s</string> <string name="store_updates_check_failed">无法从 Flathub 检查更新</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">已安装</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">文件夹</string> - <!-- Launcher themes --> <string name="theme_graphite">石墨</string> <string name="theme_graphite_detail">石墨色界面,选中、聚焦或按下的元素均使用标志自带的球形蓝色。</string> <string name="theme_paper">黑底白纸</string> @@ -1384,7 +1336,6 @@ <string name="theme_icon_blue_detail">黑色背景、白色文字,选中、聚焦或按下的元素使用蓝色。</string> <string name="theme_electric">电光藏青</string> <string name="theme_electric_detail">更明亮的蓝色,配以偏藏青而非灰色的界面。</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Ins</string> <string name="pckb_key_del">Del</string> @@ -1402,22 +1353,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">空格</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">请先安装运行时</string> <string name="game_sync_err_steam_library">Steam 库不可用</string> <string name="game_sync_err_previous_sd">之前的 SD 卡库不可用</string> <string name="game_sync_err_game_storage">游戏存储不可用</string> <string name="game_sync_err_added_games">已添加游戏的文件夹不可用</string> <string name="game_sync_err_manifests">Steam 清单文件不可用</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">多种平台</string> - <!-- File manager --> <string name="fm_drive_internal">内部存储</string> <string name="fm_drive_linux">Linux 系统</string> <string name="fm_drive_sd">SD 卡</string> @@ -1435,7 +1381,6 @@ <string name="fm_no_favorites">还没有收藏 — 用文件夹的 ⋮ 菜单固定它,之后即可快速跳回。</string> <string name="fm_unpin_favorite">从收藏夹移除</string> <string name="fm_size_zero">0 字节</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">字节</string> <string name="fm_prop_location">位置</string> <string name="fm_prop_type">类型</string> @@ -1443,7 +1388,6 @@ <string name="fm_prop_modified">修改时间</string> <string name="fm_type_folder">文件夹</string> <string name="fm_type_file">文件</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">%1$s 文件</string> <string name="fm_read_only">只读</string> <string name="fm_read_only_hint">防止游戏(及 Wine)覆盖或删除此文件。</string> @@ -1457,15 +1401,11 @@ <string name="fm_delete_failed">删除失败</string> <string name="fm_op_moving">正在移动…</string> <string name="fm_op_copying">正在复制…</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">正在移动 - %1$s</string> <string name="fm_op_copying_item">正在复制 - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">正在移动 %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">正在复制 %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d 项完成,%2$d 项失败</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d 项完成,%2$d 项已跳过</string> <plurals name="fm_paste_moved"> <item quantity="other">已移动 %1$d 项</item> @@ -1473,7 +1413,6 @@ <plurals name="fm_paste_copied"> <item quantity="other">已复制 %1$d 项</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">“%1$s”已存在</string> <string name="fm_op_renaming">正在重命名…</string> <string name="fm_rename_failed">重命名失败</string> @@ -1484,17 +1423,14 @@ <string name="fm_create">创建</string> <string name="fm_new_name">新名称</string> <string name="fm_delete_title">删除?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">要永久删除“%1$s”吗?</string> <plurals name="fm_delete_items_title"> <item quantity="other">删除 %1$d 项?</item> </plurals> <string name="fm_cannot_undo">此操作无法撤销。</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="other">…以及其他 %1$d 项</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">正在删除 %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="other">%1$d 项无法删除</item> @@ -1508,7 +1444,6 @@ <string name="fm_conflict_skip">跳过</string> <string name="fm_select_this_folder">选择此文件夹</string> <string name="fm_back">返回</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$s 已挂载,但当前无法读取</string> <string name="fm_list_view">列表视图</string> <string name="fm_grid_view">网格视图</string> @@ -1523,41 +1458,31 @@ <string name="fm_hide_favorites">隐藏收藏夹</string> <string name="fm_show_favorites">显示收藏夹</string> <string name="fm_filter_folder">筛选此文件夹</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">可用 %1$s</string> <string name="fm_selected_count">已选择 %1$d 项</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">不选</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">全选</string> <string name="fm_paste">粘贴</string> <plurals name="fm_items"> <item quantity="other">%1$d 项</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">将 %1$s 粘贴到此处</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">将 %1$s 移动到此处</string> <string name="fm_rail_title">文件</string> <string name="fm_rail_storage">存储</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">快速访问</string> <string name="fm_rail_favorites">收藏夹</string> <string name="fm_rail_downloads">下载</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">下载</string> <string name="fm_rail_roms">ROM</string> <string name="fm_rail_session_logs">会话日志</string> <string name="fm_rail_pictures">图片</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">已将“%1$s”添加到收藏夹</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">已将“%1$s”从收藏夹移除</string> <string name="fm_empty_directory">空文件夹</string> <string name="fm_up_one_level">返回上一级</string> <string name="fm_choose_folder">选择文件夹</string> <string name="fm_choose_file">选择文件</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Steam 蓝</string> <string name="ctrl_tint_sky">天蓝</string> <string name="ctrl_tint_white">白色</string> @@ -1566,7 +1491,6 @@ <string name="ctrl_tint_amber">琥珀</string> <string name="ctrl_tint_red">红色</string> <string name="ctrl_tint_pink">粉色</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">A 键</string> <string name="ctrl_button_b">B 键</string> <string name="ctrl_button_x">X 键</string> @@ -1577,28 +1501,21 @@ <string name="ctrl_button_rt">右扳机</string> <string name="ctrl_button_view">视图键</string> <string name="ctrl_button_menu">菜单键</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> - <!-- Controller View button --> <string name="ctrl_target_view">视图</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">菜单</string> <string name="ctrl_target_up">方向键上</string> <string name="ctrl_target_down">方向键下</string> <string name="ctrl_target_left">方向键左</string> <string name="ctrl_target_right">方向键右</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">隐藏</string> <string name="ctrl_editor_drag">拖动以移动</string> <string name="ctrl_editor_snap">对齐</string> <string name="ctrl_editor_save">保存</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">无</string> <string name="second_mode_keyboard_trackpad">键盘 + 触控板</string> <string name="second_mode_terminal">终端</string> <string name="second_mode_deck_controls">Deck 背键 + 触控板</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">正在移除 Linux 运行时</string> <plurals name="rtinst_removing_entries"> <item quantity="other">正在移除 Linux 运行时 · %1$d 项</item> @@ -1613,63 +1530,47 @@ <string name="rtinst_title_installing">正在安装 Linux 运行时</string> <string name="rtinst_title_removing">正在移除 Linux 运行时</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">未安装 Linux 运行时</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">目录中没有 %1$s 的校验和</string> <string name="deskpkg_checksum_mismatch">校验和不匹配 — 未做任何更改</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">正在安装 %1$s</string> <string name="deskpkg_place_failed">无法放置 AppImage</string> <string name="deskpkg_extract_failed">解压失败</string> <string name="deskpkg_install_failed">安装失败</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">Flatpak 配置失败(退出代码 %1$d)</string> <string name="flatpakmgr_setup_unfinished">Flatpak 配置未完成</string> <string name="flatpakmgr_busy">另一个商店任务正在运行</string> <string name="flatpakmgr_not_ready">请先配置 Flatpak</string> <string name="flatpakmgr_starting">启动中</string> <string name="flatpakmgr_exit_status">Flatpak 已退出,状态码 %1$d</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">正在安装 %1$s</string> <string name="flatpakmgr_updating">正在更新 %1$s</string> <string name="flatpakmgr_removing">正在移除 %1$s</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">正在安装 %1$s(%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">正在更新 %1$s(%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">正在移除 %1$s(%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">翻译</string> <string name="flatpakmgr_ref_graphics_drivers">图形驱动</string> <string name="flatpakmgr_ref_media_codecs">媒体编解码器</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">%1$s 运行时</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">%1$s 运行时 %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">文件太小,不可能是 AppImage</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">无法读取文件:%1$s</string> <string name="appimg_not_linux">这不是 AppImage(不是 Linux 程序)</string> <string name="appimg_wrong_arch">此 AppImage 既不是为 ARM64(aarch64)构建,也不是为 x86 PC 构建</string> <string name="appimg_not_appimage">这是 Linux 程序,但不是 AppImage</string> <string name="appimg_wrong_type">只能导入类型 2 的 AppImage(此文件为类型 %1$d)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">可用空间不足:导入约需 %1$s</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">可用空间不足:更新约需 %1$s</string> <string name="appimg_import_failed">导入失败</string> <string name="appimg_gone">该 AppImage 已不存在</string> <string name="appimg_update_failed">更新失败</string> - <!-- %1$s: file name --> <string name="appimg_extracting">正在解压 %1$s</string> <string name="appimg_extract_failed">无法解压该 AppImage</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">无法解压该 AppImage:%1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">安装 Decky Loader 前请先停止当前会话</string> <string name="deckymgr_download_failed">Decky Loader 下载失败</string> <string name="deckymgr_size_mismatch">Decky Loader 的大小与发布元数据不符</string> @@ -1705,20 +1606,13 @@ <string name="deckymgr_remote_entry_invalid">无效的 remote_binary 条目</string> <string name="deckymgr_remote_name_invalid">无效的远程二进制文件名</string> <string name="deckymgr_remote_url_https">远程二进制文件的 URL 必须使用 HTTPS</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">远程二进制文件 %1$s 的 SHA-256 哈希无效</string> <string name="deckymgr_bin_dir_failed">无法创建插件 bin 目录</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">正在下载插件二进制文件 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">无法下载插件二进制文件 %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">插件二进制文件校验和不匹配 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">无法将插件二进制文件设为可执行 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">无法替换插件二进制文件 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">无法安装插件二进制文件 · %1$s</string> <string name="deckymgr_folder_taken">该插件文件夹属于另一个插件</string> <string name="deckymgr_preserve_failed">无法保留现有插件</string> @@ -1726,20 +1620,14 @@ <string name="deckymgr_settings_failed">无法更新 Decky 插件设置</string> <string name="deckymgr_restore_failed">无法恢复现有插件</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">无法保存无线调试身份信息</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">ADB 命令执行失败(%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Android 未确认子进程限制已更改:%1$s</string> <string name="adbfix_no_pairing">未保存任何无线调试配对</string> <string name="adbfix_enter_address">请输入当前的无线调试 IP 地址与端口</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">等待配对弹窗超时。请从 DroidDeck 重试。</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">无法查找配对弹窗(%1$d)。请检查 Wi-Fi 是否已开启。</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">无法查找配对弹窗:%1$s</string> <string name="adbpair_six_digits">请输入完整的 6 位配对码</string> <string name="adbpair_pairing">正在配对…</string> @@ -1753,27 +1641,22 @@ <string name="adbpair_title_pair">配对无线调试</string> <string name="adbpair_waiting_text">开启无线调试,然后点按“使用配对码配对设备”。</string> <string name="adbpair_waiting_long">开启无线调试,然后点按“使用配对码配对设备”。请停留在“设置”中:配对码在此通知中输入。</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">6 位配对码</string> <string name="adbpair_code_hint">点按“输入配对码”,然后输入“设置”中显示的 6 位数字。</string> <string name="adbpair_title_code">输入 Wi-Fi 配对码</string> <string name="adbpair_title_retry">请重新输入配对码</string> - <!-- Notification action button --> <string name="adbpair_enter_code">输入配对码</string> <string name="adbpair_title_working">正在配置 Steam</string> <string name="adbpair_title_done">Steam 已就绪</string> <string name="adbpair_done_text">点按即可返回 DroidDeck。现在可以关闭无线调试了。</string> <string name="adbpair_title_failed">无线调试配置已中止</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">下载服务器返回 HTTP %1$d</string> <string name="selfinst_size_mismatch">下载的文件大小与发布的 APK 不符,已丢弃 — 请重试</string> <string name="selfinst_checksum_mismatch">下载的文件与校验和不符,已丢弃 — 请重试</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">此更新适用于 Android 软件包 %1$s,而不是 %2$s</string> <string name="selfinst_metadata_signer">更新元数据中未指明 DroidDeck 的发布签名密钥</string> <string name="selfinst_unreadable">Android 无法读取下载的 APK</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">下载的 APK 适用于 %1$s,而不是 %2$s</string> <string name="selfinst_version_mismatch">下载的 APK 的 versionCode 为 %1$d,而不是已发布的 %2$d</string> <string name="selfinst_downgrade">Android 不会用 versionCode %1$d 覆盖已安装的 %2$d</string> @@ -1781,16 +1664,11 @@ <string name="selfinst_wrong_signer">下载的 APK 未使用 DroidDeck 的发布密钥签名</string> <string name="appupd_http">更新目录返回 HTTP %1$d</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">正在启动会话…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">已用时 %1$d:%2$02d · 仍在处理</string> <string name="loadstate_steam_update">正在下载 Steam 客户端更新 · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">正在下载 Steam 客户端 · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">已有兼容性工具正在安装</string> <string name="pextra_stop_session">安装兼容性工具前请先停止当前会话</string> <string name="pextra_no_release">找不到 %1$s 的 ARM64 发布版本</string> @@ -1807,48 +1685,38 @@ <string name="pextra_checksum_mismatch">校验和不匹配;已删除下载的文件</string> <string name="pextra_session_started">下载期间有会话启动;请先停止它,再安装兼容性工具</string> <string name="pextra_installing">正在安装 %1$s</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">%1$s 安装失败(退出代码 %2$d)</string> <string name="pextra_could_not_install">无法安装 %1$s</string> <string name="esync_fetching">正在获取 droiddeck-esync 包</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">含 Linux 辅助组件</string> <string name="cmgr_fex_dlls_only">仅 DLL</string> <string name="cmgr_original_proton_updated">原件(上次替换后 Proton 已更新)</string> <string name="cmgr_changed_outside">已在“组件”之外被更改</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">不存在</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">原件 %1$s</string> <string name="cmgr_original_from">原件来自 %1$s</string> <string name="cmgr_swap_queued">%1$s %2$s 将在 %3$s 上运行的游戏关闭后放入其中。</string> <string name="cmgr_swap_done">%1$s %2$s 已放入 %3$s,将在下次启动游戏时生效。</string> <string name="cmgr_original_missing">未存储该原件包</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%2$s 的 %1$s 将在其上运行的游戏关闭后恢复为原件。</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">已从 %3$s 原件还原 %2$s 的 %1$s。</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%1$s 正在 %2$s 中使用。请先将该 Proton 替换为其他版本。</string> <string name="cmgr_package_not_stored">%1$s 未存储</string> <string name="cmgr_deleted">已删除 %1$s。</string> <string name="cmgr_original_kept">已安装构建的原件会保留:它是还原的途径。</string> <string name="cmgr_not_stored">未存储</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">已删除 %2$s 的 %1$s 原件。</string> <string name="cmgr_nightlies_only">只能从 Nightlies 发布版本下载</string> <string name="cmgr_list_predates_checksums">此软件包列表早于校验和功能 — 请刷新后重试</string> <string name="cmgr_checksum_mismatch">校验和不匹配</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">线性</string> <string name="sprefs_upscaler_nearest">最近邻</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR(质量)</string> <string name="sprefs_upscaler_sharpen">仅锐化</string> <string name="sprefs_lod_bias_auto">自动(匹配缩放)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">不应用任何效果。</string> <string name="look_game_clarity">游戏清晰</string> <string name="look_game_clarity_desc">锐化并略增对比度 — 适合日常使用。</string> @@ -1873,27 +1741,23 @@ <string name="look_anime_edge">动漫描边</string> <string name="look_anime_edge_desc">卡通描边加锐化。</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">不是 Adreno GPU</string> <string name="gpuinfo_supported">受支持</string> <string name="gpuinfo_experimental">实验性:其驱动为测试构建</string> <string name="gpuinfo_below_tested">低于已测试的硬件(Adreno 725 及更新):可能无法运行</string> <string name="gpuinfo_unsupported">不受支持:DroidDeck 需要 Adreno(骁龙)GPU</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">已达到 GitHub 的请求频率限制 — 请稍后重试</string> <string name="turnip_github_http">GitHub 返回 HTTP %1$d</string> <string name="turnip_download_http">下载返回 HTTP %1$d</string> <string name="turnip_list_predates_checksums">此驱动列表早于校验和功能 — 请刷新后重试</string> <string name="turnip_checksum_mismatch">校验和不匹配 — 已丢弃下载的文件</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">不是 AdrenoTools 驱动:%1$s</string> <string name="turnip_reject_no_meta">缺少 meta.json</string> <string name="turnip_reject_meta_unreadable">无法读取 meta.json(%1$s)</string> <string name="turnip_reject_linux_driver">这是 Linux 运行时驱动(请在“运行时驱动”下导入)</string> <string name="turnip_reject_wayland">这是 Wayland 游戏驱动,本应用用不到它</string> <string name="turnip_reject_no_library_name">meta.json 未指定 libraryName,因此无法交给 AdrenoTools</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.json 指定了 %1$s,但它不在 zip 包中</string> <string name="turnip_reject_not_aarch64">%1$s 不是 64 位 AArch64 ELF 共享库</string> <string name="turnip_reject_glibc">%1$s 链接了 glibc — 这是“-Linux”Turnip,应在“运行时驱动”下导入</string> @@ -1904,90 +1768,61 @@ <string name="bundle_entry_outside">不是驱动组合包:%1$s 指向包外</string> <string name="bundle_too_large">不是驱动组合包:解包后超过 %1$d MB</string> - <!-- Game storage --> <string name="gstore_sd_card">SD 卡</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">文件夹</string> <string name="logshare_chooser">分享会话日志</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">需要 Android 10 或更高版本(支持色彩空间的显示图层)</string> <string name="hdr_none">此显示器未报告支持 HDR</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">此显示器支持 %1$s,但不支持 HDR10</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">此 GPU</string> <string name="phantom_title_on">子进程限制已开启</string> <string name="phantom_title_unset">子进程限制未设置</string> <string name="phantom_title_unreadable">无法检查子进程限制</string> <string name="phantom_title_off">子进程限制已关闭</string> <string name="phantom_title_not_required">无需更改子进程限制</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s 如果无法做到,请将此设备连接到装有 ADB 的电脑并运行下方命令,然后返回此处重新检查。</string> <string name="phantom_instructions_off">可以启动 Steam 会话了。</string> <string name="phantom_instructions_not_applicable">此 Android 版本不使用该限制。</string> <string name="phantom_fix_toggle">在开发者选项中打开“禁用子进程限制”,之后请保持开发者选项开启。</string> <string name="phantom_fix_no_toggle">此 Android 版本没有该开关。请使用无线调试在此设备上更改。</string> <string name="upd_block_no_apk">此构建没有适用于此 DroidDeck 副本的下载。</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">Android 不会用它覆盖此构建,因为它的 versionCode %1$d 低于已安装的 %2$d。</string> <string name="upd_fail_downgrade">Android 不会用旧版本覆盖新版本。请继续使用此构建,直到该通道的版本跟上。</string> <string name="upd_fail_signature">此 DroidDeck 副本的签名不同(可能是本地构建?),因此 Android 无法就地更新。</string> <string name="upd_fail_storage">可用空间不足,无法更新。</string> <string name="upd_fail_aborted">更新已取消。</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">更新未能安装:%1$s</string> <string name="upd_fail_generic">更新未能安装。</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">%1$s zip 包</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">zip 包</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$s 尚未启动过</string> <string name="saves_none_found">未找到 %1$s 的存档文件</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$s 中没有 drive_c 文件夹,因此不是 GameHub 或 Winlator 存档 zip 包</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">无法在 %1$s 中创建文件夹</string> <string name="gstore_cannot_write">无法写入 %1$s</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">无法写入 %1$s(%2$s)</string> <string name="gfiles_folder_unavailable">文件夹不可用</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">已有另一个文件使用 %1$s</string> <string name="gfiles_art_unavailable">美术图文件夹不可用</string> <string name="gfiles_unrelated_inventory">文件夹中有无关的同步清单</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">无法移除 %1$s</string> <string name="gfiles_outside_folder">文件超出了导出文件夹</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX(无版本名称的构建)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">%1$s 中没有 profile.json</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$s 不是 Linux 组件包(类型为 %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · 可用 %2$s</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">自动</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">更新目录未通过校验(%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">DroidDeck 子进程设置</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">已登录 · 离线模式</string> <string name="setup_signed_in_unnamed">已登录</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">内置</string> <string name="driver_tag_downloaded">已下载</string> <string name="driver_tag_imported">已导入</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">Android + Linux</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s。%2$s\n\n%3$s</string> <string name="display_session">会话显示</string> <string name="display_resolution">分辨率</string> diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 226b0f500..a871f960a 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1141,16 +1141,12 @@ <string name="game_file_exported">遊戲檔案已儲存至 %1$s</string> <string name="game_frontend_files">前端檔案</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">請等待運行環境操作完成</string> <string name="main_wait_proton_install">請等待兼容性工具安裝完成</string> <string name="main_wait_desktop_install">請等待桌面應用程式安裝完成</string> <string name="main_wait_install_game">請等待安裝完成後再啟動遊戲</string> <string name="main_saves_close_session">請先關閉 Steam 會話,以免遊戲存檔覆蓋匯入的內容</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">正在匯入至 %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="other">已從 %2$s 匯入 %1$d 個檔案至 %3$s</item> </plurals> @@ -1158,15 +1154,11 @@ <item quantity="other">已從 %2$s 匯入 %1$d 個檔案至 %3$s。舊存檔已備份至 Download/DroidDeck/Saves/backups</item> </plurals> <string name="main_saves_pick_zip">為 %1$s 選擇存檔 zip</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">正在匯出 %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="other">已將 %1$d 個檔案匯出為 %2$s:%3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">選擇 %1$s 的儲存位置(%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">操作失敗(%1$s):%2$s</string> <string name="main_no_logs_yet">尚未有會話記錄:請先執行一次會話。</string> <string name="main_no_logs_to_clear">沒有可清除的會話記錄。</string> @@ -1179,68 +1171,55 @@ <string name="main_adb_copied">已複製 ADB 指令</string> <string name="main_pick_roms">選擇 ROM 資料夾</string> <string name="main_non_adreno_title">不是 Adreno GPU</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">Turnip 支援 Adreno GPU。在 %1$s 上,Steam 可能會出現黑畫面。下載大小:%2$.0f MB。</string> <string name="main_install_anyway">仍要安裝</string> <string name="main_remove_runtime_title">移除 Linux 運行環境</string> <string name="main_remove_runtime_text">此操作會刪除運行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">已套用:%1$s</string> <string name="android_app_open_failed_primary">無法在主螢幕開啟 %1$s</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">無法在 %2$s 開啟 %1$s</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">無法在顯示器 %2$d 開啟 %1$s</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">正在匯入</string> <string name="comp_busy_swapping">正在替換</string> <string name="comp_busy_restoring">正在還原</string> <string name="comp_busy_cancelling">正在取消</string> <string name="comp_busy_deleting">正在刪除</string> <string name="comp_cannot_read">無法讀取檔案</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">已匯入 %1$s</string> <string name="comp_swap_cancelled">已取消等待中的替換。</string> <string name="comp_pick_wcp">選擇元件套件(-linux .wcp)</string> <string name="comp_nightlies_unreachable">無法連接 Nightlies</string> - <!-- %1$s is the package version --> <string name="comp_stored">已儲存 %1$s</string> <string name="comp_download_failed">下載失敗:%1$s</string> <string name="drivers_pick_linux">選擇 Linux 運行環境驅動程式(-Linux zip 或 Android + Linux 組合包)</string> <string name="drivers_pick_android">選擇顯示驅動程式(AdrenoTools zip 或 Android + Linux 組合包)</string> <string name="drivers_pick_any">選擇驅動程式 zip 或 Android + Linux 組合包</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s:%2$s</string> <string name="main_removing_named">正在移除 %1$s…</string> <string name="main_forgetting_named">正在忘記 %1$s…</string> <string name="main_pick_game_storage">選擇遊戲儲存資料夾</string> <string name="main_pick_decky_zip">選擇 Decky 插件 ZIP</string> <string name="main_storage_not_usable">無法使用:%1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">程式</string> <string name="main_removing_runtime">正在移除 Linux 運行環境</string> <string name="main_settings_main_screen">無法在下方螢幕開啟「設定」,已改為在主螢幕開啟。</string> <string name="main_open_dev_options">在「設定」中開啟開發人員選項</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">無法開啟 Android 的「安裝不明應用程式」設定</string> <string name="upd_couldnt_check">無法檢查:%1$s</string> <string name="upd_no_download">此版本沒有適用於此 DroidDeck 版本的下載</string> <string name="upd_stop_first">請先停止正在執行的會話,然後再更新</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">正在下載</string> <string name="upd_stage_installing">正在安裝</string> <string name="upd_stage_confirm">正在等待你確認</string> <string name="upd_failed">更新失敗</string> <string name="upd_not_offered">Android 沒有提供安裝更新的選項</string> - <!-- Decky --> <string name="decky_install_failed">Decky 安裝失敗:%1$s</string> <string name="decky_plugin_starting">正在開始匯入插件…</string> <string name="decky_plugin_failed">插件匯入失敗:%1$s</string> <string name="decky_plugin_installed">已安裝插件。請重新啟動 Steam 以載入。</string> - <!-- GPU drivers --> <string name="drivers_not_checked">尚未檢查 - 輕按重新整理以尋找新驅動程式</string> <string name="drivers_bundle_sets_display">Android + Linux:同時設定顯示驅動程式</string> <string name="drivers_bundle_sets_runtime">Android + Linux:同時設定運行環境驅動程式</string> @@ -1248,16 +1227,13 @@ <string name="drivers_system_vulkan">系統 Vulkan:沒有適用於此 GPU 的內置版本</string> <string name="drivers_bundled">%1$s(內置)</string> <string name="drivers_manual">手動</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">目前只發佈了其中一半</string> <string name="drivers_pair_size">兩者合共 %1$.0f MB</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">沒有可設定的驅動程式:%1$s</string> <string name="drivers_checking_latest">正在檢查最新驅動程式…</string> <string name="drivers_up_to_date_check_failed">已是最新(無法檢查:%1$s)</string> <string name="drivers_recommended_unpublished">建議的驅動程式目前尚未發佈</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">正在下載 %1$s %2$s…</string> <string name="drivers_updated_to">已更新至 %1$s %2$s。</string> <string name="drivers_updated_to_restart">已更新至 %1$s %2$s。顯示驅動程式會在 DroidDeck 重新啟動後生效。</string> @@ -1272,9 +1248,7 @@ <string name="drivers_using_bundle_restart">正在使用 %1$s 作為運行環境及顯示驅動程式。顯示驅動程式會在 DroidDeck 重新啟動後生效。</string> <string name="drivers_deleted_bundle">已刪除 %1$s(兩個驅動程式)</string> <string name="drivers_deleted">已刪除 %1$s</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">最新:%1$s · %2$s檢查</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">最新:%1$s · 無法連接 %2$s · %3$s檢查</string> <plurals name="drivers_ago_minutes"> <item quantity="other">%1$d 分鐘前</item> @@ -1287,33 +1261,25 @@ </plurals> <string name="drivers_checking_sources">正在檢查 Banners-Turnip、WinNative 和 DroidDeck…</string> <string name="drivers_check_failed">檢查失敗</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">已安裝 %1$s - 在任一清單中選擇即可同時設定兩個驅動程式</string> <string name="drivers_installed_pick">已安裝 %1$s - 請在選單中選擇</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">請等待運行環境移除完成</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">遊戲</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">觸控板</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">直接</string> <string name="session_swap_failed">替換失敗:%1$s</string> <string name="session_profile_save_failed">設定檔儲存失敗:%1$s</string> <string name="session_desktop_paused">桌面已暫停</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$s 已暫停</string> <string name="session_program_paused">程式已暫停</string> <string name="session_starting_desktop">正在啟動桌面</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">正在啟動 %1$s</string> <string name="session_starting_program">正在啟動程式</string> <string name="session_no_logs">此會話沒有記錄。</string> <string name="session_logs_archive_failed">無法建立會話記錄封存檔。</string> <string name="session_logs_share_failed">無法分享會話記錄。</string> <string name="session_starting_session">正在啟動會話…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">正在下載 Linux 運行環境</string> <string name="session_runtime_downloading_mb">正在下載 Linux 運行環境 · %1$d/%2$d MB</string> <string name="session_runtime_checking">正在檢查 Linux 運行環境</string> @@ -1328,27 +1294,20 @@ <string name="session_proton_unpacking">正在解壓 Proton Experimental (ARM64)</string> <string name="session_runtime_catalog_unreachable">無法連接運行環境目錄。請檢查網絡連線,然後再按「開啟 Steam」。</string> <string name="session_runtime_install_failed">Linux 運行環境未能安裝。請在「設定」中檢查運行環境,然後再按「開啟 Steam」。</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">桌面未能安裝(%1$s)。請檢查網絡連線,然後再按「桌面」。</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$s 意外停止。請在錯誤報告中附上記錄,或再試一次。</string> <string name="session_ended_program">程式意外停止。請在錯誤報告中附上記錄,或再試一次。</string> <string name="session_ended_desktop">桌面意外停止。請在錯誤報告中附上記錄,或再試一次。</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">結束狀態 %1$d · %2$s</string> <string name="session_ended_no_log">沒有記錄</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="other">記錄顯示 %1$d 個「Function not implemented」錯誤:proot 的 seccomp 加速令此裝置上的一個輔助程式失敗。請嘗試 %2$s →「%3$s」(或「%4$s」),然後重新啟動。</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">插件</string> <string name="mode_decky_plugins_hint">安裝插件 ZIP,在下次 Steam 會話使用。</string> <string name="mode_decky_plugin_downloading">正在下載…</string> <string name="mode_decky_plugin_install_zip">從 ZIP 安裝</string> - <!-- Store --> <string name="store_cat_games">遊戲</string> <string name="store_cat_audio_video">音訊及影片</string> <string name="store_cat_graphics">圖形</string> @@ -1361,21 +1320,14 @@ <string name="store_cat_utilities">公用程式</string> <string name="store_section_popular">熱門</string> <string name="store_section_trending">趨勢</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">最新</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">最近更新</string> <string name="store_section_loading">正在載入…</string> <string name="store_flatpak_setup">Flatpak 設定</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s:%2$s</string> <string name="store_updates_check_failed">無法在 Flathub 檢查更新</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">已安裝</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">資料夾</string> - <!-- Launcher themes --> <string name="theme_graphite">石墨</string> <string name="theme_graphite_detail">石墨色介面,所有已選取、焦點所在或按下的項目都使用標誌本身的球體藍色。</string> <string name="theme_paper">黑底白紙</string> @@ -1384,7 +1336,6 @@ <string name="theme_icon_blue_detail">黑色背景、白色文字,所有已選取、焦點所在或按下的項目都使用藍色。</string> <string name="theme_electric">電光海軍藍</string> <string name="theme_electric_detail">更明亮的藍色,搭配偏海軍藍而非灰色的介面。</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Ins</string> <string name="pckb_key_del">Del</string> @@ -1402,22 +1353,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">空格</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">請先安裝運行環境</string> <string name="game_sync_err_steam_library">無法使用 Steam 收藏庫</string> <string name="game_sync_err_previous_sd">無法使用先前的 SD 卡收藏庫</string> <string name="game_sync_err_game_storage">無法使用遊戲儲存位置</string> <string name="game_sync_err_added_games">無法使用已新增遊戲的資料夾</string> <string name="game_sync_err_manifests">無法使用 Steam 清單檔案</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">多個系統</string> - <!-- File manager --> <string name="fm_drive_internal">內部</string> <string name="fm_drive_linux">Linux 系統</string> <string name="fm_drive_sd">SD 卡</string> @@ -1435,7 +1381,6 @@ <string name="fm_no_favorites">尚未有我的最愛 - 使用資料夾的 ⋮ 選單將其釘選,即可快速返回。</string> <string name="fm_unpin_favorite">從我的最愛移除</string> <string name="fm_size_zero">0 位元組</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">位元組</string> <string name="fm_prop_location">位置</string> <string name="fm_prop_type">類型</string> @@ -1443,7 +1388,6 @@ <string name="fm_prop_modified">修改日期</string> <string name="fm_type_folder">資料夾</string> <string name="fm_type_file">檔案</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">%1$s 檔案</string> <string name="fm_read_only">唯讀</string> <string name="fm_read_only_hint">防止遊戲(及 Wine)覆寫或刪除此檔案。</string> @@ -1457,15 +1401,11 @@ <string name="fm_delete_failed">刪除失敗</string> <string name="fm_op_moving">正在移動...</string> <string name="fm_op_copying">正在複製...</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">正在移動 - %1$s</string> <string name="fm_op_copying_item">正在複製 - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">正在移動 %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">正在複製 %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d 個完成,%2$d 個失敗</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d 個完成,%2$d 個已略過</string> <plurals name="fm_paste_moved"> <item quantity="other">已移動 %1$d 個項目</item> @@ -1473,7 +1413,6 @@ <plurals name="fm_paste_copied"> <item quantity="other">已複製 %1$d 個項目</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">「%1$s」已存在</string> <string name="fm_op_renaming">正在重新命名...</string> <string name="fm_rename_failed">重新命名失敗</string> @@ -1484,17 +1423,14 @@ <string name="fm_create">建立</string> <string name="fm_new_name">新名稱</string> <string name="fm_delete_title">刪除?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">要永久刪除「%1$s」嗎?</string> <plurals name="fm_delete_items_title"> <item quantity="other">刪除 %1$d 個項目?</item> </plurals> <string name="fm_cannot_undo">此操作無法復原。</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="other">…及另外 %1$d 個</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">正在刪除 %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="other">%1$d 個無法刪除</item> @@ -1508,7 +1444,6 @@ <string name="fm_conflict_skip">略過</string> <string name="fm_select_this_folder">選擇此資料夾</string> <string name="fm_back">返回</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$s 已掛載,但目前無法讀取</string> <string name="fm_list_view">清單檢視</string> <string name="fm_grid_view">格狀檢視</string> @@ -1523,41 +1458,31 @@ <string name="fm_hide_favorites">隱藏我的最愛</string> <string name="fm_show_favorites">顯示我的最愛</string> <string name="fm_filter_folder">篩選此資料夾</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">可用 %1$s</string> <string name="fm_selected_count">已選取 %1$d 個</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">無</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">全部</string> <string name="fm_paste">貼上</string> <plurals name="fm_items"> <item quantity="other">%1$d 個項目</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">在此貼上 %1$s</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">移動 %1$s 至此</string> <string name="fm_rail_title">檔案</string> <string name="fm_rail_storage">儲存空間</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">快速</string> <string name="fm_rail_favorites">我的最愛</string> <string name="fm_rail_downloads">下載</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">下載</string> <string name="fm_rail_roms">ROMs</string> <string name="fm_rail_session_logs">會話記錄</string> <string name="fm_rail_pictures">圖片</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">已將「%1$s」加入我的最愛</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">已將「%1$s」從我的最愛移除</string> <string name="fm_empty_directory">空白目錄</string> <string name="fm_up_one_level">返回上一層</string> <string name="fm_choose_folder">選擇資料夾</string> <string name="fm_choose_file">選擇檔案</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Steam 藍</string> <string name="ctrl_tint_sky">天藍</string> <string name="ctrl_tint_white">白色</string> @@ -1566,7 +1491,6 @@ <string name="ctrl_tint_amber">琥珀色</string> <string name="ctrl_tint_red">紅色</string> <string name="ctrl_tint_pink">粉紅色</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">A 鍵</string> <string name="ctrl_button_b">B 鍵</string> <string name="ctrl_button_x">X 鍵</string> @@ -1577,28 +1501,21 @@ <string name="ctrl_button_rt">右扳機鍵</string> <string name="ctrl_button_view">檢視按鈕</string> <string name="ctrl_button_menu">選單按鈕</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> - <!-- Controller View button --> <string name="ctrl_target_view">檢視</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">選單</string> <string name="ctrl_target_up">十字鍵上</string> <string name="ctrl_target_down">十字鍵下</string> <string name="ctrl_target_left">十字鍵左</string> <string name="ctrl_target_right">十字鍵右</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">隱藏</string> <string name="ctrl_editor_drag">拖曳以移動</string> <string name="ctrl_editor_snap">對齊</string> <string name="ctrl_editor_save">儲存</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">無</string> <string name="second_mode_keyboard_trackpad">鍵盤 + 觸控板</string> <string name="second_mode_terminal">終端機</string> <string name="second_mode_deck_controls">Deck 握把 + 觸控板</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">正在移除 Linux 運行環境</string> <plurals name="rtinst_removing_entries"> <item quantity="other">正在移除 Linux 運行環境 · %1$d 個項目</item> @@ -1613,63 +1530,47 @@ <string name="rtinst_title_installing">正在安裝 Linux 運行環境</string> <string name="rtinst_title_removing">正在移除 Linux 運行環境</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">尚未安裝 Linux 運行環境</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">目錄中沒有 %1$s 的校驗和</string> <string name="deskpkg_checksum_mismatch">校驗和不符 - 未作任何更改</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">正在安裝 %1$s</string> <string name="deskpkg_place_failed">無法放置 AppImage</string> <string name="deskpkg_extract_failed">解壓縮失敗</string> <string name="deskpkg_install_failed">安裝失敗</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">Flatpak 設定失敗(結束代碼 %1$d)</string> <string name="flatpakmgr_setup_unfinished">Flatpak 設定未完成</string> <string name="flatpakmgr_busy">另一項商店工作正在進行</string> <string name="flatpakmgr_not_ready">請先設定 Flatpak</string> <string name="flatpakmgr_starting">正在開始</string> <string name="flatpakmgr_exit_status">Flatpak 已結束,狀態為 %1$d</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">正在安裝 %1$s</string> <string name="flatpakmgr_updating">正在更新 %1$s</string> <string name="flatpakmgr_removing">正在移除 %1$s</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">正在安裝 %1$s(%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">正在更新 %1$s(%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">正在移除 %1$s(%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">翻譯</string> <string name="flatpakmgr_ref_graphics_drivers">圖形驅動程式</string> <string name="flatpakmgr_ref_media_codecs">媒體編解碼器</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">%1$s 運行環境</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">%1$s 運行環境 %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">檔案太小,不可能是 AppImage</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">無法讀取檔案:%1$s</string> <string name="appimg_not_linux">這不是 AppImage(它不是 Linux 程式)</string> <string name="appimg_wrong_arch">此 AppImage 既不是為 ARM64 (aarch64) 亦不是為 x86 電腦而設</string> <string name="appimg_not_appimage">這是 Linux 程式,但不是 AppImage</string> <string name="appimg_wrong_type">只可匯入第 2 類 AppImage(此檔案為第 %1$d 類)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">可用空間不足:匯入約需 %1$s</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">可用空間不足:更新約需 %1$s</string> <string name="appimg_import_failed">匯入失敗</string> <string name="appimg_gone">該 AppImage 已不存在</string> <string name="appimg_update_failed">更新失敗</string> - <!-- %1$s: file name --> <string name="appimg_extracting">正在解壓縮 %1$s</string> <string name="appimg_extract_failed">無法解壓縮 AppImage</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">無法解壓縮 AppImage:%1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">請先停止目前的會話,再安裝 Decky Loader</string> <string name="deckymgr_download_failed">Decky Loader 下載失敗</string> <string name="deckymgr_size_mismatch">Decky Loader 的大小與發佈中繼資料不符</string> @@ -1705,20 +1606,13 @@ <string name="deckymgr_remote_entry_invalid">remote_binary 項目無效</string> <string name="deckymgr_remote_name_invalid">遠端二進位檔名稱無效</string> <string name="deckymgr_remote_url_https">遠端二進位檔網址必須使用 HTTPS</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">遠端二進位檔 %1$s 的 SHA-256 雜湊值無效</string> <string name="deckymgr_bin_dir_failed">無法建立插件 bin 目錄</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">正在下載插件二進位檔 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">無法下載插件二進位檔 %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">插件二進位檔校驗和不符 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">無法將插件二進位檔設為可執行 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">無法取代插件二進位檔 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">無法安裝插件二進位檔 · %1$s</string> <string name="deckymgr_folder_taken">插件資料夾已屬於另一個插件</string> <string name="deckymgr_preserve_failed">無法保留現有插件</string> @@ -1726,20 +1620,14 @@ <string name="deckymgr_settings_failed">無法更新 Decky 插件設定</string> <string name="deckymgr_restore_failed">無法還原現有插件</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">無法儲存無線偵錯身分</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">ADB 指令失敗(%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Android 未確認子進程限制已更改:%1$s</string> <string name="adbfix_no_pairing">沒有已儲存的無線偵錯配對</string> <string name="adbfix_enter_address">請輸入目前的無線偵錯 IP 位址和連接埠</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">等待配對彈出式視窗逾時,請在 DroidDeck 重試。</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">無法尋找配對彈出式視窗(%1$d)。請確認 Wi-Fi 已開啟。</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">無法尋找配對彈出式視窗:%1$s</string> <string name="adbpair_six_digits">請輸入完整的 6 位數配對碼</string> <string name="adbpair_pairing">正在配對…</string> @@ -1753,27 +1641,22 @@ <string name="adbpair_title_pair">配對無線偵錯</string> <string name="adbpair_waiting_text">開啟無線偵錯,然後輕按「使用配對碼配對裝置」。</string> <string name="adbpair_waiting_long">開啟無線偵錯,然後輕按「使用配對碼配對裝置」。請留在「設定」中:配對碼需在此通知中輸入。</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">6 位數配對碼</string> <string name="adbpair_code_hint">輕按「輸入配對碼」,然後輸入「設定」中顯示的 6 位數字。</string> <string name="adbpair_title_code">輸入 Wi-Fi 配對碼</string> <string name="adbpair_title_retry">請再次輸入配對碼</string> - <!-- Notification action button --> <string name="adbpair_enter_code">輸入配對碼</string> <string name="adbpair_title_working">正在設定 Steam</string> <string name="adbpair_title_done">Steam 已準備就緒</string> <string name="adbpair_done_text">輕按即可返回 DroidDeck。你現在可以關閉無線偵錯。</string> <string name="adbpair_title_failed">無線偵錯設定已停止</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">下載回應 HTTP %1$d</string> <string name="selfinst_size_mismatch">下載的大小與已發佈的 APK 不符,已被捨棄 - 請再試一次</string> <string name="selfinst_checksum_mismatch">下載的檔案與其校驗和不符,已被捨棄 - 請再試一次</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">此更新屬於 Android 套件 %1$s,而非 %2$s</string> <string name="selfinst_metadata_signer">更新中繼資料未指明 DroidDeck 的發佈簽署金鑰</string> <string name="selfinst_unreadable">Android 無法讀取已下載的 APK</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">已下載的 APK 屬於 %1$s,而非 %2$s</string> <string name="selfinst_version_mismatch">已下載的 APK 的 versionCode 為 %1$d,而非已發佈的 %2$d</string> <string name="selfinst_downgrade">Android 不會以 versionCode %1$d 覆蓋已安裝的 %2$d</string> @@ -1781,16 +1664,11 @@ <string name="selfinst_wrong_signer">已下載的 APK 並非使用 DroidDeck 的發佈金鑰簽署</string> <string name="appupd_http">更新目錄回應 HTTP %1$d</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">正在啟動會話…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">已過 %1$d:%2$02d · 仍在處理</string> <string name="loadstate_steam_update">正在下載 Steam 用戶端更新 · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">正在下載 Steam 用戶端 · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">已有兼容性工具正在安裝</string> <string name="pextra_stop_session">請先停止目前的會話,再安裝兼容性工具</string> <string name="pextra_no_release">找不到 %1$s 的 ARM64 發佈版本</string> @@ -1807,48 +1685,38 @@ <string name="pextra_checksum_mismatch">校驗和不符;已刪除下載的檔案</string> <string name="pextra_session_started">下載期間有會話啟動;請先停止該會話,再安裝兼容性工具</string> <string name="pextra_installing">正在安裝 %1$s</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">%1$s 安裝失敗(結束代碼 %2$d)</string> <string name="pextra_could_not_install">無法安裝 %1$s</string> <string name="esync_fetching">正在下載 droiddeck-esync 套件</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">包含 Linux 輔助程式</string> <string name="cmgr_fex_dlls_only">只有 DLL</string> <string name="cmgr_original_proton_updated">原始(上次替換後 Proton 已更新)</string> <string name="cmgr_changed_outside">已在「元件」以外更改</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">不存在</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">原始 %1$s</string> <string name="cmgr_original_from">原始版本來自 %1$s</string> <string name="cmgr_swap_queued">%1$s %2$s 會在 %3$s 上執行的遊戲關閉後放入其中。</string> <string name="cmgr_swap_done">%1$s %2$s 現已放入 %3$s,會在下次啟動遊戲時生效。</string> <string name="cmgr_original_missing">未儲存該原始組合包</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%2$s 的 %1$s 會在其上執行的遊戲關閉後還原為原始版本。</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">已從 %3$s 的原始版本還原 %2$s 的 %1$s。</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%1$s 正在 %2$s 中使用。請先將該 Proton 替換為其他版本。</string> <string name="cmgr_package_not_stored">未儲存 %1$s</string> <string name="cmgr_deleted">已刪除 %1$s。</string> <string name="cmgr_original_kept">已安裝版本的原始版本會保留,以便還原。</string> <string name="cmgr_not_stored">未儲存</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">已刪除 %2$s 的 %1$s 原始版本。</string> <string name="cmgr_nightlies_only">只會從 Nightlies 發佈版本下載</string> <string name="cmgr_list_predates_checksums">此軟件包清單早於校驗和推出 - 請重新整理後再試一次</string> <string name="cmgr_checksum_mismatch">校驗和不符</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">線性</string> <string name="sprefs_upscaler_nearest">最近鄰</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR(品質)</string> <string name="sprefs_upscaler_sharpen">只銳化</string> <string name="sprefs_lod_bias_auto">自動(配合縮放)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">沒有套用任何效果。</string> <string name="look_game_clarity">遊戲清晰</string> <string name="look_game_clarity_desc">銳化並略增對比度 - 日常之選。</string> @@ -1873,27 +1741,23 @@ <string name="look_anime_edge">動畫描邊</string> <string name="look_anime_edge_desc">卡通描邊加上銳化。</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">不是 Adreno GPU</string> <string name="gpuinfo_supported">受支援</string> <string name="gpuinfo_experimental">實驗性:其驅動程式為測試版本</string> <string name="gpuinfo_below_tested">低於已測試的硬件(Adreno 725 或更新):可能無法執行</string> <string name="gpuinfo_unsupported">不受支援:DroidDeck 需要 Adreno (Snapdragon) GPU</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">已達 GitHub 的速率限制 - 請稍後再試</string> <string name="turnip_github_http">GitHub 回應 HTTP %1$d</string> <string name="turnip_download_http">下載回應 HTTP %1$d</string> <string name="turnip_list_predates_checksums">此驅動程式清單早於校驗和推出 - 請重新整理後再試一次</string> <string name="turnip_checksum_mismatch">校驗和不符 - 已捨棄下載的檔案</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">不是 AdrenoTools 驅動程式:%1$s</string> <string name="turnip_reject_no_meta">沒有 meta.json</string> <string name="turnip_reject_meta_unreadable">無法讀取 meta.json(%1$s)</string> <string name="turnip_reject_linux_driver">這是 Linux 運行環境驅動程式(請在「運行環境驅動程式」下匯入)</string> <string name="turnip_reject_wayland">這是 Wayland 遊戲驅動程式,此應用程式用不上</string> <string name="turnip_reject_no_library_name">meta.json 沒有指明 libraryName,因此無法交給 AdrenoTools</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.json 指明了 %1$s,但 zip 中沒有此檔案</string> <string name="turnip_reject_not_aarch64">%1$s 不是 64 位元 AArch64 ELF 共用程式庫</string> <string name="turnip_reject_glibc">%1$s 連結 glibc - 這是「-Linux」Turnip,應在「運行環境驅動程式」下匯入</string> @@ -1904,90 +1768,61 @@ <string name="bundle_entry_outside">不是驅動程式組合包:%1$s 指向組合包以外</string> <string name="bundle_too_large">不是驅動程式組合包:解壓後超過 %1$d MB</string> - <!-- Game storage --> <string name="gstore_sd_card">SD 卡</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">資料夾</string> <string name="logshare_chooser">分享會話記錄</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">需要 Android 10 或更新版本(支援色彩空間的顯示圖層)</string> <string name="hdr_none">此螢幕沒有回報支援 HDR</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">此螢幕支援 %1$s,但不支援 HDR10</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">此 GPU</string> <string name="phantom_title_on">子進程限制已開啟</string> <string name="phantom_title_unset">子進程限制未設定</string> <string name="phantom_title_unreadable">無法檢查子進程限制</string> <string name="phantom_title_off">子進程限制已關閉</string> <string name="phantom_title_not_required">無需理會子進程限制</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s 如無法這樣做,請將此裝置連接到已安裝 ADB 的電腦,並執行以下指令,然後返回這裡再檢查一次。</string> <string name="phantom_instructions_off">可以啟動 Steam 會話。</string> <string name="phantom_instructions_not_applicable">此 Android 版本不使用此限制。</string> <string name="phantom_fix_toggle">在開發人員選項中開啟「停用子進程限制」,之後請保持開發人員選項開啟。</string> <string name="phantom_fix_no_toggle">此 Android 版本沒有此開關。請使用無線偵錯,直接在此裝置上更改。</string> <string name="upd_block_no_apk">此版本沒有適用於此 DroidDeck 版本的下載。</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">Android 不會以此版本覆蓋安裝,因為其 versionCode %1$d 低於已安裝的 %2$d。</string> <string name="upd_fail_downgrade">Android 不會以舊版本覆蓋較新的版本。請繼續使用此版本,直至該頻道追上為止。</string> <string name="upd_fail_signature">此 DroidDeck 的簽署方式不同(可能是本機自行建置的版本?),因此 Android 無法直接更新。</string> <string name="upd_fail_storage">可用空間不足,無法更新。</string> <string name="upd_fail_aborted">已取消更新。</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">更新未能安裝:%1$s</string> <string name="upd_fail_generic">更新未能安裝。</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">%1$s zip</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">zip</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$s 尚未啟動過</string> <string name="saves_none_found">找不到 %1$s 的存檔</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$s 沒有 drive_c 資料夾,因此不是 GameHub 或 Winlator 存檔 zip</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">無法在 %1$s 中建立資料夾</string> <string name="gstore_cannot_write">無法寫入 %1$s</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">無法寫入 %1$s(%2$s)</string> <string name="gfiles_folder_unavailable">無法使用資料夾</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">另一個檔案已使用 %1$s</string> <string name="gfiles_art_unavailable">無法使用圖片資料夾</string> <string name="gfiles_unrelated_inventory">資料夾中有不相關的同步清單</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">無法移除 %1$s</string> <string name="gfiles_outside_folder">檔案超出匯出資料夾範圍</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX(沒有版本名稱的版本)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">%1$s 中沒有 profile.json</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$s 不是 Linux 元件套件(類型為 %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · 可用 %2$s</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">自動</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">更新目錄未通過檢查(%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">DroidDeck 子進程設定</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">已登入 · 離線模式</string> <string name="setup_signed_in_unnamed">已登入</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">內置</string> <string name="driver_tag_downloaded">已下載</string> <string name="driver_tag_imported">已匯入</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">ANDROID + LINUX</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s。%2$s\n\n%3$s</string> <string name="display_session">會話顯示</string> <string name="display_resolution">解像度</string> diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index e5fc5d76a..5e37513a1 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1141,16 +1141,12 @@ <string name="game_file_exported">遊戲檔案已儲存到 %1$s</string> <string name="game_frontend_files">前端檔案</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">請等待執行環境作業完成</string> <string name="main_wait_proton_install">請等待相容性工具安裝完成</string> <string name="main_wait_desktop_install">請等待桌面應用程式安裝完成</string> <string name="main_wait_install_game">請等待安裝完成後再啟動遊戲</string> <string name="main_saves_close_session">請先關閉 Steam 工作階段,以免遊戲存檔覆蓋匯入的內容</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">正在匯入到 %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="other">已從 %2$s 匯入 %1$d 個檔案到 %3$s</item> </plurals> @@ -1158,15 +1154,11 @@ <item quantity="other">已從 %2$s 匯入 %1$d 個檔案到 %3$s。舊存檔已備份到 Download/DroidDeck/Saves/backups</item> </plurals> <string name="main_saves_pick_zip">選擇 %1$s 的存檔壓縮檔</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">正在匯出 %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="other">已將 %1$d 個檔案匯出為 %2$s:%3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">選擇 %1$s 的儲存位置(%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">失敗(%1$s):%2$s</string> <string name="main_no_logs_yet">還沒有工作階段日誌:請先執行一次工作階段。</string> <string name="main_no_logs_to_clear">沒有可清除的工作階段日誌。</string> @@ -1179,68 +1171,55 @@ <string name="main_adb_copied">已複製 ADB 指令</string> <string name="main_pick_roms">選擇 ROM 資料夾</string> <string name="main_non_adreno_title">不是 Adreno GPU</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">Turnip 支援 Adreno GPU。在 %1$s 上,Steam 可能會出現黑畫面。下載大小:%2$.0f MB。</string> <string name="main_install_anyway">仍要安裝</string> <string name="main_remove_runtime_title">移除 Linux 執行環境</string> <string name="main_remove_runtime_text">這會刪除執行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">已套用:%1$s</string> <string name="android_app_open_failed_primary">無法在主螢幕上開啟 %1$s</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">無法在 %2$s 上開啟 %1$s</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">無法在螢幕 %2$d 上開啟 %1$s</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">正在匯入</string> <string name="comp_busy_swapping">正在換入</string> <string name="comp_busy_restoring">正在還原</string> <string name="comp_busy_cancelling">正在取消</string> <string name="comp_busy_deleting">正在刪除</string> <string name="comp_cannot_read">無法讀取檔案</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">已匯入 %1$s</string> <string name="comp_swap_cancelled">已取消等待中的換入。</string> <string name="comp_pick_wcp">選擇元件套件(-linux .wcp)</string> <string name="comp_nightlies_unreachable">無法連線到 Nightlies</string> - <!-- %1$s is the package version --> <string name="comp_stored">已儲存 %1$s</string> <string name="comp_download_failed">下載失敗:%1$s</string> <string name="drivers_pick_linux">選擇 Linux 執行環境驅動程式(-Linux zip 或 Android + Linux 套件組合)</string> <string name="drivers_pick_android">選擇顯示驅動程式(AdrenoTools zip 或 Android + Linux 套件組合)</string> <string name="drivers_pick_any">選擇驅動程式 zip 或 Android + Linux 套件組合</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s:%2$s</string> <string name="main_removing_named">正在移除 %1$s…</string> <string name="main_forgetting_named">正在忘記 %1$s…</string> <string name="main_pick_game_storage">選擇遊戲儲存資料夾</string> <string name="main_pick_decky_zip">選擇 Decky 外掛程式 ZIP</string> <string name="main_storage_not_usable">無法使用:%1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">程式</string> <string name="main_removing_runtime">正在移除 Linux 執行環境</string> <string name="main_settings_main_screen">無法在底部螢幕開啟「設定」,已改在主螢幕上開啟。</string> <string name="main_open_dev_options">在「設定」中開啟開發人員選項</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">無法開啟 Android 的「安裝不明應用程式」設定</string> <string name="upd_couldnt_check">無法檢查:%1$s</string> <string name="upd_no_download">此組建版本沒有適用於此 DroidDeck 複本的下載檔案</string> <string name="upd_stop_first">請先停止正在執行的工作階段,再進行更新</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">正在下載</string> <string name="upd_stage_installing">正在安裝</string> <string name="upd_stage_confirm">等待你確認</string> <string name="upd_failed">更新失敗</string> <string name="upd_not_offered">Android 未提供安裝此更新的選項</string> - <!-- Decky --> <string name="decky_install_failed">Decky 安裝失敗:%1$s</string> <string name="decky_plugin_starting">正在開始匯入外掛程式…</string> <string name="decky_plugin_failed">外掛程式匯入失敗:%1$s</string> <string name="decky_plugin_installed">外掛程式已安裝。請重新啟動 Steam 以載入。</string> - <!-- GPU drivers --> <string name="drivers_not_checked">尚未檢查 - 點擊重新整理以尋找新驅動程式</string> <string name="drivers_bundle_sets_display">Android + Linux:同時設定顯示驅動程式</string> <string name="drivers_bundle_sets_runtime">Android + Linux:同時設定執行環境驅動程式</string> @@ -1248,16 +1227,13 @@ <string name="drivers_system_vulkan">系統 Vulkan:沒有適用於此 GPU 的內建版本</string> <string name="drivers_bundled">%1$s(內建)</string> <string name="drivers_manual">手動</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">目前只發布了其中一半</string> <string name="drivers_pair_size">兩者共 %1$.0f MB</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">沒有可設定的驅動程式:%1$s</string> <string name="drivers_checking_latest">正在檢查最新驅動程式…</string> <string name="drivers_up_to_date_check_failed">已是最新(無法檢查:%1$s)</string> <string name="drivers_recommended_unpublished">建議的驅動程式目前尚未發布</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">正在下載 %1$s %2$s…</string> <string name="drivers_updated_to">已更新到 %1$s %2$s。</string> <string name="drivers_updated_to_restart">已更新到 %1$s %2$s。顯示驅動程式會在 DroidDeck 重新啟動後生效。</string> @@ -1272,9 +1248,7 @@ <string name="drivers_using_bundle_restart">正在將 %1$s 同時用作執行環境驅動程式和顯示驅動程式。顯示驅動程式會在 DroidDeck 重新啟動後生效。</string> <string name="drivers_deleted_bundle">已刪除 %1$s(兩個驅動程式)</string> <string name="drivers_deleted">已刪除 %1$s</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">最新:%1$s · 檢查於 %2$s</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">最新:%1$s · 無法連線到 %2$s · 檢查於 %3$s</string> <plurals name="drivers_ago_minutes"> <item quantity="other">%1$d 分鐘前</item> @@ -1287,33 +1261,25 @@ </plurals> <string name="drivers_checking_sources">正在檢查 Banners-Turnip、WinNative 和 DroidDeck…</string> <string name="drivers_check_failed">檢查失敗</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">已安裝 %1$s - 在任一清單中選擇它即可同時設定兩個驅動程式</string> <string name="drivers_installed_pick">已安裝 %1$s - 請在選單中選擇</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">請等待執行環境移除完成</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">遊戲</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">觸控板</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">直接</string> <string name="session_swap_failed">換入失敗:%1$s</string> <string name="session_profile_save_failed">設定檔儲存失敗:%1$s</string> <string name="session_desktop_paused">桌面已暫停</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$s 已暫停</string> <string name="session_program_paused">程式已暫停</string> <string name="session_starting_desktop">正在啟動桌面</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">正在啟動 %1$s</string> <string name="session_starting_program">正在啟動程式</string> <string name="session_no_logs">此工作階段沒有日誌。</string> <string name="session_logs_archive_failed">無法建立工作階段日誌封存檔。</string> <string name="session_logs_share_failed">無法分享工作階段日誌。</string> <string name="session_starting_session">正在啟動工作階段…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">正在下載 Linux 執行環境</string> <string name="session_runtime_downloading_mb">正在下載 Linux 執行環境 · %1$d/%2$d MB</string> <string name="session_runtime_checking">正在檢查 Linux 執行環境</string> @@ -1328,27 +1294,20 @@ <string name="session_proton_unpacking">正在解壓縮 Proton Experimental(ARM64)</string> <string name="session_runtime_catalog_unreachable">無法連線到執行環境目錄。請檢查網路連線,然後再按一次「執行」。</string> <string name="session_runtime_install_failed">Linux 執行環境未能安裝。請在「設定」中檢查執行環境,然後再按一次「執行」。</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">桌面未能安裝(%1$s)。請檢查網路連線,然後再按一次「桌面」。</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$s 意外停止。請在錯誤報告中附上日誌,或再試一次。</string> <string name="session_ended_program">程式意外停止。請在錯誤報告中附上日誌,或再試一次。</string> <string name="session_ended_desktop">桌面意外停止。請在錯誤報告中附上日誌,或再試一次。</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">結束狀態 %1$d · %2$s</string> <string name="session_ended_no_log">沒有日誌</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="other">日誌中出現 %1$d 個「Function not implemented」錯誤:proot 的 seccomp 加速在這部裝置上導致輔助程式失敗。請嘗試 %2$s →「%3$s」(或「%4$s」),然後重新啟動。</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">外掛程式</string> <string name="mode_decky_plugins_hint">安裝外掛程式 ZIP,供下次 Steam 工作階段使用。</string> <string name="mode_decky_plugin_downloading">正在下載…</string> <string name="mode_decky_plugin_install_zip">從 ZIP 安裝</string> - <!-- Store --> <string name="store_cat_games">遊戲</string> <string name="store_cat_audio_video">影音</string> <string name="store_cat_graphics">圖形</string> @@ -1361,21 +1320,14 @@ <string name="store_cat_utilities">工具程式</string> <string name="store_section_popular">熱門</string> <string name="store_section_trending">趨勢</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">最新上架</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">最近更新</string> <string name="store_section_loading">正在載入…</string> <string name="store_flatpak_setup">Flatpak 設定</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s:%2$s</string> <string name="store_updates_check_failed">無法從 Flathub 檢查更新</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">已安裝</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">資料夾</string> - <!-- Launcher themes --> <string name="theme_graphite">石墨</string> <string name="theme_graphite_detail">石墨色介面,選取、聚焦或按下的項目都使用標誌本身的球體藍。</string> <string name="theme_paper">黑底白紙</string> @@ -1384,7 +1336,6 @@ <string name="theme_icon_blue_detail">黑色背景、白色文字,選取、聚焦或按下的項目使用藍色。</string> <string name="theme_electric">電光藏青</string> <string name="theme_electric_detail">更亮的藍色,搭配偏藏青而非灰色的介面。</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Ins</string> <string name="pckb_key_del">Del</string> @@ -1402,22 +1353,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">Space</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">請先安裝執行環境</string> <string name="game_sync_err_steam_library">無法使用 Steam 收藏庫</string> <string name="game_sync_err_previous_sd">無法使用先前的 SD 卡收藏庫</string> <string name="game_sync_err_game_storage">無法使用遊戲儲存空間</string> <string name="game_sync_err_added_games">無法使用已新增遊戲的資料夾</string> <string name="game_sync_err_manifests">無法使用 Steam 資訊清單</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">多種主機</string> - <!-- File manager --> <string name="fm_drive_internal">內部</string> <string name="fm_drive_linux">Linux 系統</string> <string name="fm_drive_sd">SD 卡</string> @@ -1435,7 +1381,6 @@ <string name="fm_no_favorites">還沒有我的最愛 - 透過資料夾的 ⋮ 選單釘選,即可快速回到這裡。</string> <string name="fm_unpin_favorite">從我的最愛移除</string> <string name="fm_size_zero">0 位元組</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">位元組</string> <string name="fm_prop_location">位置</string> <string name="fm_prop_type">類型</string> @@ -1443,7 +1388,6 @@ <string name="fm_prop_modified">修改日期</string> <string name="fm_type_folder">資料夾</string> <string name="fm_type_file">檔案</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">%1$s 檔案</string> <string name="fm_read_only">唯讀</string> <string name="fm_read_only_hint">防止遊戲(和 Wine)覆寫或刪除此檔案。</string> @@ -1457,15 +1401,11 @@ <string name="fm_delete_failed">刪除失敗</string> <string name="fm_op_moving">正在移動...</string> <string name="fm_op_copying">正在複製...</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">正在移動 - %1$s</string> <string name="fm_op_copying_item">正在複製 - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">正在移動 %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">正在複製 %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d 個完成,%2$d 個失敗</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d 個完成,%2$d 個略過</string> <plurals name="fm_paste_moved"> <item quantity="other">已移動 %1$d 個項目</item> @@ -1473,7 +1413,6 @@ <plurals name="fm_paste_copied"> <item quantity="other">已複製 %1$d 個項目</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">「%1$s」已存在</string> <string name="fm_op_renaming">正在重新命名...</string> <string name="fm_rename_failed">重新命名失敗</string> @@ -1484,17 +1423,14 @@ <string name="fm_create">建立</string> <string name="fm_new_name">新名稱</string> <string name="fm_delete_title">要刪除嗎?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">要永久刪除「%1$s」嗎?</string> <plurals name="fm_delete_items_title"> <item quantity="other">要刪除 %1$d 個項目嗎?</item> </plurals> <string name="fm_cannot_undo">此動作無法復原。</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="other">…及其他 %1$d 個</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">正在刪除 %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="other">有 %1$d 個無法刪除</item> @@ -1508,7 +1444,6 @@ <string name="fm_conflict_skip">略過</string> <string name="fm_select_this_folder">選擇此資料夾</string> <string name="fm_back">返回</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$s 已掛載,但目前無法讀取</string> <string name="fm_list_view">清單檢視</string> <string name="fm_grid_view">格狀檢視</string> @@ -1523,41 +1458,31 @@ <string name="fm_hide_favorites">隱藏我的最愛</string> <string name="fm_show_favorites">顯示我的最愛</string> <string name="fm_filter_folder">篩選此資料夾</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">可用 %1$s</string> <string name="fm_selected_count">已選取 %1$d 個</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">全不選</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">全選</string> <string name="fm_paste">貼上</string> <plurals name="fm_items"> <item quantity="other">%1$d 個項目</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">將 %1$s 貼到這裡</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">將 %1$s 移動到這裡</string> <string name="fm_rail_title">檔案</string> <string name="fm_rail_storage">儲存空間</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">快速存取</string> <string name="fm_rail_favorites">我的最愛</string> <string name="fm_rail_downloads">下載</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">下載</string> <string name="fm_rail_roms">ROM</string> <string name="fm_rail_session_logs">工作階段日誌</string> <string name="fm_rail_pictures">圖片</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">已將「%1$s」加入我的最愛</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">已將「%1$s」從我的最愛移除</string> <string name="fm_empty_directory">空的資料夾</string> <string name="fm_up_one_level">返回上一層</string> <string name="fm_choose_folder">選擇資料夾</string> <string name="fm_choose_file">選擇檔案</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Steam 藍</string> <string name="ctrl_tint_sky">天藍</string> <string name="ctrl_tint_white">白色</string> @@ -1566,7 +1491,6 @@ <string name="ctrl_tint_amber">琥珀</string> <string name="ctrl_tint_red">紅色</string> <string name="ctrl_tint_pink">粉紅</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">A 按鈕</string> <string name="ctrl_button_b">B 按鈕</string> <string name="ctrl_button_x">X 按鈕</string> @@ -1577,28 +1501,21 @@ <string name="ctrl_button_rt">右扳機</string> <string name="ctrl_button_view">檢視按鈕</string> <string name="ctrl_button_menu">選單按鈕</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> - <!-- Controller View button --> <string name="ctrl_target_view">檢視</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">選單</string> <string name="ctrl_target_up">方向鍵上</string> <string name="ctrl_target_down">方向鍵下</string> <string name="ctrl_target_left">方向鍵左</string> <string name="ctrl_target_right">方向鍵右</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">隱藏</string> <string name="ctrl_editor_drag">拖曳以移動</string> <string name="ctrl_editor_snap">對齊</string> <string name="ctrl_editor_save">儲存</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">無</string> <string name="second_mode_keyboard_trackpad">鍵盤 + 觸控板</string> <string name="second_mode_terminal">終端機</string> <string name="second_mode_deck_controls">Deck 握把 + 觸控板</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">正在移除 Linux 執行環境</string> <plurals name="rtinst_removing_entries"> <item quantity="other">正在移除 Linux 執行環境 · %1$d 個項目</item> @@ -1613,63 +1530,47 @@ <string name="rtinst_title_installing">正在安裝 Linux 執行環境</string> <string name="rtinst_title_removing">正在移除 Linux 執行環境</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">尚未安裝 Linux 執行環境</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">目錄中沒有 %1$s 的校驗碼</string> <string name="deskpkg_checksum_mismatch">校驗碼不符 - 未進行任何變更</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">正在安裝 %1$s</string> <string name="deskpkg_place_failed">無法放置 AppImage</string> <string name="deskpkg_extract_failed">解壓縮失敗</string> <string name="deskpkg_install_failed">安裝失敗</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">Flatpak 設定失敗(結束代碼 %1$d)</string> <string name="flatpakmgr_setup_unfinished">Flatpak 設定未完成</string> <string name="flatpakmgr_busy">另一個商店工作正在執行</string> <string name="flatpakmgr_not_ready">請先設定 Flatpak</string> <string name="flatpakmgr_starting">正在啟動</string> <string name="flatpakmgr_exit_status">Flatpak 已結束,狀態碼 %1$d</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">正在安裝 %1$s</string> <string name="flatpakmgr_updating">正在更新 %1$s</string> <string name="flatpakmgr_removing">正在移除 %1$s</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">正在安裝 %1$s(%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">正在更新 %1$s(%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">正在移除 %1$s(%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">翻譯</string> <string name="flatpakmgr_ref_graphics_drivers">圖形驅動程式</string> <string name="flatpakmgr_ref_media_codecs">媒體轉碼器</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">%1$s 執行環境</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">%1$s 執行環境 %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">檔案太小,不可能是 AppImage</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">無法讀取檔案:%1$s</string> <string name="appimg_not_linux">這不是 AppImage(不是 Linux 程式)</string> <string name="appimg_wrong_arch">此 AppImage 既不是為 ARM64(aarch64)也不是為 x86 電腦組建的</string> <string name="appimg_not_appimage">這是 Linux 程式,但不是 AppImage</string> <string name="appimg_wrong_type">只能匯入第 2 型 AppImage(此檔案為第 %1$d 型)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">可用空間不足:匯入需要約 %1$s</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">可用空間不足:更新需要約 %1$s</string> <string name="appimg_import_failed">匯入失敗</string> <string name="appimg_gone">該 AppImage 已不存在</string> <string name="appimg_update_failed">更新失敗</string> - <!-- %1$s: file name --> <string name="appimg_extracting">正在解壓縮 %1$s</string> <string name="appimg_extract_failed">無法解壓縮 AppImage</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">無法解壓縮 AppImage:%1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">安裝 Decky Loader 前,請先停止執行中的工作階段</string> <string name="deckymgr_download_failed">Decky Loader 下載失敗</string> <string name="deckymgr_size_mismatch">Decky Loader 的大小與發行版本中繼資料不符</string> @@ -1705,20 +1606,13 @@ <string name="deckymgr_remote_entry_invalid">remote_binary 項目無效</string> <string name="deckymgr_remote_name_invalid">遠端二進位檔名稱無效</string> <string name="deckymgr_remote_url_https">遠端二進位檔 URL 必須使用 HTTPS</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">遠端二進位檔 %1$s 的 SHA-256 雜湊值無效</string> <string name="deckymgr_bin_dir_failed">無法建立外掛程式的 bin 目錄</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">正在下載外掛程式二進位檔 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">無法下載外掛程式二進位檔 %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">外掛程式二進位檔校驗碼不符 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">無法將外掛程式二進位檔標示為可執行 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">無法取代外掛程式二進位檔 · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">無法安裝外掛程式二進位檔 · %1$s</string> <string name="deckymgr_folder_taken">外掛程式資料夾屬於另一個外掛程式</string> <string name="deckymgr_preserve_failed">無法保留現有的外掛程式</string> @@ -1726,20 +1620,14 @@ <string name="deckymgr_settings_failed">無法更新 Decky 外掛程式設定</string> <string name="deckymgr_restore_failed">無法還原現有的外掛程式</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">無法儲存無線偵錯身分</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">ADB 指令失敗(%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Android 未確認子處理程序限制已變更:%1$s</string> <string name="adbfix_no_pairing">尚未儲存任何無線偵錯配對</string> <string name="adbfix_enter_address">請輸入目前的無線偵錯 IP 位址和連接埠</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">等待配對彈出視窗逾時。請從 DroidDeck 再試一次。</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">無法尋找配對彈出視窗(%1$d)。請確認 Wi-Fi 已開啟。</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">無法尋找配對彈出視窗:%1$s</string> <string name="adbpair_six_digits">請輸入完整的 6 位數配對碼</string> <string name="adbpair_pairing">正在配對…</string> @@ -1753,27 +1641,22 @@ <string name="adbpair_title_pair">配對無線偵錯</string> <string name="adbpair_waiting_text">開啟無線偵錯,然後點擊「使用配對碼配對裝置」。</string> <string name="adbpair_waiting_long">開啟無線偵錯,然後點擊「使用配對碼配對裝置」。請留在「設定」中:配對碼要輸入在這則通知裡。</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">6 位數配對碼</string> <string name="adbpair_code_hint">點擊「輸入配對碼」,然後輸入「設定」中顯示的 6 位數字。</string> <string name="adbpair_title_code">輸入 Wi-Fi 配對碼</string> <string name="adbpair_title_retry">請再次輸入配對碼</string> - <!-- Notification action button --> <string name="adbpair_enter_code">輸入配對碼</string> <string name="adbpair_title_working">正在設定 Steam</string> <string name="adbpair_title_done">Steam 已就緒</string> <string name="adbpair_done_text">點擊即可返回 DroidDeck。現在可以關閉無線偵錯了。</string> <string name="adbpair_title_failed">無線偵錯設定已停止</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">下載回應 HTTP %1$d</string> <string name="selfinst_size_mismatch">下載的大小與發布的 APK 不符,已捨棄 - 請再試一次</string> <string name="selfinst_checksum_mismatch">下載的檔案與校驗碼不符,已捨棄 - 請再試一次</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">此更新適用於 Android 套件 %1$s,而非 %2$s</string> <string name="selfinst_metadata_signer">更新中繼資料未列出 DroidDeck 的發行簽署金鑰</string> <string name="selfinst_unreadable">Android 無法讀取下載的 APK</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">下載的 APK 適用於 %1$s,而非 %2$s</string> <string name="selfinst_version_mismatch">下載的 APK 標示 versionCode 為 %1$d,與發布的 %2$d 不符</string> <string name="selfinst_downgrade">Android 不會以 versionCode %1$d 覆蓋已安裝的 %2$d</string> @@ -1781,16 +1664,11 @@ <string name="selfinst_wrong_signer">下載的 APK 並非以 DroidDeck 的發行金鑰簽署</string> <string name="appupd_http">更新目錄回應 HTTP %1$d</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">正在啟動工作階段…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">已經過 %1$d:%2$02d · 仍在處理中</string> <string name="loadstate_steam_update">正在下載 Steam 用戶端更新 · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">正在下載 Steam 用戶端 · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">已有相容性工具正在安裝</string> <string name="pextra_stop_session">安裝相容性工具前,請先停止執行中的工作階段</string> <string name="pextra_no_release">找不到 %1$s 的 ARM64 發行版本</string> @@ -1807,48 +1685,38 @@ <string name="pextra_checksum_mismatch">校驗碼不符;已刪除下載內容</string> <string name="pextra_session_started">下載期間有工作階段啟動;請先停止它,再安裝相容性工具</string> <string name="pextra_installing">正在安裝 %1$s</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">%1$s 安裝失敗(結束代碼 %2$d)</string> <string name="pextra_could_not_install">無法安裝 %1$s</string> <string name="esync_fetching">正在取得 droiddeck-esync 套件</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">含 Linux 輔助程式</string> <string name="cmgr_fex_dlls_only">僅 DLL</string> <string name="cmgr_original_proton_updated">原始(上次換入後 Proton 已更新)</string> <string name="cmgr_changed_outside">已在「元件」之外變更</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">不存在</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">原始 %1$s</string> <string name="cmgr_original_from">原始版本來自 %1$s</string> <string name="cmgr_swap_queued">%1$s %2$s 會在 %3$s 上執行的遊戲關閉後換入。</string> <string name="cmgr_swap_done">%1$s %2$s 已換入 %3$s。下次啟動遊戲時生效。</string> <string name="cmgr_original_missing">未儲存該原始套件組合</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%2$s 的 %1$s 會在其上執行的遊戲關閉後還原為原始版本。</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">已從 %3$s 的原始版本還原 %2$s 的 %1$s。</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%2$s 正在使用 %1$s。請先為該 Proton 換入其他版本。</string> <string name="cmgr_package_not_stored">未儲存 %1$s</string> <string name="cmgr_deleted">已刪除 %1$s。</string> <string name="cmgr_original_kept">已安裝組建的原始版本會保留,以便還原。</string> <string name="cmgr_not_stored">未儲存</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">已刪除 %2$s 來自 %1$s 的原始版本。</string> <string name="cmgr_nightlies_only">只會從 Nightlies 發行版本下載</string> <string name="cmgr_list_predates_checksums">此套件清單早於校驗碼機制 - 請重新整理後再試一次</string> <string name="cmgr_checksum_mismatch">校驗碼不符</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">線性</string> <string name="sprefs_upscaler_nearest">最近鄰</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR(品質)</string> <string name="sprefs_upscaler_sharpen">僅銳化</string> <string name="sprefs_lod_bias_auto">自動(依縮放)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">未套用任何效果。</string> <string name="look_game_clarity">遊戲清晰</string> <string name="look_game_clarity_desc">銳化並稍微提高對比 - 適合日常使用。</string> @@ -1873,27 +1741,23 @@ <string name="look_anime_edge">動畫描邊</string> <string name="look_anime_edge_desc">卡通描邊加上銳化。</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">不是 Adreno GPU</string> <string name="gpuinfo_supported">受支援</string> <string name="gpuinfo_experimental">實驗性:其驅動程式為測試版本</string> <string name="gpuinfo_below_tested">低於已測試的硬體(Adreno 725 及更新機型):可能無法執行</string> <string name="gpuinfo_unsupported">不受支援:DroidDeck 需要 Adreno(Snapdragon)GPU</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">已達 GitHub 速率限制 - 請稍後再試</string> <string name="turnip_github_http">GitHub 回應 HTTP %1$d</string> <string name="turnip_download_http">下載回應 HTTP %1$d</string> <string name="turnip_list_predates_checksums">此驅動程式清單早於校驗碼機制 - 請重新整理後再試一次</string> <string name="turnip_checksum_mismatch">校驗碼不符 - 已捨棄下載內容</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">不是 AdrenoTools 驅動程式:%1$s</string> <string name="turnip_reject_no_meta">沒有 meta.json</string> <string name="turnip_reject_meta_unreadable">無法讀取 meta.json(%1$s)</string> <string name="turnip_reject_linux_driver">這是 Linux 執行環境驅動程式(請在「執行環境驅動程式」下匯入)</string> <string name="turnip_reject_wayland">這是 Wayland 遊戲驅動程式,此應用程式用不到</string> <string name="turnip_reject_no_library_name">meta.json 未指定 libraryName,因此無法交給 AdrenoTools</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.json 指定了 %1$s,但 zip 中沒有此檔案</string> <string name="turnip_reject_not_aarch64">%1$s 不是 64 位元 AArch64 ELF 共用程式庫</string> <string name="turnip_reject_glibc">%1$s 連結 glibc - 這是「-Linux」Turnip,應放在「執行環境驅動程式」下</string> @@ -1904,90 +1768,61 @@ <string name="bundle_entry_outside">不是驅動程式套件組合:%1$s 指向組合外部</string> <string name="bundle_too_large">不是驅動程式套件組合:解壓縮後超過 %1$d MB</string> - <!-- Game storage --> <string name="gstore_sd_card">SD 卡</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">資料夾</string> <string name="logshare_chooser">分享工作階段日誌</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">需要 Android 10 或更新版本(具備色彩空間的顯示圖層)</string> <string name="hdr_none">此顯示器未回報支援 HDR</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">此顯示器支援 %1$s,但不支援 HDR10</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">此 GPU</string> <string name="phantom_title_on">子處理程序限制已開啟</string> <string name="phantom_title_unset">子處理程序限制未設定</string> <string name="phantom_title_unreadable">無法檢查子處理程序限制</string> <string name="phantom_title_off">子處理程序限制已關閉</string> <string name="phantom_title_not_required">不需要變更子處理程序限制</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s 如果無法這麼做,請將此裝置連接到裝有 ADB 的電腦並執行下方指令,然後返回這裡重新檢查。</string> <string name="phantom_instructions_off">可以啟動 Steam 工作階段。</string> <string name="phantom_instructions_not_applicable">此 Android 版本不使用這項限制。</string> <string name="phantom_fix_toggle">在開發人員選項中開啟「停用子處理程序限制」,之後請保持開發人員選項開啟。</string> <string name="phantom_fix_no_toggle">此 Android 版本沒有這個開關。請使用無線偵錯,直接在此裝置上變更。</string> <string name="upd_block_no_apk">此組建版本沒有適用於此 DroidDeck 複本的下載檔案。</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">Android 不會以此版本覆蓋目前的組建,因為它的 versionCode %1$d 低於已安裝的 %2$d。</string> <string name="upd_fail_downgrade">Android 不會以舊版本覆蓋新版本。請先留在此組建版本,等更新頻道趕上。</string> <string name="upd_fail_signature">此 DroidDeck 複本的簽章不同(是本機組建的嗎?),因此 Android 無法直接覆寫更新。</string> <string name="upd_fail_storage">可用空間不足,無法更新。</string> <string name="upd_fail_aborted">已取消更新。</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">未能安裝更新:%1$s</string> <string name="upd_fail_generic">未能安裝更新。</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">%1$s 壓縮檔</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">壓縮檔</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$s 尚未啟動過</string> <string name="saves_none_found">找不到 %1$s 的存檔</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$s 中沒有 drive_c 資料夾,因此不是 GameHub 或 Winlator 存檔壓縮檔</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">無法在 %1$s 中建立資料夾</string> <string name="gstore_cannot_write">無法寫入 %1$s</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">無法寫入 %1$s(%2$s)</string> <string name="gfiles_folder_unavailable">無法使用資料夾</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">已有其他檔案使用 %1$s</string> <string name="gfiles_art_unavailable">無法使用圖片資料夾</string> <string name="gfiles_unrelated_inventory">資料夾中有不相關的同步清單</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">無法移除 %1$s</string> <string name="gfiles_outside_folder">檔案位於匯出資料夾之外</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX(未標示版本名稱的組建)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">%1$s 中沒有 profile.json</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$s 不是 Linux 元件套件(類型 %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · 可用 %2$s</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">自動</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">更新目錄未通過檢查(%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">DroidDeck 子處理程序設定</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">已登入 · 離線模式</string> <string name="setup_signed_in_unnamed">已登入</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">內建</string> <string name="driver_tag_downloaded">已下載</string> <string name="driver_tag_imported">已匯入</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">ANDROID + LINUX</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s。%2$s\n\n%3$s</string> <string name="display_session">工作階段顯示</string> <string name="display_resolution">解析度</string> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3415cc58d..cc33a651f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1121,11 +1121,8 @@ <string name="proton_installed">Installed %1$s</string> <string name="proton_queued">Pending from an earlier request</string> <string name="proton_not_installed">Not installed</string> - <!-- %1$s is the newer release's tag --> <string name="proton_update_available">Update available → %1$s</string> - <!-- %1$s is the Proton's name (GE-Proton, proton-cachyos, DroidDeck-Proton) --> <string name="proton_remove_older">Remove old %1$s versions</string> - <!-- %1$s lists the folders that are removed --> <string name="proton_remove_older_body">Removes %1$s. Games set to these versions must be switched to another Proton.</string> <string name="proton_remove_body">Games set to this version must be switched to another Proton.</string> <string name="proton_install_now">Install now</string> @@ -1183,16 +1180,12 @@ <string name="game_file_exported">Game files saved to %1$s</string> <string name="game_frontend_files">Frontend files</string> - <!-- Launcher, session and menus --> - <!-- Launcher (MainActivity) --> <string name="main_wait_runtime">Wait for the runtime operation to finish</string> <string name="main_wait_proton_install">Wait for the compatibility tool install to finish</string> <string name="main_wait_desktop_install">Wait for the desktop app install to finish</string> <string name="main_wait_install_game">Wait for the install to finish before launching a game</string> <string name="main_saves_close_session">Close the Steam session first, so the game can\'t save over the import</string> - <!-- Busy line while saves are imported; %1$s is the game's name --> <string name="main_saves_importing">Importing into %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name --> <plurals name="main_saves_imported"> <item quantity="one">Imported %1$d file from the %2$s into %3$s</item> <item quantity="other">Imported %1$d files from the %2$s into %3$s</item> @@ -1202,16 +1195,12 @@ <item quantity="other">Imported %1$d files from the %2$s into %3$s. Old saves backed up to Download/DroidDeck/Saves/backups</item> </plurals> <string name="main_saves_pick_zip">Choose a save zip for %1$s</string> - <!-- Busy line while saves are exported; %1$s is the game's name --> <string name="main_saves_exporting">Exporting %1$s</string> - <!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path --> <plurals name="main_saves_exported"> <item quantity="one">Exported %1$d file as a %2$s: %3$s</item> <item quantity="other">Exported %1$d files as a %2$s: %3$s</item> </plurals> - <!-- %1$s is the game's name, %2$s the save layout --> <string name="main_saves_pick_folder">Choose where to save %1$s (%2$s)</string> - <!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error --> <string name="main_action_failed">%1$s failed: %2$s</string> <string name="main_no_logs_yet">No session logs yet: run a session first.</string> <string name="main_no_logs_to_clear">No session logs to clear.</string> @@ -1225,68 +1214,55 @@ <string name="main_adb_copied">ADB command copied</string> <string name="main_pick_roms">Choose the ROMs folder</string> <string name="main_non_adreno_title">Not an Adreno GPU</string> - <!-- %1$s is the GPU's name, %2$.0f the download size in MB --> <string name="main_non_adreno_text">Turnip supports Adreno GPUs. On %1$s, Steam may show a black screen. Download: %2$.0f MB.</string> <string name="main_install_anyway">Install anyway</string> <string name="main_remove_runtime_title">Remove Linux runtime</string> <string name="main_remove_runtime_text">This deletes the runtime, the Steam client inside it, and every game installed there.</string> - <!-- Toast; %1$s is a list of components, separated by commas --> <string name="main_applied">Applied: %1$s</string> <string name="android_app_open_failed_primary">Could not open %1$s on the primary screen</string> - <!-- %1$s is the app, %2$s the name of the display --> <string name="android_app_open_failed_on">Could not open %1$s on %2$s</string> - <!-- %1$s is the app, %2$d the display's number --> <string name="android_app_open_failed_display">Could not open %1$s on display %2$d</string> - <!-- Components page busy lines, shown with … after them --> <string name="comp_busy_importing">Importing</string> <string name="comp_busy_swapping">Swapping</string> <string name="comp_busy_restoring">Restoring</string> <string name="comp_busy_cancelling">Cancelling</string> <string name="comp_busy_deleting">Deleting</string> <string name="comp_cannot_read">cannot read the file</string> - <!-- %1$s is a version or a name --> <string name="comp_imported">Imported %1$s</string> <string name="comp_swap_cancelled">The waiting swap was cancelled.</string> <string name="comp_pick_wcp">Choose a component package (-linux .wcp)</string> <string name="comp_nightlies_unreachable">The Nightlies could not be reached</string> - <!-- %1$s is the package version --> <string name="comp_stored">Stored %1$s</string> <string name="comp_download_failed">Download failed: %1$s</string> <string name="drivers_pick_linux">Choose a Linux runtime driver (-Linux zip or Android + Linux bundle)</string> <string name="drivers_pick_android">Choose a display driver (AdrenoTools zip or Android + Linux bundle)</string> <string name="drivers_pick_any">Choose a driver zip or an Android + Linux bundle</string> - <!-- %1$s is a desktop app's name, %2$s what went wrong --> <string name="main_pkg_problem">%1$s: %2$s</string> <string name="main_removing_named">Removing %1$s…</string> <string name="main_forgetting_named">Forgetting %1$s…</string> <string name="main_pick_game_storage">Choose the game storage folder</string> <string name="main_pick_decky_zip">Choose a Decky plugin ZIP</string> <string name="main_storage_not_usable">Not usable: %1$s</string> - <!-- What is running, when it is a program without a known name --> <string name="main_running_program">Program</string> <string name="main_removing_runtime">Removing Linux runtime</string> <string name="main_settings_main_screen">Could not open Settings on the bottom screen; opened it on the main screen.</string> <string name="main_open_dev_options">Open Developer options in Settings</string> - <!-- Updates --> <string name="upd_unknown_apps_failed">Couldn\'t open Android\'s \"Install unknown apps\" setting</string> <string name="upd_couldnt_check">Couldn\'t check: %1$s</string> <string name="upd_no_download">This build has no download for this copy of DroidDeck</string> <string name="upd_stop_first">Stop the running session first, then update</string> - <!-- Update stages, shown with … after them --> <string name="upd_stage_downloading">Downloading</string> <string name="upd_stage_installing">Installing</string> <string name="upd_stage_confirm">Waiting for you to confirm</string> <string name="upd_failed">The update failed</string> <string name="upd_not_offered">Android didn\'t offer to install the update</string> - <!-- Decky --> <string name="decky_install_failed">Decky install failed: %1$s</string> <string name="decky_plugin_starting">Starting plugin import…</string> <string name="decky_plugin_failed">Plugin import failed: %1$s</string> <string name="decky_plugin_installed">Plugin installed. Restart Steam to load it.</string> - <!-- GPU drivers --> <string name="drivers_not_checked">Not checked yet - tap refresh to look for new drivers</string> <string name="drivers_bundle_sets_display">Android + Linux: sets the display driver with it</string> <string name="drivers_bundle_sets_runtime">Android + Linux: sets the runtime driver with it</string> @@ -1294,16 +1270,13 @@ <string name="drivers_system_vulkan">system Vulkan: no bundled build for this GPU</string> <string name="drivers_bundled">%1$s (bundled)</string> <string name="drivers_manual">Manual</string> - <!-- Driver mode, then the driver pair in use --> <string name="drivers_summary">%1$s · %2$s</string> <string name="drivers_half_published">Only one half is published right now</string> <string name="drivers_pair_size">%1$.0f MB for both</string> - <!-- %1$s says why (the GPU's support status) --> <string name="drivers_none_to_set">No drivers to set: %1$s</string> <string name="drivers_checking_latest">Checking for the latest drivers…</string> <string name="drivers_up_to_date_check_failed">Up to date (couldn\'t check: %1$s)</string> <string name="drivers_recommended_unpublished">The recommended drivers aren\'t published right now</string> - <!-- %1$s is the driver pair's name, %2$s its version --> <string name="drivers_downloading_pair">Downloading %1$s %2$s…</string> <string name="drivers_updated_to">Updated to %1$s %2$s.</string> <string name="drivers_updated_to_restart">Updated to %1$s %2$s. The display driver applies after DroidDeck restarts.</string> @@ -1318,9 +1291,7 @@ <string name="drivers_using_bundle_restart">Using %1$s as both the runtime and display driver. The display driver applies after DroidDeck restarts.</string> <string name="drivers_deleted_bundle">Deleted %1$s (both drivers)</string> <string name="drivers_deleted">Deleted %1$s</string> - <!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) --> <string name="drivers_release_status">Latest: %1$s · checked %2$s</string> - <!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked --> <string name="drivers_release_status_failed">Latest: %1$s · %2$s unreachable · checked %3$s</string> <plurals name="drivers_ago_minutes"> <item quantity="one">%1$d min ago</item> @@ -1336,33 +1307,25 @@ </plurals> <string name="drivers_checking_sources">Checking Banners-Turnip, WinNative and DroidDeck…</string> <string name="drivers_check_failed">check failed</string> - <!-- %1$s is the driver's name --> <string name="drivers_installed_pick_either">Installed %1$s - pick it in either list to set both drivers</string> <string name="drivers_installed_pick">Installed %1$s - pick it in the menu</string> - <!-- Session (SessionActivity) --> <string name="session_wait_removal">Wait for runtime removal to finish</string> - <!-- Drawer title when the running program has no known name --> <string name="session_title_game">Game</string> - <!-- Shown as "Auto (touchpad)" in the drawer's touch setting --> <string name="session_touch_auto_touchpad">touchpad</string> - <!-- Shown as "Auto (direct)" in the drawer's touch setting --> <string name="session_touch_auto_direct">direct</string> <string name="session_swap_failed">Swap failed: %1$s</string> <string name="session_profile_save_failed">Profile save failed: %1$s</string> <string name="session_desktop_paused">The desktop is paused</string> - <!-- %1$s is the program's name --> <string name="session_named_paused">%1$s is paused</string> <string name="session_program_paused">The program is paused</string> <string name="session_starting_desktop">Starting the desktop</string> - <!-- %1$s is the program's name --> <string name="session_starting_named">Starting %1$s</string> <string name="session_starting_program">Starting the program</string> <string name="session_no_logs">No logs for this session.</string> <string name="session_logs_archive_failed">Could not create the session log archive.</string> <string name="session_logs_share_failed">Could not share the session logs.</string> <string name="session_starting_session">Starting the session…</string> - <!-- Loading screen lines while packages download; %1$d and %2$d are megabytes --> <string name="session_runtime_downloading">downloading the Linux runtime</string> <string name="session_runtime_downloading_mb">downloading the Linux runtime · %1$d of %2$d MB</string> <string name="session_runtime_checking">checking the Linux runtime</string> @@ -1377,28 +1340,21 @@ <string name="session_proton_unpacking">unpacking Proton Experimental (ARM64)</string> <string name="session_runtime_catalog_unreachable">Could not reach the runtime catalog. Check the connection and press Play again.</string> <string name="session_runtime_install_failed">The Linux runtime did not install. Check the runtime in Setup and press Play again.</string> - <!-- %1$s says what went wrong --> <string name="session_desktop_install_failed">The desktop did not install (%1$s). Check the connection and press Desktop again.</string> - <!-- %1$s is the program's name, or Steam --> <string name="session_ended_named">%1$s stopped unexpectedly. Share the logs with a bug report, or try again.</string> <string name="session_ended_program">The program stopped unexpectedly. Share the logs with a bug report, or try again.</string> <string name="session_ended_desktop">The desktop stopped unexpectedly. Share the logs with a bug report, or try again.</string> - <!-- %1$d is the exit status, %2$s the log file's path --> <string name="session_ended_detail">Exit status %1$d · %2$s</string> <string name="session_ended_no_log">no log</string> - <!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings --> <plurals name="session_hint_enosys"> <item quantity="one">The log shows %1$d \"Function not implemented\" error: proot\'s seccomp acceleration is failing a helper on this device. Try %2$s → \"%3$s\" (or \"%4$s\") and start again.</item> <item quantity="other">The log shows %1$d \"Function not implemented\" errors: proot\'s seccomp acceleration is failing a helper on this device. Try %2$s → \"%3$s\" (or \"%4$s\") and start again.</item> </plurals> - <!-- Screens, files, controller and store --> - <!-- Mode settings --> <string name="mode_decky_plugins">Plugins</string> <string name="mode_decky_plugins_hint">Install a plugin ZIP for the next Steam session.</string> <string name="mode_decky_plugin_downloading">Downloading…</string> <string name="mode_decky_plugin_install_zip">Install from ZIP</string> - <!-- Store --> <string name="store_cat_games">Games</string> <string name="store_cat_audio_video">Audio & Video</string> <string name="store_cat_graphics">Graphics</string> @@ -1411,21 +1367,14 @@ <string name="store_cat_utilities">Utilities</string> <string name="store_section_popular">Popular</string> <string name="store_section_trending">Trending</string> - <!-- Store front-page section of recently added apps --> <string name="store_section_new">New</string> - <!-- Store front-page section of recently updated apps --> <string name="store_section_updated">Updated</string> <string name="store_section_loading">loading…</string> <string name="store_flatpak_setup">Flatpak setup</string> - <!-- %1$s is what failed (an app name or "Flatpak setup"), %2$s the error --> <string name="store_problem">%1$s: %2$s</string> <string name="store_updates_check_failed">Could not check Flathub for updates</string> - <!-- Library tiles --> - <!-- Subtitle of a game tile whose cover art was found --> <string name="content_rom_installed">installed</string> - <!-- Subtitle of a game that is a folder rather than a single file --> <string name="content_rom_folder">folder</string> - <!-- Launcher themes --> <string name="theme_graphite">Graphite</string> <string name="theme_graphite_detail">Graphite surfaces, with the logo\'s own orb blue for everything selected, focused or pressed.</string> <string name="theme_paper">Paper on black</string> @@ -1434,7 +1383,6 @@ <string name="theme_icon_blue_detail">Black ground, white type, the blue for everything selected, focused or pressed.</string> <string name="theme_electric">Electric navy</string> <string name="theme_electric_detail">The blue brighter, on surfaces that lean navy instead of grey.</string> - <!-- PC keyboard key caps (keep them short; they label physical PC keys) --> <string name="pckb_key_esc">Esc</string> <string name="pckb_key_ins">Ins</string> <string name="pckb_key_del">Del</string> @@ -1452,22 +1400,17 @@ <string name="pckb_key_alt">Alt</string> <string name="pckb_key_space">Space</string> <string name="pckb_key_altgr">AltGr</string> - <!-- Terminal key strip on the second screen --> <string name="second_key_tab">Tab</string> - <!-- A modifier key (%1$s, e.g. Ctrl) that is currently held --> <string name="second_key_on">%1$s ✓</string> <string name="second_key_ctrl_c">Ctrl+C</string> <string name="second_key_ctrl_d">Ctrl+D</string> - <!-- Game file sync errors --> <string name="game_sync_err_runtime">Install the runtime first</string> <string name="game_sync_err_steam_library">Steam library is unavailable</string> <string name="game_sync_err_previous_sd">Previous SD library is unavailable</string> <string name="game_sync_err_game_storage">Game storage is unavailable</string> <string name="game_sync_err_added_games">Added games folder is unavailable</string> <string name="game_sync_err_manifests">Steam manifests are unavailable</string> - <!-- The systems RetroArch emulates, shown under its name --> <string name="content_system_many">Many systems</string> - <!-- File manager --> <string name="fm_drive_internal">Internal</string> <string name="fm_drive_linux">Linux system</string> <string name="fm_drive_sd">SD card</string> @@ -1485,7 +1428,6 @@ <string name="fm_no_favorites">No favorites yet - pin a folder with its ⋮ menu to jump back here fast.</string> <string name="fm_unpin_favorite">Remove from favorites</string> <string name="fm_size_zero">0 bytes</string> - <!-- Size unit for sizes under 1 KB, shown after a number --> <string name="fm_unit_bytes">bytes</string> <string name="fm_prop_location">Location</string> <string name="fm_prop_type">Type</string> @@ -1493,7 +1435,6 @@ <string name="fm_prop_modified">Modified</string> <string name="fm_type_folder">Folder</string> <string name="fm_type_file">File</string> - <!-- File type in Properties; %1$s is the file extension in capitals, e.g. TXT --> <string name="fm_type_ext_file">%1$s file</string> <string name="fm_read_only">Read-only</string> <string name="fm_read_only_hint">Stops games (and Wine) from overwriting or deleting this file.</string> @@ -1507,15 +1448,11 @@ <string name="fm_delete_failed">Delete failed</string> <string name="fm_op_moving">Moving...</string> <string name="fm_op_copying">Copying...</string> - <!-- %1$s is the file name --> <string name="fm_op_moving_item">Moving - %1$s</string> <string name="fm_op_copying_item">Copying - %1$s</string> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_moving_batch">Moving %1$d/%2$d - %3$s</string> <string name="fm_op_copying_batch">Copying %1$d/%2$d - %3$s</string> - <!-- Paste result; %1$d items done, %2$d items failed --> <string name="fm_paste_failed">%1$d done, %2$d failed</string> - <!-- Paste result; %1$d items done, %2$d items skipped --> <string name="fm_paste_skipped">%1$d done, %2$d skipped</string> <plurals name="fm_paste_moved"> <item quantity="one">%1$d item moved</item> @@ -1525,7 +1462,6 @@ <item quantity="one">%1$d item copied</item> <item quantity="other">%1$d items copied</item> </plurals> - <!-- %1$s is a file or folder name --> <string name="fm_already_exists">\"%1$s\" already exists</string> <string name="fm_op_renaming">Renaming...</string> <string name="fm_rename_failed">Rename failed</string> @@ -1536,19 +1472,16 @@ <string name="fm_create">Create</string> <string name="fm_new_name">New name</string> <string name="fm_delete_title">Delete?</string> - <!-- %1$s is a file or folder name --> <string name="fm_delete_text">Delete \"%1$s\" permanently?</string> <plurals name="fm_delete_items_title"> <item quantity="one">Delete %1$d item?</item> <item quantity="other">Delete %1$d items?</item> </plurals> <string name="fm_cannot_undo">This can\'t be undone.</string> - <!-- After a list of the first five names in a delete confirmation --> <plurals name="fm_and_more"> <item quantity="one">…and %1$d more</item> <item quantity="other">…and %1$d more</item> </plurals> - <!-- %1$d is the item number, %2$d the number of items, %3$s the file name --> <string name="fm_op_deleting_batch">Deleting %1$d/%2$d - %3$s</string> <plurals name="fm_delete_some_failed"> <item quantity="one">%1$d couldn\'t be deleted</item> @@ -1563,7 +1496,6 @@ <string name="fm_conflict_skip">Skip</string> <string name="fm_select_this_folder">Select this folder</string> <string name="fm_back">Back</string> - <!-- %1$s is a storage name, e.g. SD card --> <string name="fm_drive_not_readable">%1$s is mounted but not readable right now</string> <string name="fm_list_view">List view</string> <string name="fm_grid_view">Grid view</string> @@ -1578,42 +1510,32 @@ <string name="fm_hide_favorites">Hide favorites</string> <string name="fm_show_favorites">Show favorites</string> <string name="fm_filter_folder">Filter this folder</string> - <!-- Free space on the storage being browsed; %1$s is a size, e.g. 12.50 GB --> <string name="fm_free_space">%1$s free</string> <string name="fm_selected_count">%1$d selected</string> - <!-- Selection bar: clear the selection --> <string name="fm_select_none">None</string> - <!-- Selection bar: select every item --> <string name="fm_select_all">All</string> <string name="fm_paste">Paste</string> <plurals name="fm_items"> <item quantity="one">%1$d item</item> <item quantity="other">%1$d items</item> </plurals> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_here">Paste %1$s here</string> - <!-- %1$s is a file name or a number of items --> <string name="fm_paste_move_here">Paste %1$s (move) here</string> <string name="fm_rail_title">Files</string> <string name="fm_rail_storage">STORAGE</string> - <!-- Rail section of shortcut folders (Downloads, ROMs, …) --> <string name="fm_rail_quick">QUICK</string> <string name="fm_rail_favorites">FAVORITES</string> <string name="fm_rail_downloads">Downloads</string> - <!-- Downloads shortened to fit under an icon (about 8 characters) --> <string name="fm_rail_downloads_short">Downlds</string> <string name="fm_rail_roms">ROMs</string> <string name="fm_rail_session_logs">Session logs</string> <string name="fm_rail_pictures">Pictures</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_added">Added \"%1$s\" to Favorites</string> - <!-- %1$s is a folder name --> <string name="fm_favorite_removed">Removed \"%1$s\" from Favorites</string> <string name="fm_empty_directory">Empty directory</string> <string name="fm_up_one_level">Up one level</string> <string name="fm_choose_folder">Choose a folder</string> <string name="fm_choose_file">Choose a file</string> - <!-- On-screen controller tint colors --> <string name="ctrl_tint_steam_blue">Steam blue</string> <string name="ctrl_tint_sky">Sky</string> <string name="ctrl_tint_white">White</string> @@ -1622,7 +1544,6 @@ <string name="ctrl_tint_amber">Amber</string> <string name="ctrl_tint_red">Red</string> <string name="ctrl_tint_pink">Pink</string> - <!-- On-screen buttons that can be remapped --> <string name="ctrl_button_a">A button</string> <string name="ctrl_button_b">B button</string> <string name="ctrl_button_x">X button</string> @@ -1633,7 +1554,6 @@ <string name="ctrl_button_rt">Right trigger</string> <string name="ctrl_button_view">View button</string> <string name="ctrl_button_menu">Menu button</string> - <!-- What a remapped on-screen button sends; letters and Steam are controller button names --> <string name="ctrl_target_a" translatable="false">A</string> <string name="ctrl_target_b" translatable="false">B</string> <string name="ctrl_target_x" translatable="false">X</string> @@ -1645,27 +1565,21 @@ <string name="ctrl_target_l3" translatable="false">L3</string> <string name="ctrl_target_r3" translatable="false">R3</string> <string name="ctrl_target_steam" translatable="false">Steam</string> - <!-- Controller View button --> <string name="ctrl_target_view">View</string> - <!-- Controller Menu button --> <string name="ctrl_target_menu">Menu</string> <string name="ctrl_target_up">D-pad up</string> <string name="ctrl_target_down">D-pad down</string> <string name="ctrl_target_left">D-pad left</string> <string name="ctrl_target_right">D-pad right</string> - <!-- The on-screen button is not shown --> <string name="ctrl_target_hidden">Hidden</string> <string name="ctrl_editor_drag">Drag to move</string> <string name="ctrl_editor_snap">Snap</string> <string name="ctrl_editor_save">Save</string> - <!-- What the second screen shows during a session --> <string name="second_mode_none">None</string> <string name="second_mode_keyboard_trackpad">Keyboard + trackpad</string> <string name="second_mode_terminal">Terminal</string> <string name="second_mode_deck_controls">Deck grips + trackpads</string> - <!-- Runtime, updates and setup helpers --> - <!-- Linux runtime install / removal (LinuxRuntimeInstaller, RuntimeInstallService) --> <string name="rtinst_removing">Removing Linux runtime</string> <plurals name="rtinst_removing_entries"> <item quantity="one">Removing Linux runtime · %1$d entry</item> @@ -1681,63 +1595,47 @@ <string name="rtinst_title_installing">Installing the Linux runtime</string> <string name="rtinst_title_removing">Removing the Linux runtime</string> - <!-- Hosted desktop and emulator packages (DesktopCatalog) --> <string name="deskpkg_runtime_missing">The Linux runtime is not installed</string> - <!-- %1$s: package name --> <string name="deskpkg_no_checksum">The catalog has no checksum for %1$s</string> <string name="deskpkg_checksum_mismatch">Checksum mismatch - nothing was changed</string> - <!-- %1$s: package name --> <string name="deskpkg_installing">Installing %1$s</string> <string name="deskpkg_place_failed">Could not place the AppImage</string> <string name="deskpkg_extract_failed">Extraction failed</string> <string name="deskpkg_install_failed">Install failed</string> - <!-- Flatpak store operations (FlatpakManager) --> <string name="flatpakmgr_setup_failed">Flatpak setup failed (exit %1$d)</string> <string name="flatpakmgr_setup_unfinished">Flatpak setup did not finish</string> <string name="flatpakmgr_busy">Another store task is running</string> <string name="flatpakmgr_not_ready">Set up Flatpak first</string> <string name="flatpakmgr_starting">Starting</string> <string name="flatpakmgr_exit_status">Flatpak exited with status %1$d</string> - <!-- %1$s: an app or runtime name, e.g. "SuperTux" or "GNOME runtime 50" --> <string name="flatpakmgr_installing">Installing %1$s</string> <string name="flatpakmgr_updating">Updating %1$s</string> <string name="flatpakmgr_removing">Removing %1$s</string> - <!-- %1$s: an app or runtime name; %2$d of %3$d: this step and the number of steps --> <string name="flatpakmgr_installing_step">Installing %1$s (%2$d/%3$d)</string> <string name="flatpakmgr_updating_step">Updating %1$s (%2$d/%3$d)</string> <string name="flatpakmgr_removing_step">Removing %1$s (%2$d/%3$d)</string> - <!-- Names for Flatpak runtime parts, shown inside "Installing %1$s" --> <string name="flatpakmgr_ref_translations">translations</string> <string name="flatpakmgr_ref_graphics_drivers">graphics drivers</string> <string name="flatpakmgr_ref_media_codecs">media codecs</string> - <!-- %1$s: runtime vendor, e.g. GNOME, KDE, Freedesktop --> <string name="flatpakmgr_ref_runtime">%1$s runtime</string> - <!-- %1$s: runtime vendor, e.g. GNOME; %2$s: its version, e.g. 50 --> <string name="flatpakmgr_ref_runtime_branch">%1$s runtime %2$s</string> - <!-- Imported AppImages (AppImageManager) --> <string name="appimg_too_small">The file is too small to be an AppImage</string> - <!-- %1$s: the system's reason --> <string name="appimg_unreadable">The file cannot be read: %1$s</string> <string name="appimg_not_linux">This is not an AppImage (it is not a Linux program)</string> <string name="appimg_wrong_arch">This AppImage is built for neither ARM64 (aarch64) nor x86 PCs</string> <string name="appimg_not_appimage">This is a Linux program but not an AppImage</string> <string name="appimg_wrong_type">Only type 2 AppImages can be imported (this one is type %1$d)</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_import">Not enough free space: importing needs about %1$s</string> - <!-- %1$s: a size such as "3.2 GB" --> <string name="appimg_no_space_update">Not enough free space: updating needs about %1$s</string> <string name="appimg_import_failed">Import failed</string> <string name="appimg_gone">That AppImage is gone</string> <string name="appimg_update_failed">Update failed</string> - <!-- %1$s: file name --> <string name="appimg_extracting">Extracting %1$s</string> <string name="appimg_extract_failed">The AppImage could not be extracted</string> - <!-- %1$s: the last line the extractor printed --> <string name="appimg_extract_failed_detail">The AppImage could not be extracted: %1$s</string> - <!-- Decky Loader and plugin installs (DeckyManager) --> <string name="deckymgr_stop_session_loader">Stop the active session before installing Decky Loader</string> <string name="deckymgr_download_failed">Decky Loader download failed</string> <string name="deckymgr_size_mismatch">Decky Loader size did not match the release metadata</string> @@ -1773,20 +1671,13 @@ <string name="deckymgr_remote_entry_invalid">Invalid remote_binary entry</string> <string name="deckymgr_remote_name_invalid">Invalid remote binary name</string> <string name="deckymgr_remote_url_https">Remote binary URL must use HTTPS</string> - <!-- %1$s: binary file name --> <string name="deckymgr_remote_hash_invalid">Remote binary %1$s has an invalid SHA-256 hash</string> <string name="deckymgr_bin_dir_failed">Could not create plugin bin directory</string> - <!-- %1$s: binary file name --> <string name="deckymgr_downloading_binary">Downloading plugin binary · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_download_failed">Could not download plugin binary %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_checksum_mismatch">Plugin binary checksum mismatch · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_not_executable">Could not mark plugin binary executable · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_replace_failed">Could not replace plugin binary · %1$s</string> - <!-- %1$s: binary file name --> <string name="deckymgr_binary_install_failed">Could not install plugin binary · %1$s</string> <string name="deckymgr_folder_taken">Plugin folder belongs to another plugin</string> <string name="deckymgr_preserve_failed">Could not preserve the existing plugin</string> @@ -1794,20 +1685,14 @@ <string name="deckymgr_settings_failed">Could not update Decky plugin settings</string> <string name="deckymgr_restore_failed">Could not restore the existing plugin</string> - <!-- Child-process limit change over Wireless debugging (WirelessAdbFix) --> <string name="adbfix_save_failed">Could not save Wireless debugging identity</string> - <!-- %1$d: the command's exit status --> <string name="adbfix_command_failed">ADB command failed (%1$d)</string> - <!-- %1$s: what Android answered --> <string name="adbfix_not_confirmed">Android did not confirm the child-process limit was changed: %1$s</string> <string name="adbfix_no_pairing">No Wireless debugging pairing is saved</string> <string name="adbfix_enter_address">Enter the current Wireless debugging IP address & Port</string> - <!-- Wireless debugging pairing notification (WirelessAdbPairingService) --> <string name="adbpair_timed_out">Timed out waiting for the pairing pop-up. Try again from DroidDeck.</string> - <!-- %1$d: Android's error code --> <string name="adbpair_discovery_failed_code">Could not look for the pairing pop-up (%1$d). Check that Wi-Fi is on.</string> - <!-- %1$s: the system's reason --> <string name="adbpair_discovery_failed">Could not look for the pairing pop-up: %1$s</string> <string name="adbpair_six_digits">Enter all 6 digits of the code</string> <string name="adbpair_pairing">Pairing…</string> @@ -1821,27 +1706,22 @@ <string name="adbpair_title_pair">Pair Wireless debugging</string> <string name="adbpair_waiting_text">Turn on Wireless debugging, then tap “Pair device with pairing code”.</string> <string name="adbpair_waiting_long">Turn on Wireless debugging, then tap “Pair device with pairing code”. Stay in Settings: the code goes in this notification.</string> - <!-- Label of the notification's reply field --> <string name="adbpair_code_label">6-digit pairing code</string> <string name="adbpair_code_hint">Tap Enter code and type the 6 digits shown in Settings.</string> <string name="adbpair_title_code">Enter the Wi-Fi pairing code</string> <string name="adbpair_title_retry">Try the code again</string> - <!-- Notification action button --> <string name="adbpair_enter_code">Enter code</string> <string name="adbpair_title_working">Setting up Steam</string> <string name="adbpair_title_done">Steam is ready</string> <string name="adbpair_done_text">Tap to go back to DroidDeck. You can turn Wireless debugging off now.</string> <string name="adbpair_title_failed">Wireless debugging setup stopped</string> - <!-- DroidDeck's own updates (SelfInstaller, AppUpdates) --> <string name="selfinst_http">The download answered HTTP %1$d</string> <string name="selfinst_size_mismatch">The download size didn\'t match the published APK and was discarded - try again</string> <string name="selfinst_checksum_mismatch">The download didn\'t match its checksum and was discarded - try again</string> - <!-- %1$s: the update's Android package name; %2$s: this app's --> <string name="selfinst_wrong_package">The update is for Android package %1$s, not %2$s</string> <string name="selfinst_metadata_signer">The update metadata doesn\'t name DroidDeck\'s release signing key</string> <string name="selfinst_unreadable">Android couldn\'t read the downloaded APK</string> - <!-- %1$s: the APK's Android package name; %2$s: this app's --> <string name="selfinst_apk_wrong_package">The downloaded APK is for %1$s, not %2$s</string> <string name="selfinst_version_mismatch">The downloaded APK says versionCode %1$d, not the published %2$d</string> <string name="selfinst_downgrade">Android won\'t install versionCode %1$d over the installed %2$d</string> @@ -1849,16 +1729,11 @@ <string name="selfinst_wrong_signer">The downloaded APK isn\'t signed with DroidDeck\'s release key</string> <string name="appupd_http">The update catalog answered HTTP %1$d</string> - <!-- Session, GPU and display --> - <!-- Session loading screen --> <string name="loadstate_starting">Starting the session…</string> - <!-- %1$d minutes, %2$02d seconds since the session started --> <string name="loadstate_elapsed">%1$d:%2$02d elapsed · still working</string> <string name="loadstate_steam_update">Downloading the Steam client update · %1$d%%</string> - <!-- %1$s is the client's own download progress, as it prints it --> <string name="loadstate_steam_client">Downloading the Steam client · %1$s</string> - <!-- Installing GE-Proton / proton-cachyos / DroidDeck-Proton (%1$s is the tool's name, %2$s its release tag) --> <string name="pextra_busy">A compatibility tool install is already running</string> <string name="pextra_stop_session">Stop the active session before installing compatibility tools</string> <string name="pextra_no_release">Could not find an ARM64 release for %1$s</string> @@ -1875,48 +1750,38 @@ <string name="pextra_checksum_mismatch">Checksum mismatch; the download was deleted</string> <string name="pextra_session_started">A session started during the download; stop it before installing compatibility tools</string> <string name="pextra_installing">Installing %1$s</string> - <!-- %2$d is the installer's exit status --> <string name="pextra_install_failed">%1$s installation failed (exit %2$d)</string> <string name="pextra_could_not_install">Could not install %1$s</string> <string name="esync_fetching">Fetching droiddeck-esync pack</string> - <!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton --> <string name="cmgr_fex_linux_helpers">with Linux helpers</string> <string name="cmgr_fex_dlls_only">DLLs only</string> <string name="cmgr_original_proton_updated">Original (Proton updated since the last swap)</string> <string name="cmgr_changed_outside">Changed outside Components</string> - <!-- A component version that could not be found in a Proton --> <string name="cmgr_not_present">not present</string> - <!-- %1$s is the Proton build the original files were saved from --> <string name="cmgr_original_version">Original %1$s</string> <string name="cmgr_original_from">Original from %1$s</string> <string name="cmgr_swap_queued">%1$s %2$s goes into %3$s when the game running on it closes.</string> <string name="cmgr_swap_done">%1$s %2$s is now in %3$s. It applies the next time a game starts.</string> <string name="cmgr_original_missing">That original bundle is not stored</string> - <!-- %1$s component, %2$s Proton --> <string name="cmgr_restore_queued">%1$s of %2$s goes back to its original when the game running on it closes.</string> - <!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from --> <string name="cmgr_restore_done">%1$s of %2$s restored from its %3$s original.</string> - <!-- %1$s package file, %2$s Proton id --> <string name="cmgr_package_in_use">%1$s is in use in %2$s. Swap that Proton to something else first.</string> <string name="cmgr_package_not_stored">%1$s is not stored</string> <string name="cmgr_deleted">Deleted %1$s.</string> <string name="cmgr_original_kept">The installed build\'s original is kept: it is the way back.</string> <string name="cmgr_not_stored">Not stored</string> - <!-- %1$s Proton build, %2$s component --> <string name="cmgr_deleted_original">Deleted the %1$s original of %2$s.</string> <string name="cmgr_nightlies_only">Downloads come only from the Nightlies releases</string> <string name="cmgr_list_predates_checksums">This package list predates checksums - refresh it and try again</string> <string name="cmgr_checksum_mismatch">Checksum mismatch</string> - <!-- Display choices --> <string name="sprefs_upscaler_linear">Linear</string> <string name="sprefs_upscaler_nearest">Nearest</string> <string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (quality)</string> <string name="sprefs_upscaler_sharpen">Sharpen only</string> <string name="sprefs_lod_bias_auto">Auto (match scaling)</string> - <!-- Screen effect Looks: preset names and the line under the row --> <string name="look_off_desc">Nothing applied.</string> <string name="look_game_clarity">Game Clarity</string> <string name="look_game_clarity_desc">Sharpening with a touch of contrast - the everyday one.</string> @@ -1941,27 +1806,23 @@ <string name="look_anime_edge">Anime Edge</string> <string name="look_anime_edge_desc">Toon outlines plus sharpening.</string> - <!-- GPU support line --> <string name="gpuinfo_not_adreno">Not an Adreno GPU</string> <string name="gpuinfo_supported">Supported</string> <string name="gpuinfo_experimental">Experimental: its drivers are test builds</string> <string name="gpuinfo_below_tested">Below tested hardware (Adreno 725 and newer): it may not run</string> <string name="gpuinfo_unsupported">Not supported: DroidDeck needs an Adreno (Snapdragon) GPU</string> - <!-- Driver downloads and imports --> <string name="turnip_rate_limited">GitHub\'s rate limit was hit - try again later</string> <string name="turnip_github_http">GitHub answered HTTP %1$d</string> <string name="turnip_download_http">download answered HTTP %1$d</string> <string name="turnip_list_predates_checksums">This driver list predates checksums - refresh it and try again</string> <string name="turnip_checksum_mismatch">Checksum mismatch - the download was discarded</string> - <!-- %1$s is one of the turnip_reject_* reasons --> <string name="turnip_not_adrenotools">Not an AdrenoTools driver: %1$s</string> <string name="turnip_reject_no_meta">no meta.json</string> <string name="turnip_reject_meta_unreadable">meta.json is unreadable (%1$s)</string> <string name="turnip_reject_linux_driver">this is a Linux runtime driver (import it under \"Linux runtime driver\")</string> <string name="turnip_reject_wayland">this is a Wayland game driver, which this app has no use for</string> <string name="turnip_reject_no_library_name">meta.json names no libraryName, so nothing could be handed to AdrenoTools</string> - <!-- %1$s is a library file name --> <string name="turnip_reject_library_missing">meta.json names %1$s, which is not in the zip</string> <string name="turnip_reject_not_aarch64">%1$s is not a 64-bit AArch64 ELF shared library</string> <string name="turnip_reject_glibc">%1$s links glibc - a \"-Linux\" Turnip, which belongs under \"Linux runtime driver\"</string> @@ -1972,91 +1833,62 @@ <string name="bundle_entry_outside">Not a driver bundle: %1$s points outside it</string> <string name="bundle_too_large">Not a driver bundle: it unpacks to more than %1$d MB</string> - <!-- Game storage --> <string name="gstore_sd_card">SD card</string> - <!-- %1$s is a size, like 12.3 GB --> <string name="gstore_folder">Folder</string> <string name="gstore_folder_on_volume" translatable="false">%1$s (%2$s)</string> <string name="logshare_chooser">Share session logs</string> - <!-- Stands in for the Steam account name when the client did not record one --> - <!-- Why the HDR switch is greyed out --> <string name="hdr_needs_android_10">needs Android 10 or newer (display layers with a color space)</string> <string name="hdr_none">this display reports no HDR support</string> - <!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ --> <string name="hdr_not_hdr10">this display supports %1$s, not HDR10</string> - <!-- Follow-up --> - <!-- Stands in for the GPU name when the device does not report one --> <string name="gpu_this_gpu">this GPU</string> <string name="phantom_title_on">Child-process limit is on</string> <string name="phantom_title_unset">Child-process limit is unset</string> <string name="phantom_title_unreadable">Child-process limit could not be checked</string> <string name="phantom_title_off">Child-process limit is off</string> <string name="phantom_title_not_required">Child-process limit not required</string> - <!-- %1$s is the sentence saying how to turn the limit off on this device --> <string name="phantom_instructions_fix">%1$s If that is not possible, connect this device to a computer with ADB and run the command below. Return here and check again.</string> <string name="phantom_instructions_off">Steam sessions can start.</string> <string name="phantom_instructions_not_applicable">This Android version does not use this limit.</string> <string name="phantom_fix_toggle">Turn on “Disable child process restrictions” in Developer options, and keep Developer options on afterwards.</string> <string name="phantom_fix_no_toggle">This Android version has no switch for it. Use Wireless debugging to change it from this device.</string> <string name="upd_block_no_apk">This build has no download for this copy of DroidDeck.</string> - <!-- %1$d is the offered build's versionCode, %2$d the installed one's --> <string name="upd_block_version_code">Android won\'t install it over this build because its versionCode %1$d is below the installed %2$d.</string> <string name="upd_fail_downgrade">Android won\'t install an older version over a newer one. Stay on this build until the channel catches up.</string> <string name="upd_fail_signature">This copy of DroidDeck is signed differently (a local build?), so Android won\'t update it in place.</string> <string name="upd_fail_storage">There isn\'t enough free space for the update.</string> <string name="upd_fail_aborted">The update was cancelled.</string> - <!-- %1$s is Android's own (English) installer message --> <string name="upd_fail_with_message">The update didn\'t install: %1$s</string> <string name="upd_fail_generic">The update didn\'t install.</string> - <!-- A save zip's layout; %1$s is the app it comes from (GameHub, Winlator) --> <string name="saves_layout_zip">%1$s zip</string> - <!-- A save zip whose layout is not known --> <string name="saves_zip">zip</string> - <!-- %1$s is the game's name --> <string name="saves_not_launched">%1$s has not been launched yet</string> <string name="saves_none_found">No save files found for %1$s</string> - <!-- %1$s is the zip's file name --> <string name="saves_not_save_zip">%1$s has no drive_c folder, so it isn\'t a GameHub or Winlator save zip</string> - <!-- %1$s is a folder path --> <string name="gstore_cannot_create">cannot create folders in %1$s</string> <string name="gstore_cannot_write">cannot write in %1$s</string> - <!-- %1$s is a folder path, %2$s the system's error --> <string name="gstore_cannot_write_detail">cannot write in %1$s (%2$s)</string> <string name="gfiles_folder_unavailable">Folder is unavailable</string> - <!-- %1$s is a file name --> <string name="gfiles_name_taken">A different file already uses %1$s</string> <string name="gfiles_art_unavailable">Artwork folder is unavailable</string> <string name="gfiles_unrelated_inventory">Folder has an unrelated sync inventory</string> - <!-- %1$s is a file name --> <string name="gfiles_remove_failed">Could not remove %1$s</string> <string name="gfiles_outside_folder">File leaves the export folder</string> - <!-- A FEX build whose DLL carries no version name --> <string name="cmgr_fex_unnamed">FEX (build without a version name)</string> - <!-- %1$s is a package file name --> <string name="cmgr_no_profile">no profile.json in %1$s</string> - <!-- %1$s is a package file name, %2$s the type its profile.json gives --> <string name="cmgr_not_component_package">%1$s is not a Linux component package (type %2$s)</string> - <!-- A storage volume in the second-library menu; %1$s is its name, %2$s its free space, like 12.3 GB --> <string name="gstore_volume_free">%1$s · %2$s free</string> - <!-- The second-library setting's value when it is automatic (short form of mode_storage_auto) --> <string name="mode_storage_auto_short">Automatic</string> - <!-- %1$s is the technical reason, kept in English --> <string name="appupd_catalog_rejected">The update catalog failed its checks (%1$s)</string> - <!-- Label of the copied ADB command on the clipboard --> <string name="main_adb_clip_label">DroidDeck child-process setting</string> - <!-- Signed in, but the Steam client recorded no account name --> <string name="setup_signed_in_offline_unnamed">Signed in · offline mode</string> <string name="setup_signed_in_unnamed">Signed in</string> - <!-- Small uppercase tags beside a driver's name: where it came from --> <string name="driver_tag_bundled">BUNDLED</string> <string name="driver_tag_downloaded">DOWNLOADED</string> <string name="driver_tag_imported">IMPORTED</string> - <!-- The driver is one half of a bundle that sets both drivers --> <string name="driver_tag_bundle">ANDROID + LINUX</string> - <!-- %1$s is the limit's state (e.g. Child-process limit is on), %2$s what to do, %3$s the ADB command --> <string name="phantom_warning">%1$s. %2$s\n\n%3$s</string> <string name="display_session">Session display</string> <string name="display_resolution">Resolution</string> @@ -2085,7 +1917,6 @@ <string name="force_ssbs_title">Force SSBS</string> <string name="force_ssbs_hint">Keeps a CPU speed setting on that Wine keeps turning off. Can make Windows games faster on Snapdragon 8 Elite; little change seen on older chips. Applies on the next game launch.</string> - <!-- Stores: GOG, Epic Games and Amazon Games --> <string name="rail_stores">Stores</string> <string name="source_steam">Steam</string> <string name="source_added">Custom</string> @@ -2131,7 +1962,6 @@ <string name="stores_play">Play</string> <string name="stores_install">Install</string> <string name="stores_resume_install">Resume install</string> - <!-- A Downloads row for an install that stopped when the app closed: it can be resumed --> <string name="stores_dl_interrupted">Interrupted</string> <string name="stores_install_size">Install · %1$s</string> <string name="stores_get_free">Get for free</string> @@ -2166,7 +1996,6 @@ <string name="stores_eyebrow_installed">installed</string> <string name="stores_eyebrow_library">in your library</string> <string name="stores_eyebrow_store">store</string> - <!-- A GOG or Epic game's achievements: the store page's section and the session panel. --> <string name="achievements_title">Achievements</string> <string name="achievements_count">%1$d of %2$d unlocked</string> <string name="achievements_count_short">%1$d / %2$d</string> @@ -2182,7 +2011,6 @@ <string name="achievements_none">%1$s lists no achievements for this game.</string> <string name="achievements_show_all">Show all %1$d</string> <string name="achievements_dialog_title">Achievements · %1$s</string> - <!-- A store game's facts row: the store's own facts, then Metacritic and HowLongToBeat, looked up by name. --> <string name="stores_fact_developer">Developer</string> <string name="stores_fact_publisher">Publisher</string> <string name="stores_fact_released">Released</string> @@ -2196,7 +2024,6 @@ <string name="stores_in_steam">✓ In Steam</string> <string name="stores_uninstall">Uninstall</string> <string name="stores_uninstalling">Uninstalling…</string> - <!-- The heading of the confirm that steps out of a store game's Uninstall; %1$s is the game --> <string name="stores_uninstall_title">Uninstall %1$s?</string> <string name="stores_uninstall_body_size">Frees %1$s and removes it from Steam.</string> <string name="stores_uninstall_body">Removes its files and its Steam shortcut.</string> @@ -2312,13 +2139,10 @@ <item quantity="one">%1$d already in Games</item> <item quantity="other">%1$d already in Games</item> </plurals> - <!-- A page reached through a link from another section: the way back, named for where it came from. --> <string name="hop_back">B · Back to %1$s</string> <string name="hop_from_games">the game</string> <string name="hop_from_play">Play settings</string> - <!-- The lifted settings sheets under Play and Open desktop. --> <string name="sheet_graphics_drivers">Graphics drivers</string> - <!-- Components as one stack of layers. --> <string name="comp_this_proton">This Proton</string> <string name="comp_every_proton">Every Proton</string> <string name="comp_for_every_proton">For every Proton</string> @@ -2335,7 +2159,6 @@ <string name="gpu_separately">Runtime and display drivers separately</string> <string name="gpu_use_title">Use %1$s?</string> <string name="proton_remove_title">Remove %1$s?</string> - <!-- Setup sorted by scope. --> <string name="setup_tab_diagnostics">Diagnostics</string> <string name="setup_ready">Ready to play</string> <string name="setup_not_ready">Not ready to play yet</string> @@ -2346,7 +2169,6 @@ <string name="setup_group_look">Look</string> <string name="setup_group_device">This device</string> <string name="setup_group_art">Library art</string> - <!-- Flathub, in the stores' style. --> <string name="flathub_eyebrow">LINUX APPS</string> <string name="flathub_name">Flathub</string> <string name="flathub_line">Desktop apps for the Linux desktop, opened from Desktop.</string> @@ -2354,7 +2176,6 @@ <string name="flathub_remove_title">Remove %1$s?</string> <string name="flathub_remove_note">Its settings and files are kept.</string> <string name="flathub_keep">Keep</string> - <!-- Desktop in the Steam tab's shape. --> <string name="desktop_size">Size on disk</string> <string name="desktop_uninstall_title">Uninstall the desktop?</string> <string name="desktop_all_systems">All</string> diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index bf4532cea..005841e7a 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,7 +3,6 @@ <style name="AppTheme" parent="@android:style/Theme.Material.NoActionBar"> <item name="android:colorBackground">#FF101418</item> </style> - <!-- A session owns the whole panel: no bars, no title, and black behind the compositor. --> <style name="SessionTheme" parent="@android:style/Theme.Material.NoActionBar.Fullscreen"> <item name="android:windowBackground">@android:color/black</item> </style> diff --git a/app/src/main/res/xml/game_mode_config.xml b/app/src/main/res/xml/game_mode_config.xml index b3f3e0d31..adf601e00 100644 --- a/app/src/main/res/xml/game_mode_config.xml +++ b/app/src/main/res/xml/game_mode_config.xml @@ -1,13 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- - What Android's GameManager (12+) and the OEM game services keyed to it may do to a session. - Performance mode is welcome: it is what raises the clock floor on a regular flagship the way a - gaming phone's mode does out of the box. What is refused is the two "helpful" interventions - that make a game look slow: Samsung's Game Optimizing Service and its cousins cap unknown - games' frame rate (allowGameFpsOverride) and render them at a lower resolution - (allowGameDownscaling). gamescope already owns the output size; a cap on top of Big Picture's - own pacing is exactly the low-fps-on-a-flagship symptom. ---> <game-mode-config xmlns:android="http://schemas.android.com/apk/res/android" android:supportsPerformanceGameMode="true" android:supportsBatteryGameMode="true" diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 62fd269ce..c6ade4b06 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -1,6 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- The languages DroidDeck ships, for Android's per-app language setting. Keep in step with - AppLanguage.supported and the values-* folders. --> <locale-config xmlns:android="http://schemas.android.com/apk/res/android"> <locale android:name="en" /> <locale android:name="id" /> diff --git a/app/src/main/rust/stores/Cargo.lock b/app/src/main/rust/stores/Cargo.lock index b259f7d3b..d46174b20 100644 --- a/app/src/main/rust/stores/Cargo.lock +++ b/app/src/main/rust/stores/Cargo.lock @@ -1,5 +1,3 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. version = 4 [[package]] diff --git a/app/src/main/rust/stores/Cargo.toml b/app/src/main/rust/stores/Cargo.toml index 49c36453e..f9e9ce49f 100644 --- a/app/src/main/rust/stores/Cargo.toml +++ b/app/src/main/rust/stores/Cargo.toml @@ -2,9 +2,6 @@ name = "droiddeckstores" version = "0.1.0" edition = "2021" -# The engines are a port of Bannerlator's bl-steam-client store modules (GPL-3.0-or-later, derived -# from WinNative's wn-steam-client; the Epic selection/verify rules trace back to GameNative and -# Legendary, both GPL-3.0). DroidDeck is GPL-3.0, so the crate keeps the licence it came with. license = "GPL-3.0-or-later" publish = false @@ -14,24 +11,12 @@ crate-type = ["cdylib"] [dependencies] jni = "0.21" -# setpriority/gettid: every engine thread runs at nice +10 (see src/priority.rs). Already in the -# dependency tree through tokio, at the version Cargo.lock pins. libc = "0.2" -# zlib inflate for GOG gen2 chunks and Epic chunk bodies. The pure-Rust backend keeps the .so free -# of a C zlib and of the NDK sysroot's libz, which the apk does not ship. flate2 = { version = "1", default-features = false, features = ["rust_backend"] } -# The fetch core drives every request through ONE tokio current-thread runtime and feeds a sync -# process pool; rustls (via ring) is the TLS stack so there is no OpenSSL to cross-compile. `gzip` -# transparently inflates a CDN answer that comes back content-encoded. No `blocking` client: the -# three engines are async-fetch only. reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "gzip"] } tokio = { version = "1", default-features = false, features = ["rt", "net", "time", "sync"] } futures-util = { version = "0.3", default-features = false, features = ["std"] } -# preserve_order keeps JSON object keys in source order, as in Bannerlator, so the GOG depot plan -# walks a manifest's files in the order the manifest lists them (largest-first sorting happens -# after, explicitly). serde_json = { version = "1", features = ["preserve_order"] } -# Epic chunks are SHA-1 verified, Amazon files SHA-256; GOG's MD5 is the in-tree md5_small. sha1 = "0.10" sha2 = "0.10" diff --git a/app/src/main/rust/stores/src/amazon.rs b/app/src/main/rust/stores/src/amazon.rs index 55826f941..1f24533b2 100644 --- a/app/src/main/rust/stores/src/amazon.rs +++ b/app/src/main/rust/stores/src/amazon.rs @@ -1,22 +1,3 @@ -//! Amazon Games download adapter over the shared fetch core. -//! -//! Replaces ONLY the byte-fetching pool of `AmazonDownloadManager.install()` (Step 4). -//! Java still resolves the download spec, fetches and parses `manifest.proto`, and hands -//! this module the resolved file list (one JSON entry per manifest file with the exact -//! URL the Java loop would have opened). The adapter mirrors the Java rules 1:1 where the -//! core allows it — see Bannerlator's `docs/RUST_AMAZON_PARITY.md` §4 / §9 for the table and the -//! documented deviations: -//! -//! * one `FetchItem` per WHOLE file, streamed (`FetchOptions.stream = true`): pieces are -//! appended to `<dest>.tmp` as they arrive, 8 files wide like Java's pool, memory bounded -//! to the pieces not yet written; -//! * resume-skip = `st_size == manifest size`, no hash check on skip (Java `:250`); -//! * SHA-256 is accumulated while streaming and checked in `on_finish` when the manifest -//! carries a hash; a mismatch deletes the tmp and is a retryable failure (Java `:269`); -//! * `<dest>.tmp` → delete existing dest → rename (Java `:280`); rename failure is fatal -//! without retry (Java `:281`); -//! * a piece credits its wire length to progress as it is written (Java `:347`), and pieces -//! of an attempt that later fails stay counted (Java never rolls `totalDownloaded` back). pub mod jni; @@ -30,34 +11,20 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -/// Java `AmazonDownloadManager.MAX_PARALLEL`. pub const MAX_PARALLEL: usize = 8; -/// Java `AmazonDownloadManager.DOWNLOAD_USER_AGENT`. pub const DOWNLOAD_USER_AGENT: &str = "nile/0.1 Amazon"; -/// Java `conn.setReadTimeout(120000)`; in stream mode the core applies this to the response -/// headers and then as an idle deadline per received piece — the same shape as a read timeout. pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(120); -/// Java `new File(installDir, file.unixPath() + ".tmp")`. pub const TMP_SUFFIX: &str = ".tmp"; -/// Log label used in the core's `fetch-window` lines. pub const LABEL: &str = "amazon"; -/// One resolved manifest file as serialised by Java (`AmazonDownloadManager.buildRustPlan`). #[derive(Clone, Debug, PartialEq, Eq)] pub struct PlanEntry { - /// `ManifestFile.unixPath()` — relative to the install dir, forward slashes. pub rel_path: String, - /// `AmazonApiClient.appendPath(downloadUrl, "files/" + hashHex)` — signed, whole file. pub url: String, - /// `ManifestFile.size`. pub size: u64, - /// SHA-256 to verify against; EMPTY = Java would not verify (`hashAlgorithm != 0` or - /// no hash bytes). pub sha256: Vec<u8>, } -/// Parse the JSON array `[{relPath, url, size, sha256hex}]`. `relPath` is normalised the same -/// way as `ManifestFile.unixPath()` so a caller that forgot is still byte-identical. pub fn parse_plan(json: &str) -> Result<Vec<PlanEntry>, String> { let value: serde_json::Value = serde_json::from_str(json).map_err(|err| format!("plan json: {err}"))?; @@ -106,12 +73,10 @@ pub fn parse_plan(json: &str) -> Result<Vec<PlanEntry>, String> { Ok(entries) } -/// `ManifestFile.unixPath()`. pub fn unix_path(path: &str) -> String { path.replace('\\', "/") } -/// Lowercase/uppercase hex → bytes; `None` on odd length or a non-hex digit. pub fn hex_decode(hex: &str) -> Option<Vec<u8>> { let bytes = hex.as_bytes(); if bytes.len() % 2 != 0 { @@ -132,29 +97,21 @@ pub fn hex_decode(hex: &str) -> Option<Vec<u8>> { Some(out) } -/// `new File(installDir, rel)` — Java resolves the child textually under the parent, so -/// build the path by concatenation (never `Path::join`, which would let a leading `/` escape). pub fn dest_path(install_dir: &str, rel_path: &str) -> PathBuf { let base = install_dir.trim_end_matches('/'); PathBuf::from(format!("{base}/{rel_path}")) } -/// `new File(installDir, rel + ".tmp")`. pub fn tmp_path(install_dir: &str, rel_path: &str) -> PathBuf { dest_path(install_dir, &format!("{rel_path}{TMP_SUFFIX}")) } -/// Java `:250`: `destFile.exists() && destFile.length() == file.size`. `File.length()` is -/// `st_size`, which is what `Metadata::len()` returns — including for a directory, so the -/// (degenerate) behaviours match too. No hash is checked on skip, on purpose. pub fn is_present_and_complete(install_dir: &str, entry: &PlanEntry) -> bool { fs::metadata(dest_path(install_dir, &entry.rel_path)) .map(|meta| meta.len() == entry.size) .unwrap_or(false) } -/// `scheme://host[:port]` of a URL — the per-host cap key. Amazon serves every file from the -/// one signed CDN base, so this yields a single host. pub fn host_key(url: &str) -> String { let (scheme, rest) = match url.find("://") { Some(idx) => (&url[..idx], &url[idx + 3..]), @@ -164,33 +121,22 @@ pub fn host_key(url: &str) -> String { format!("{scheme}://{}", &rest[..end]) } -/// Improvements round 1: `max(6, ceil(max_workers / distinct_hosts))` — a single-host store -/// (Amazon's one signed a2z base) gets the whole ceiling on its one host; the core clamps the -/// window to `hosts × per_host_cap`, so anything smaller would silently cut the ceiling. pub fn per_host_cap_for(max_workers: usize, distinct_hosts: usize) -> usize { let hosts = distinct_hosts.max(1); let per_host = max_workers.max(1).div_ceil(hosts); per_host.max(crate::fetch_tuning::PER_HOST_CAP) } -/// The resolved work for one run. #[derive(Debug, Default)] pub struct Plan { - /// Items to fetch (index into `entries` via `FetchItem.id`). pub items: Vec<FetchItem>, - /// Every manifest file, in manifest order (fetched and skipped alike). pub entries: Vec<PlanEntry>, - /// Σ size of all entries = `ParsedManifest.totalInstallSize`. pub total_bytes: u64, - /// Bytes credited up front for files that passed the resume check. pub skipped_bytes: u64, - /// Number of files that passed the resume check. pub skipped_files: u64, - /// Distinct host keys in fetch order. pub hosts: Vec<String>, } -/// Apply the Java resume-skip rule to every entry and turn the rest into whole-file items. pub fn build_plan(entries: Vec<PlanEntry>, install_dir: &str) -> Plan { let mut plan = Plan { total_bytes: entries.iter().map(|e| e.size).sum(), @@ -220,23 +166,17 @@ pub fn build_plan(entries: Vec<PlanEntry>, install_dir: &str) -> Plan { plan } -/// One `<dest>.tmp` being streamed: the open handle, the running hash and the bytes written -/// by the current attempt. struct OpenTmp { file: File, hasher: Sha256, written: u64, } -/// Sink: stream pieces into `<dest>.tmp` (SHA-256 accumulated on the fly), verify and rename -/// in `on_finish`. `process` (whole-body mode) does the same in one step. pub struct AmazonSink { install_dir: String, entries: Vec<PlanEntry>, cancel: Arc<AtomicBool>, files_done: AtomicU64, - /// Per-item open tmp files. The map lock is held only to look the entry up; the write - /// itself happens under the per-item lock so 8 streams never serialise on one mutex. open: Mutex<HashMap<u64, Arc<Mutex<OpenTmp>>>>, } @@ -261,16 +201,12 @@ impl AmazonSink { .ok_or_else(|| SinkError::Fatal(format!("plan index {} out of range", item.id))) } - /// Java `:260` — `destFile.getParentFile().mkdirs()` before each attempt; a failure - /// surfaces on the create/write below, like Java's. fn ensure_parent(&self, entry: &PlanEntry) { if let Some(parent) = dest_path(&self.install_dir, &entry.rel_path).parent() { let _ = fs::create_dir_all(parent); } } - /// Java `:280-284`: delete an existing dest, rename tmp → dest; rename failure fails the - /// file with NO retry (→ whole install fails). fn rename_into_place(&self, entry: &PlanEntry) -> Result<(), SinkError> { let dest = dest_path(&self.install_dir, &entry.rel_path); let tmp = tmp_path(&self.install_dir, &entry.rel_path); @@ -288,8 +224,6 @@ impl AmazonSink { Ok(()) } - /// Java `:267-277`: SHA-256 check when the manifest carries a hash; a mismatch deletes the - /// tmp and is retried. fn verify(&self, entry: &PlanEntry, digest: &[u8]) -> Result<(), SinkError> { if !entry.sha256.is_empty() && digest != entry.sha256.as_slice() { let _ = fs::remove_file(tmp_path(&self.install_dir, &entry.rel_path)); @@ -301,7 +235,6 @@ impl AmazonSink { Ok(()) } - /// Whole-body path (`FetchOptions.stream = false`): verify → write tmp → rename. pub fn commit(&self, entry: &PlanEntry, body: &[u8]) -> Result<u64, SinkError> { if !entry.sha256.is_empty() { let digest = Sha256::digest(body); @@ -322,10 +255,6 @@ impl AmazonSink { Ok(body.len() as u64) } - /// After the run: close and delete every tmp that never reached `on_finish` (a stream that - /// failed after max attempts, or was cut by cancel / a fatal error). Java deletes the tmp - /// of every failed or cancelled attempt (`:271`, `:288`, `:293`, `:297`), so no `.tmp` - /// may survive a run on either engine. Returns how many were removed. pub fn cleanup_partials(&self) -> usize { let leftovers: Vec<(u64, Arc<Mutex<OpenTmp>>)> = match self.open.lock() { Ok(mut open) => open.drain().collect(), @@ -355,13 +284,11 @@ impl FetchSink for AmazonSink { fn on_chunk(&self, item: &FetchItem, offset: u64, data: &[u8]) -> Result<(), SinkError> { if self.cancel.load(Ordering::Relaxed) { - // Java `:342-344`: cancel inside the read loop → tmp deleted (cleanup_partials). return Err(SinkError::Fatal("cancelled".to_string())); } let entry = self.entry(item)?; let tmp = tmp_path(&self.install_dir, &entry.rel_path); let open = if offset == 0 { - // (Re)start of an attempt: Java opens a fresh FileOutputStream (truncating). self.ensure_parent(entry); let file = File::create(&tmp).map_err(|err| { let _ = fs::remove_file(&tmp); @@ -405,7 +332,6 @@ impl FetchSink for AmazonSink { ))); } if let Err(err) = state.file.write_all(data) { - // Java: IOException inside downloadFile → tmp deleted → next attempt. let _ = fs::remove_file(&tmp); return Err(SinkError::Retry(format!("write {}: {err}", tmp.display()))); } @@ -435,7 +361,6 @@ impl FetchSink for AmazonSink { (written, digest) } None => { - // Zero-byte body: no piece ever arrived. Java would have created an empty tmp. if total_len != 0 { return Err(SinkError::Retry(format!( "finish without a stream for: {}", @@ -462,22 +387,16 @@ impl FetchSink for AmazonSink { } } -/// Outcome of one `run_download`. #[derive(Clone, Debug, Default)] pub struct RunResult { pub success: bool, pub cancelled: bool, pub error: String, - /// Bytes fetched by this run (skipped files excluded; failed attempts' pieces included, - /// like Java's `totalDownloaded`). pub bytes_written: u64, - /// Files committed by this run plus files skipped by the resume check. pub files_done: u64, pub files_total: u64, } -/// Throughput bookkeeping for the final summary line (avg over the run, peak over ≥500 ms -/// progress intervals — the same sampling window the Java speed label uses). struct SpeedMeter { started: Instant, last_at: Instant, @@ -516,10 +435,6 @@ impl SpeedMeter { } } -/// Blocking: plan → stream-fetch → verify/rename, calling `progress(bytes_done, bytes_total, -/// files_done, files_total)` after each written piece and each finished file (skipped files -/// are pre-credited) and `log` for every diagnostic line. `bytes_done` includes skipped bytes, -/// matching Java's `totalDownloaded` (which credits skipped files at `:251`). pub fn run_download( plan_json: &str, install_dir: &str, @@ -582,16 +497,12 @@ pub fn run_download( let opts = FetchOptions { max_workers, - // Every Amazon file comes from the one signed CDN base: give that host the whole - // ceiling (`per_host_cap_for`), or the core would clamp the window to hosts × cap. per_host_cap, timeout: REQUEST_TIMEOUT, headers: vec![("User-Agent".to_string(), DOWNLOAD_USER_AGENT.to_string())], ca_bundle_path: ca_bundle_path.to_string(), process_workers, label: LABEL.to_string(), - // Whole files, streamed into the tmp as they arrive: 8 wide like Java's pool, memory - // bounded to the pieces not yet written. stream: true, ..FetchOptions::default() }; @@ -753,10 +664,8 @@ mod tests { fn build_plan_skips_only_size_matched_files() { let dir = scratch_dir(); let entries = parse_plan(PLAN).unwrap(); - // Entry 1 present with the right size → skipped (no hash check even if wrong bytes). fs::create_dir_all(format!("{dir}/data")).unwrap(); fs::write(format!("{dir}/data/level1.pak"), b"zz").unwrap(); - // Entry 0 present with the wrong size → fetched. fs::create_dir_all(format!("{dir}/Binaries/Win64")).unwrap(); fs::write(format!("{dir}/Binaries/Win64/Game.exe"), b"tes").unwrap(); let plan = build_plan(entries, &dir); @@ -803,7 +712,6 @@ mod tests { let cancel = Arc::new(AtomicBool::new(false)); let sink = AmazonSink::new(&dir, vec![entry.clone()], Arc::clone(&cancel)); let item = item_for(&entry, 0); - // Mismatch → Retry, nothing on disk. match sink.process(&item, b"nope".to_vec()) { Err(SinkError::Retry(msg)) => assert!(msg.contains("SHA-256 mismatch")), Err(SinkError::Fatal(msg)) => panic!("expected Retry, got Fatal({msg})"), @@ -811,14 +719,11 @@ mod tests { } assert!(!dest_path(&dir, "sub/Game.exe").exists()); assert!(!tmp_path(&dir, "sub/Game.exe").exists()); - // Match → written, renamed, tmp gone, credited body length. assert_eq!(sink.process(&item, body.clone()).unwrap(), 4); assert_eq!(fs::read(dest_path(&dir, "sub/Game.exe")).unwrap(), body); assert!(!tmp_path(&dir, "sub/Game.exe").exists()); assert_eq!(sink.files_done(), 1); - // Existing dest is replaced (Java `:280`). assert_eq!(sink.process(&item, body.clone()).unwrap(), 4); - // Cancel → Fatal, no write. cancel.store(true, Ordering::Relaxed); assert!(matches!( sink.process(&item, body), @@ -863,7 +768,6 @@ mod tests { }; let sink = AmazonSink::new(&dir, vec![entry.clone()], Arc::new(AtomicBool::new(false))); let item = item_for(&entry, 0); - // Attempt 1 delivers garbage, then the core restarts at offset 0. sink.on_chunk(&item, 0, b"zzzz").unwrap(); sink.on_chunk(&item, 0, b"abc").unwrap(); sink.on_chunk(&item, 3, b"xyz").unwrap(); @@ -874,21 +778,17 @@ mod tests { } assert!(!tmp_path(&dir, "f.bin").exists()); assert!(!dest_path(&dir, "f.bin").exists()); - // Out-of-order piece → Retry. sink.on_chunk(&item, 0, b"abc").unwrap(); assert!(matches!( sink.on_chunk(&item, 5, b"f"), Err(SinkError::Retry(_)) )); - // Length mismatch at finish → Retry. sink.on_chunk(&item, 0, b"abc").unwrap(); assert!(matches!(sink.on_finish(&item, 6), Err(SinkError::Retry(_)))); - // Piece without a start → Retry. assert!(matches!( sink.on_chunk(&item, 3, b"def"), Err(SinkError::Retry(_)) )); - // Good attempt. sink.on_chunk(&item, 0, b"abc").unwrap(); sink.on_chunk(&item, 3, b"def").unwrap(); assert_eq!(sink.on_finish(&item, 6).unwrap(), 0); @@ -915,10 +815,8 @@ mod tests { let sink = AmazonSink::new(&dir, vec![big.clone(), empty.clone()], Arc::clone(&cancel)); sink.on_chunk(&item_for(&big, 0), 0, b"12345").unwrap(); assert!(tmp_path(&dir, "cut.bin").exists()); - // Zero-byte body: on_finish with no pieces creates and commits an empty file. assert_eq!(sink.on_finish(&item_for(&empty, 1), 0).unwrap(), 0); assert_eq!(fs::read(dest_path(&dir, "empty.bin")).unwrap(), b""); - // Cancel mid-stream → Fatal; cleanup deletes the partial tmp like Java does. cancel.store(true, Ordering::Relaxed); assert!(matches!( sink.on_chunk(&item_for(&big, 0), 5, b"6"), diff --git a/app/src/main/rust/stores/src/amazon/jni.rs b/app/src/main/rust/stores/src/amazon/jni.rs index 26797773b..32ce4c62c 100644 --- a/app/src/main/rust/stores/src/amazon/jni.rs +++ b/app/src/main/rust/stores/src/amazon/jni.rs @@ -1,12 +1,3 @@ -//! JNI facade for the Amazon download adapter. -//! -//! Kotlin side: `com.droiddeck.launcher.stores.amazon.AmazonNative` (`nativeStart` / -//! `nativeCancel` / `nativeRelease`) with an `AmazonNative.Listener` -//! (`onProgress(JJJJ)V`, `onLog(String)V`, `onComplete(ZString;J)V`). Mirrors the shape of -//! the GOG and Epic facades: the run happens on a spawned native -//! thread, callbacks attach the JavaVM as daemon threads and hold a `GlobalRef` to the -//! listener; cancel flips an `AtomicBool`. This module keeps its own `JavaVM` handle (taken -//! from the calling `JNIEnv` on first use) so it depends on nothing outside this module. use crate::amazon::{run_download, MAX_PARALLEL}; use jni::objects::{GlobalRef, JClass, JObject, JString, JValue}; @@ -16,10 +7,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, OnceLock}; use std::thread; -/// Logcat tag (`adb logcat -s AmazonNative`). Every engine line reaches logcat exactly ONCE: -/// this module only forwards lines to the listener (`onLog`), and the Java manager is the one -/// place that writes them to `android.util.Log` + its debug file. (Round 1 fix: the native side -/// used to also call `__android_log_write`, so each line appeared twice.) pub const LOG_TAG: &str = "AmazonNative"; static JVM: OnceLock<JavaVM> = OnceLock::new(); @@ -36,7 +23,6 @@ fn jstring_to_string(env: &mut JNIEnv, value: &JString) -> Option<String> { .map(|s| s.to_string_lossy().into_owned()) } -/// One live run; boxed and handed to Kotlin as an opaque `jlong`. struct AmazonRun { cancel: Arc<AtomicBool>, } @@ -115,7 +101,6 @@ fn call_complete( clear_pending_exception(env); } -/// Attach the current (native) thread and run `f` with the listener object. fn with_attached_env(listener: &GlobalRef, f: impl FnOnce(&mut JNIEnv, &JObject)) { let Some(vm) = JVM.get() else { return; @@ -126,9 +111,6 @@ fn with_attached_env(listener: &GlobalRef, f: impl FnOnce(&mut JNIEnv, &JObject) f(&mut env, listener.as_obj()); } -/// `AmazonNative.nativeStart(planJson, installDir, caBundlePath, maxWorkers, -/// processWorkers, listener) -> handle`. Returns 0 (and fires `onComplete(false, …)`) when the -/// inputs are unusable. The run itself happens on a spawned thread. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeStart( mut env: JNIEnv, @@ -200,8 +182,6 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_amazon_AmazonNative_na to_handle(run) } -/// `AmazonNative.nativeCancel(handle)` — flips the run's cancel flag; the core stops -/// issuing requests, in-flight bodies are dropped, `onComplete(false, "cancelled", …)` follows. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeCancel( _env: JNIEnv, @@ -213,8 +193,6 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_amazon_AmazonNative_na } } -/// `AmazonNative.nativeRelease(handle)` — frees the handle. Safe at any time: the worker -/// thread owns its own `Arc` of the cancel flag and the listener `GlobalRef`. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_amazon_AmazonNative_nativeRelease( _env: JNIEnv, diff --git a/app/src/main/rust/stores/src/epic.rs b/app/src/main/rust/stores/src/epic.rs index 258c273b9..41ac19767 100644 --- a/app/src/main/rust/stores/src/epic.rs +++ b/app/src/main/rust/stores/src/epic.rs @@ -1,29 +1,4 @@ -//! Epic Games Store download adapter for the shared fetch core (`crate::fetch_core`). -//! -//! Scope = the chunk-fetch inner loop of `EpicDownloadManager.java` (the fixed 8-thread pool that -//! fills `<installDir>/.chunks/<GUID>`), nothing more. The Java manager still parses the manifest -//! API JSON, downloads the manifest, selects files (install tags), runs the delta/verify pass, -//! assembles files from the chunk cache and does every post-install step. The adapter re-parses -//! the same manifest bytes, rebuilds the same chunk plan for the pending files Java hands it, -//! skips chunks already in the cache and writes verified chunks with the same `.part` + rename -//! protocol, so the on-disk state is identical whichever engine ran (see -//! Bannerlator's `docs/RUST_EPIC_PARITY.md` for the rule-by-rule table). -//! -//! Submodules: -//! - [`manifest`] — ChunksV4 binary manifest + legacy JSON manifest parsers (1:1 with the Java -//! `parseManifest` / `parseJsonManifest`). -//! - [`plan`] — install-tag selection, unique-chunk plan, chunk paths / CDN URLs. -//! - [`chunk`] — chunk header parse, zlib inflate, SHA-1 verify, cache write. -//! - [`driver`] — plan → `FetchItem`s → `fetch_core::run_fetch` with the chunk-cache sink. -//! - [`assemble`] — the files from the cache, one sequential write each, SHA-1 checked as they -//! are written; with a scratch cache (one the caller placed apart from the install) each chunk -//! is deleted as soon as the last file that uses it is done. -//! - [`jni`] — `Java_com_droiddeck_launcher_stores_epic_EpicNative_native*` exports. -//! -//! The selective-install-tag, delta/resume and chunk verification rules mirrored here were ported -//! into the Java manager from GameNative (`service/epic/EpicDownloadManager.kt`, -//! `manifest/ManifestUtils.kt`), itself derived from Legendary; both are GPL-3.0 and this crate -//! carries the same licence (see `Cargo.toml`). +//! Derived from GameNative's Epic manager (itself derived from Legendary); both are GPL-3.0 and this crate carries the same licence (see Cargo.toml). pub mod assemble; pub mod chunk; @@ -32,15 +7,11 @@ pub mod jni; pub mod manifest; pub mod plan; -/// User-Agent the Java manager sends on every chunk request (`EpicDownloadManager.UA`). pub const USER_AGENT: &str = "UELauncher/11.0.1-14907503+++Portal+Release-Live Windows/10.0.19041.1.256.64bit"; -/// Name of the chunk cache directory under the install dir (`new File(installDir, ".chunks")`). pub const CHUNK_CACHE_DIR: &str = ".chunks"; -/// Fixed pool width of the Java chunk downloader (`Executors.newFixedThreadPool(8)`). pub const JAVA_POOL_THREADS: usize = 8; -/// logcat tag for every engine line (the Kotlin manager mirrors it into its own debug log). pub const LOG_TAG: &str = "EpicNative"; diff --git a/app/src/main/rust/stores/src/epic/assemble.rs b/app/src/main/rust/stores/src/epic/assemble.rs index 580cb8c24..6881640b8 100644 --- a/app/src/main/rust/stores/src/epic/assemble.rs +++ b/app/src/main/rust/stores/src/epic/assemble.rs @@ -1,26 +1,3 @@ -//! Files from the chunk cache: the last stage of an Epic install, as a native run. -//! -//! The Kotlin manager's own loop does this too (`EpicDownloadManager.install`, after the chunk -//! fetch): for every pending file, open it, and for every part read the cached chunk and write -//! `chunk[offset..offset+size]`. This module does the same thing, in the same order, with the -//! same wording on a missing chunk, plus what the Kotlin loop cannot do without re-reading the -//! finished file from a slow card: -//! -//! - the file's SHA-1 is computed from the bytes as they are written (no extra I/O) and compared -//! with the manifest's, when the manifest has one; a mismatch deletes the file and fails the -//! run, so a bad assembly can never pass the next delta pass by size alone; -//! - with a scratch cache — one the caller placed apart from the install directory — every chunk -//! is reference-counted across the pending files and deleted the moment the last file that -//! needs it is written and verified, so the cache never holds more than what is still owed. -//! -//! The cache directory itself goes at the end of a successful run, scratch or not, which is what -//! the Kotlin loop does with `<installDir>/.chunks`. Cancellation and failure leave the cache -//! exactly as it stands, so the next run's delta pass skips the finished files and the fetch -//! skips the chunks still cached — on either engine. -//! -//! Why the cache may live elsewhere at all: an install to an SD card wrote every byte twice to -//! the card (chunk cache, then assembled file). With the cache on internal storage the card sees -//! each byte once, and the fetchers stop waiting on the card's write speed. use std::collections::HashMap; use std::fs::{self, File}; @@ -33,33 +10,25 @@ use sha1::{Digest, Sha1}; use super::manifest::{parse_manifest, FileInfo, Manifest}; use super::plan::resolve_cache_dir; -/// Everything the JNI layer hands over for one assembly run. pub struct AssembleRequest { pub manifest_bytes: Vec<u8>, pub install_dir: String, - /// `""` = `<installDir>/.chunks` (no scratch semantics); anything else = scratch cache. pub chunk_cache_dir: String, - /// Indices into `manifest.files` of the files to assemble, in order. pub file_indices: Vec<usize>, } -/// Terminal result of an assembly run. #[derive(Clone, Debug, Default)] pub struct AssembleOutcome { pub success: bool, pub cancelled: bool, pub error: String, - /// Bytes written into assembled files this run. pub bytes_written: u64, pub files_done: u64, pub files_total: u64, pub bytes_total: u64, - /// Chunk files deleted early because no remaining file needed them (scratch cache only). pub chunks_removed: u64, } -/// The parsed plan: manifest, the files to write, and how many of them still need each chunk. -/// Built up front so a bad index fails before anything is written. #[derive(Debug)] pub struct AssemblePlan { pub manifest: Manifest, @@ -68,12 +37,9 @@ pub struct AssemblePlan { pub scratch_cache: bool, pub files: Vec<usize>, pub bytes_total: u64, - /// GUID → number of files in `files` whose parts reference it (a file counts once per GUID, - /// however many of its parts use the chunk). pub refs: HashMap<String, u32>, } -/// Parse + plan. `Err` = nothing was written. pub fn build_assemble_plan(req: &AssembleRequest) -> Result<AssemblePlan, String> { if req.install_dir.is_empty() { return Err("assemble: empty install dir".to_string()); @@ -104,7 +70,6 @@ pub fn build_assemble_plan(req: &AssembleRequest) -> Result<AssemblePlan, String }) } -/// How many of `file_indices` reference each chunk GUID. pub fn chunk_refs(m: &Manifest, file_indices: &[usize]) -> HashMap<String, u32> { let mut refs: HashMap<String, u32> = HashMap::new(); for &fi in file_indices { @@ -118,7 +83,6 @@ pub fn chunk_refs(m: &Manifest, file_indices: &[usize]) -> HashMap<String, u32> refs } -/// The GUIDs a file's parts reference, each once, in first-seen order. fn distinct_guids(f: &FileInfo) -> Vec<String> { let mut out: Vec<String> = Vec::with_capacity(f.parts.len()); for p in &f.parts { @@ -130,19 +94,14 @@ fn distinct_guids(f: &FileInfo) -> Vec<String> { out } -/// The manifest's relative path with Windows separators normalised, as the Kotlin loop does -/// (`filename.replace("\\", "/")`). pub fn relative_path(f: &FileInfo) -> String { f.filename.replace('\\', "/") } -/// A manifest SHA-1 that is all zero means "none" (the Kotlin delta pass treats it the same way). fn expected_sha1(f: &FileInfo) -> Option<&[u8; 20]> { f.sha1.as_ref().filter(|h| h.iter().any(|&b| b != 0)) } -/// Chunk bytes, read whole, with the last chunk kept: consecutive parts of one file usually come -/// from the same chunk, and a file's parts are the only place a chunk is read. struct ChunkReader { cache_dir: PathBuf, last: Option<(String, Vec<u8>)>, @@ -161,7 +120,6 @@ impl ChunkReader { Ok(self.last.as_ref().map(|(_, d)| d.as_slice()).unwrap_or(&[])) } - /// Drop the kept copy of a chunk that is about to be deleted. fn forget(&mut self, guid: &str) { if self.last.as_ref().map(|(g, _)| g.as_str()) == Some(guid) { self.last = None; @@ -169,8 +127,6 @@ impl ChunkReader { } } -/// Write one file from the cache. Returns the bytes written. The output is removed on any error, -/// so a file only ever exists complete (and, when the manifest hashes it, verified) or not at all. fn write_file(reader: &mut ChunkReader, f: &FileInfo, out_path: &Path) -> Result<u64, String> { let rel_path = relative_path(f); if let Some(parent) = out_path.parent() { @@ -219,8 +175,6 @@ fn write_file(reader: &mut ChunkReader, f: &FileInfo, out_path: &Path) -> Result result } -/// Run the plan. `progress(bytes_done, bytes_total, files_done, files_total)` fires after every -/// file; `log` receives the start line, failures and the terminal summary. pub fn run_assemble( plan: &AssemblePlan, cancel: &AtomicBool, @@ -270,9 +224,6 @@ pub fn run_assemble( return outcome; } } - // Scratch cache: this file was the last user of a chunk → the chunk goes now, not at the - // end, so the cache holds only what is still owed to files not yet written. Released - // before the progress event, so "file done" also means "its chunks are freed". if plan.scratch_cache { for guid in distinct_guids(f) { let Some(count) = refs.get_mut(&guid) else { @@ -297,8 +248,6 @@ pub fn run_assemble( } outcome.success = true; - // Done with the cache: remove it whole (a scratch cache is already down to the chunks no - // pending file referenced; the default one is what the Kotlin loop deleted at this point). match fs::remove_dir_all(&plan.cache_dir) { Ok(()) => log(&format!("cache removed {}", plan.cache_dir.display())), Err(e) => log(&format!( @@ -337,7 +286,6 @@ mod tests { out } - /// file 0 = A[0..100] ++ B[10..60]; file 1 = A[100..300]. A is shared, B is file 0's alone. fn expected_files() -> (Vec<u8>, Vec<u8>) { let (a, b) = (chunk_a(), chunk_b()); let mut f0 = a[0..100].to_vec(); @@ -426,7 +374,6 @@ mod tests { fill_cache(&cache); let plan = build_assemble_plan(&request(&install, cache.to_str().unwrap())).unwrap(); assert!(plan.scratch_cache); - // After file 0: B (only file 0 used it) must be gone, A (file 1 still needs it) must stay. let seen = Mutex::new(Vec::new()); let progress = |_: u64, _: u64, files_done: u64, _: u64| { if files_done == 1 { diff --git a/app/src/main/rust/stores/src/epic/chunk.rs b/app/src/main/rust/stores/src/epic/chunk.rs index 22ebb864d..3e0e96c49 100644 --- a/app/src/main/rust/stores/src/epic/chunk.rs +++ b/app/src/main/rust/stores/src/epic/chunk.rs @@ -1,16 +1,3 @@ -//! One chunk: header parse → (zlib inflate | copy) → SHA-1 → `.part` → rename into the cache. -//! -//! Port of `EpicDownloadManager.downloadChunkStreaming` minus the HTTP part (the fetch core -//! hands us the whole response body). Java's protocol, kept exactly: -//! - 41-byte header: magic(4) headerVersion(4) headerSize(4) compressedSize(4) GUID(16) hash(8) -//! storedAs(1); payload starts at `max(headerSize, 41)` (Java never seeks back); -//! - payload = the next `compressedSize` bytes, or whatever the body still has (a short body -//! is NOT an error — the SHA-1 check decides); -//! - `storedAs & 1` → zlib; corrupt zlib = failure; a stream that ends early = partial output; -//! - the decompressed bytes go to `<final>.part`; the SHA-1 (when the manifest has one) must -//! match or the `.part` is deleted; then an atomic rename publishes `<final>`. -//! A cache file therefore exists under its final name ONLY if it fully downloaded and verified, -//! on either engine. use std::fs::{self, File}; use std::io::{self, BufWriter, Write}; @@ -18,10 +5,8 @@ use std::path::{Path, PathBuf}; use sha1::{Digest, Sha1}; -/// Chunk file magic (`0xB1FE3AA2`). pub const CHUNK_MAGIC: u32 = 0xB1FE_3AA2; -/// Bytes Java reads before looking at the header (`byte[] hdrBuf = new byte[41]`). pub const CHUNK_HEADER_PREFIX: usize = 41; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -37,7 +22,6 @@ impl ChunkHeader { (self.stored_as & 1) != 0 } - /// Where the payload begins in the body (`if (headerSize > 41) skipFully(headerSize - 41)`). pub fn payload_start(&self) -> usize { if self.header_size > CHUNK_HEADER_PREFIX as i32 { self.header_size as usize @@ -47,8 +31,6 @@ impl ChunkHeader { } } -/// `readFully(hdrBuf)` + header decode. `Err` = the Java `continue`/exception paths -/// (short stream, bad magic). pub fn parse_chunk_header(body: &[u8]) -> Result<ChunkHeader, String> { if body.len() < CHUNK_HEADER_PREFIX { return Err(format!( @@ -70,9 +52,6 @@ pub fn parse_chunk_header(body: &[u8]) -> Result<ChunkHeader, String> { }) } -/// The payload slice Java's read loop would consume: `compressedSize` bytes after the header, -/// truncated to what the body holds (`if (n <= 0) break;`). `Err` when the header skip runs -/// past the body (`skipFully` throws). pub fn chunk_payload<'a>(body: &'a [u8], hdr: &ChunkHeader) -> Result<&'a [u8], String> { let start = hdr.payload_start(); if start > body.len() { @@ -88,7 +67,6 @@ pub fn chunk_payload<'a>(body: &'a [u8], hdr: &ChunkHeader) -> Result<&'a [u8], Ok(&body[start..start + take]) } -/// `Writer` that mirrors `fos.write(...)` + `sha.update(...)` on every block. struct HashingWriter<W: Write> { inner: W, sha: Option<Sha1>, @@ -110,23 +88,18 @@ impl<W: Write> Write for HashingWriter<W> { } } -/// `new File(outFile.getPath() + ".part")`. pub fn part_path(final_path: &Path) -> PathBuf { let mut s = final_path.as_os_str().to_os_string(); s.push(".part"); PathBuf::from(s) } -/// Process one downloaded body into `final_path`. Returns the number of DECOMPRESSED bytes -/// written. `Err(reason)` = Java's "try the next CDN" outcome; the `.part` never survives an -/// error and `final_path` is only ever created by the rename after verification. pub fn write_verified_chunk( body: &[u8], expected_sha1: Option<&[u8; 20]>, final_path: &Path, ) -> Result<u64, String> { let tmp = part_path(final_path); - // `tmp.delete()` — clear any stale partial from a prior interrupted attempt. let _ = fs::remove_file(&tmp); let result = write_part(body, expected_sha1, &tmp, final_path); @@ -153,9 +126,6 @@ fn write_part( }; if hdr.is_compressed() { - // Java: inflate until the stream finishes or the input runs out; a corrupt stream throws - // (DataFormatException) → this CDN attempt fails. flate2 behaves the same way: EOF on a - // truncated stream is a clean end (partial output), corruption is an error. let mut dec = flate2::read::ZlibDecoder::new(payload); io::copy(&mut dec, &mut out).map_err(|e| format!("inflate: {e}"))?; } else { @@ -164,7 +134,7 @@ fn write_part( out.flush().map_err(|e| format!("flush: {e}"))?; let written = out.written; let digest = out.sha.take().map(|s| s.finalize()); - drop(out); // close the file before the rename + drop(out); if let (Some(expected), Some(actual)) = (expected_sha1, digest) { if actual.as_slice() != &expected[..] { @@ -172,7 +142,6 @@ fn write_part( } } - // Verified → publish atomically (same directory → same filesystem). fs::rename(tmp, final_path).map_err(|e| format!("Chunk rename failed: {e}"))?; Ok(written) } @@ -181,7 +150,6 @@ fn write_part( pub(crate) mod test_support { use super::*; - /// Build a chunk body the way the CDN serves it. pub fn build_chunk_body( decompressed: &[u8], compress: bool, @@ -199,15 +167,15 @@ pub(crate) mod test_support { let header_size = (CHUNK_HEADER_PREFIX + extra_header) as i32; let mut out = Vec::new(); out.extend_from_slice(&CHUNK_MAGIC.to_le_bytes()); - out.extend_from_slice(&3i32.to_le_bytes()); // headerVersion + out.extend_from_slice(&3i32.to_le_bytes()); out.extend_from_slice(&header_size.to_le_bytes()); out.extend_from_slice( &declared_compressed_size .unwrap_or(payload.len() as i32) .to_le_bytes(), ); - out.extend_from_slice(&[0xAB; 16]); // GUID (unused by the reader) - out.extend_from_slice(&[0xCD; 8]); // hash (unused by the reader) + out.extend_from_slice(&[0xAB; 16]); + out.extend_from_slice(&[0xCD; 8]); out.push(if compress { 1 } else { 0 }); out.extend(std::iter::repeat(0x5A).take(extra_header)); assert_eq!(out.len(), header_size as usize); @@ -280,7 +248,6 @@ mod tests { let neg = build_chunk_body(b"abcdef", false, 0, Some(-1)); let h = parse_chunk_header(&neg).unwrap(); assert_eq!(chunk_payload(&neg, &h).unwrap(), b""); - // headerSize past the body → skipFully throws. let mut far = build_chunk_body(b"abcdef", false, 0, None); far[8..12].copy_from_slice(&5000i32.to_le_bytes()); let h = parse_chunk_header(&far).unwrap(); @@ -345,12 +312,10 @@ mod tests { let data = sample_data(); let mut body = build_chunk_body(&data, true, 0, None); let final_path = dir.join("chunk"); - // Truncate the body: inflate ends early → partial output → SHA-1 mismatch. let cut = body.len() - 1000; let err = write_verified_chunk(&body[..cut], Some(&sha1_of(&data)), &final_path).unwrap_err(); assert!(err.contains("SHA-1 mismatch") || err.contains("inflate"), "{err}"); assert!(!final_path.exists()); - // Corrupt the deflate stream itself. for b in body.iter_mut().skip(60).take(64) { *b ^= 0x55; } @@ -366,7 +331,7 @@ mod tests { let dir = temp_dir("stale"); let final_path = dir.join("chunk"); fs::write(part_path(&final_path), b"stale").unwrap(); - let body = [0u8; 10]; // too short → header failure + let body = [0u8; 10]; assert!(write_verified_chunk(&body, None, &final_path).is_err()); assert!(!part_path(&final_path).exists()); assert!(!final_path.exists()); diff --git a/app/src/main/rust/stores/src/epic/driver.rs b/app/src/main/rust/stores/src/epic/driver.rs index 1c39f5ac6..f72118b87 100644 --- a/app/src/main/rust/stores/src/epic/driver.rs +++ b/app/src/main/rust/stores/src/epic/driver.rs @@ -1,12 +1,3 @@ -//! Plan → `FetchItem`s → `crate::fetch_core::run_fetch` with a sink that fills the chunk cache. -//! -//! This is the replacement for the body of the Java pool block in `EpicDownloadManager.install` -//! ("Download unique chunks — 8 parallel threads"). Inputs are exactly what that block sees: -//! the parsed manifest (re-parsed here from the same bytes), the pending file set (indices Java -//! computed after its delta/verify pass), the CDN prefixes (`baseUrl + cloudDir`, cloudflare -//! already skipped) and the chunk cache dir. Output = `<cache>/<GUID>` for every needed chunk, -//! where `<cache>` is `<installDir>/.chunks` unless the caller named another directory; the -//! files are assembled from it afterwards (by the Kotlin manager, or by [`super::assemble`]). use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; @@ -21,65 +12,44 @@ use super::plan::{ total_credit_bytes, unique_chunks_for_files, }; -/// Java `conn.setReadTimeout(60000)` — the longer of the two Java timeouts (connect was 30 s). pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(60); -/// Everything the JNI layer hands over for one run. pub struct EpicRequest { pub manifest_bytes: Vec<u8>, pub install_dir: String, - /// Chunk cache directory; `""` = `<installDir>/.chunks`. See [`resolve_cache_dir`]. pub chunk_cache_dir: String, - /// `cdn.baseUrl + cdn.cloudDir` per manifest-API CDN entry, in Java's order. pub cdn_prefixes: Vec<String>, - /// Indices into `manifest.files` of Java's `pendingFiles` (post delta/verify), in order. pub pending_file_indices: Vec<usize>, - /// Java's `neededChunks.size()` / `totalBytes` for the plan cross-check (`None` = skip). pub expected_chunks: Option<u64>, pub expected_bytes: Option<u64>, pub ca_bundle_path: String, - /// Window ceiling: the Steam speed tier's network window (Fast = 32) since improvements - /// round 1; the Java fallback pool keeps its fixed 8 (`super::JAVA_POOL_THREADS`). pub max_workers: usize, pub process_workers: usize, pub label: String, } -/// The chunk plan for one run. #[derive(Debug)] pub struct EpicPlan { pub manifest: Manifest, pub cache_dir: PathBuf, - /// The cache is a directory the caller named, apart from the install: scratch space that - /// the assembler empties as it goes and removes when it is done. pub scratch_cache: bool, - /// Chunk indices (into `manifest.unique_chunks`) in Java's submission order. pub needed: Vec<usize>, - /// `Σ max(fileSize, 1)` over `needed`. pub total_bytes: u64, - /// Distinct CDN prefixes = fetch-core host keys. pub hosts: Vec<String>, } -/// Terminal result of a run, in the shape Java's pool block needs to reproduce its own exit -/// paths (`CANCELLED during chunk download (n/total chunks)`, `N chunks failed`, `chunksOK=`). #[derive(Clone, Debug, Default)] pub struct EpicOutcome { pub success: bool, pub cancelled: bool, pub error: String, - /// Credited (manifest `max(fileSize,1)`) bytes: skipped-cached + fetched. pub bytes_credited: u64, - /// Chunks accounted for (skipped-cached + fetched), Java's `completedCount`. pub chunks_done: u64, pub chunks_total: u64, pub bytes_total: u64, - /// Decompressed bytes actually written to the cache this run. pub decompressed_written: u64, } -/// Parse + plan (no I/O beyond reading the cache dir). `Err` = "engine could not start"; Java -/// then runs its own pool, since nothing has been fetched yet. pub fn build_plan(req: &EpicRequest) -> Result<EpicPlan, String> { let manifest = parse_manifest(&req.manifest_bytes).map_err(|e| format!("plan: {e}"))?; for &i in &req.pending_file_indices { @@ -124,10 +94,8 @@ pub fn build_plan(req: &EpicRequest) -> Result<EpicPlan, String> { }) } -/// Sink: one downloaded body → verified cache file. `id` indexes `fetch_chunks`. struct ChunkCacheSink<'a> { plan: &'a EpicPlan, - /// Chunk indices (into `manifest.unique_chunks`) of the items actually being fetched. fetch_chunks: &'a [usize], decompressed: AtomicU64, } @@ -144,15 +112,11 @@ impl<'a> FetchSink for ChunkCacheSink<'a> { self.decompressed.fetch_add(written, Ordering::Relaxed); Ok(chunk.credit_bytes()) } - // Every per-attempt failure in Java is "try the next CDN"; the core's Retry rotates - // hosts and backs off the same way (bounded at its attempt cap). Err(reason) => Err(SinkError::Retry(format!("{} {reason}", chunk.guid_str()))), } } } -/// Rolling speed sampler for the end-of-run summary: samples at ≥500 ms like the Java pool's -/// `lastSpeedMs` logic, keeps the peak. struct SpeedMeter { start: Instant, last_at: Instant, @@ -197,9 +161,6 @@ impl SpeedMeter { } } -/// Run the plan. `progress(bytes_done, chunks_done)` fires once per accounted chunk (cached-skip -/// or fetched), exactly the cadence of the Java pool's per-task `progress(...)` call. `log` -/// receives engine lines (`fetch-window`, `summary`, failures). pub fn run_plan( plan: &EpicPlan, req: &EpicRequest, @@ -228,8 +189,6 @@ pub fn run_plan( req.process_workers )); - // Java: each pool task first checks the cancel flag, then `cachedFile.exists()` → credit - // without fetching. Account for the cached chunks up front (same credit, same callback). let mut fetch_chunks: Vec<usize> = Vec::with_capacity(plan.needed.len()); let mut pre_bytes: u64 = 0; let mut pre_chunks: u64 = 0; @@ -293,16 +252,12 @@ pub fn run_plan( let opts = FetchOptions { max_workers: req.max_workers.max(1), - // Improvements round 1: the tier ceiling split across the distinct CDNs (floor 6), so - // the whole window is reachable on 1 host or on all 3. per_host_cap: host_cap, timeout: REQUEST_TIMEOUT, headers: vec![("User-Agent".to_string(), super::USER_AGENT.to_string())], ca_bundle_path: req.ca_bundle_path.clone(), process_workers: req.process_workers.max(1), label: req.label.clone(), - // Whole-body mode (Epic chunks are ≤ ~1 MiB compressed; the core's byte budget scales - // with the window); `stream` and any future field keep the core's defaults. ..FetchOptions::default() }; diff --git a/app/src/main/rust/stores/src/epic/jni.rs b/app/src/main/rust/stores/src/epic/jni.rs index 937d6e42a..1c3c2a01e 100644 --- a/app/src/main/rust/stores/src/epic/jni.rs +++ b/app/src/main/rust/stores/src/epic/jni.rs @@ -1,25 +1,3 @@ -//! JNI exports for `com.droiddeck.launcher.stores.epic.EpicNative`. -//! -//! ```text -//! nativeStart(manifest: byte[], installDir: String, chunkCacheDir: String, cdnPrefixes: String[], -//! pendingFileIdx: int[], expectedChunks: int, expectedBytes: long, -//! caBundlePath: String, maxWorkers: int, processWorkers: int, -//! listener: EpicNative.Listener) -> long handle (0 = not started) -//! nativeAssemble(manifest: byte[], installDir: String, chunkCacheDir: String, fileIdx: int[], -//! listener: EpicNative.Listener) -> long handle (0 = not started) -//! nativeCancel(handle) — flips the run's AtomicBool; fetch or assembly stops promptly -//! nativeRelease(handle) — frees the handle (after onComplete) -//! ``` -//! `chunkCacheDir` = "" keeps the cache at `<installDir>/.chunks`; any other directory is a -//! scratch cache (see `assemble.rs`). Listener (all methods run on native threads): -//! `onPlan(int chunksTotal, long bytesTotal, String chunkDir)` once before any fetch, -//! `onProgress(long bytesDone, long bytesTotal, int done, int total)` per chunk fetched (or, in -//! an assembly run, per file written), `onLog(String line)`, -//! `onComplete(boolean success, String error, long bytes)`. -//! -//! Planning (manifest parse + cross-check) runs synchronously on the calling thread so a plan -//! failure returns 0 BEFORE any fetch and Java can fall back to its own pool; the fetch runs on -//! a new thread. JavaVM attach / GlobalRef handling is the same shape as the GOG facade. use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -59,8 +37,6 @@ fn android_log(message: &str) { } } -/// Per-run native state behind the `jlong` handle — the same type for a fetch run and an -/// assembly run, so one `nativeCancel` / `nativeRelease` pair serves both. pub struct EpicDownloadHandle { cancel: Arc<AtomicBool>, } @@ -200,7 +176,6 @@ fn call_on_complete(env: &mut JNIEnv, listener: &JObject, success: bool, error: clear_pending_exception(env); } -/// Log to logcat AND to the listener (the Kotlin manager mirrors it into its own debug log). fn log_both(env: &mut JNIEnv, listener: &JObject, line: &str) { android_log(line); call_on_log(env, listener, line); @@ -334,8 +309,6 @@ fn run_on_thread( let bytes_total = plan.total_bytes; let chunks_total = plan.needed.len() as u64; - // Progress fires from the core's process-pool threads; attach each as a daemon like the - // Steam engine's `dispatch_download_progress` does. let progress = |bytes_done: u64, chunks_done: u64| { let Ok(mut env) = vm.attach_current_thread_as_daemon() else { return; @@ -371,11 +344,6 @@ fn run_on_thread( ); } -/// `EpicNative.nativeAssemble(manifest, installDir, chunkCacheDir, fileIdx, listener)`: write -/// the files `fileIdx` names from the chunk cache (see `assemble.rs`). Parse + plan run on the -/// calling thread, so bad inputs return 0 (after `onComplete(false, reason, 0)`) before anything -/// is written; the assembly runs on a new thread and reports per file through `onProgress(bytes, -/// bytesTotal, filesDone, filesTotal)`, then `onComplete(success, error, bytesWritten)`. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_nativeAssemble( mut env: JNIEnv, @@ -481,7 +449,5 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_epic_EpicNative_native if handle == 0 { return; } - // The worker thread owns its own Arc clone of the cancel flag and its own GlobalRef, so - // freeing the handle is safe even if a (cancelled) run is still winding down. drop(unsafe { Box::from_raw(handle as *mut EpicDownloadHandle) }); } diff --git a/app/src/main/rust/stores/src/epic/manifest.rs b/app/src/main/rust/stores/src/epic/manifest.rs index 7fbe834ea..d0b38fdd2 100644 --- a/app/src/main/rust/stores/src/epic/manifest.rs +++ b/app/src/main/rust/stores/src/epic/manifest.rs @@ -1,43 +1,24 @@ -//! Epic build manifest parsers — a 1:1 port of `EpicDownloadManager.parseManifest` (binary, -//! magic `0x44BEC00C`) and `parseJsonManifest` (legacy JSON manifests). -//! -//! Every quirk of the Java reader is kept on purpose (the Java manager stays the reference and -//! the two engines must derive the same plan from the same bytes): section skips by declared -//! size, `readFString` UTF-16LE/ASCII decoding, duplicate-GUID handling (last value, first -//! position), the JSON blob decoding (`blobToNum`) and its fallbacks, and "any exception → -//! parse failed". use std::collections::HashMap; use std::io::Read; -/// Binary manifest magic (`EpicDownloadManager.parseManifest`). pub const MANIFEST_MAGIC: u32 = 0x44BE_C00C; -/// One entry of the ChunkDataList section (`EpicDownloadManager.ChunkInfo`). #[derive(Clone, Debug, PartialEq, Eq)] pub struct ChunkInfo { - /// Four uint32 in binary read order. pub guid: [u32; 4], - /// uint64 "hash" that forms the first half of the chunk file name. pub hash: u64, - /// SHA-1 of the DECOMPRESSED chunk; `None` for JSON manifests (Java: `sha1 == null`). pub sha1: Option<[u8; 20]>, - /// Subfolder number (DECIMAL `%02d`). `i32` so the JSON parser's `Integer.parseInt` range is - /// representable exactly (binary manifests always yield 0..=255). pub group_num: i32, - /// Uncompressed size (0 for JSON manifests). pub window_size: i32, - /// Compressed (wire) size. Signed like the Java `long`; JSON manifests may yield anything. pub file_size: i64, } impl ChunkInfo { - /// `String.format("%08X%08X%08X%08X", guid[0..4])`. pub fn guid_str(&self) -> String { guid_to_string(&self.guid) } - /// `ChunkInfo.getPath`: `"<chunkDir>/<%02d groupNum>/<%016X hash>_<GUID>.chunk"`. pub fn path(&self, chunk_dir: &str) -> String { format!( "{}/{:02}/{:016X}_{}.chunk", @@ -48,8 +29,6 @@ impl ChunkInfo { ) } - /// Java `chunk.sha1 != null && length == 20 && !allZero` — the condition under which the - /// streaming downloader verifies the decompressed chunk. pub fn verifiable_sha1(&self) -> Option<&[u8; 20]> { match &self.sha1 { Some(h) if h.iter().any(|b| *b != 0) => Some(h), @@ -57,7 +36,6 @@ impl ChunkInfo { } } - /// `Math.max(chunk.fileSize, 1)` — the byte credit the Java pool uses for totals/progress. pub fn credit_bytes(&self) -> u64 { if self.file_size < 1 { 1 @@ -67,13 +45,10 @@ impl ChunkInfo { } } -/// One file part referencing a chunk (`EpicDownloadManager.ChunkPart`). #[derive(Clone, Debug, PartialEq, Eq)] pub struct ChunkPart { pub guid: [u32; 4], - /// Offset into the DECOMPRESSED chunk (Java `int`). pub offset: i32, - /// Part length (Java `int`; summed as `size & 0xFFFFFFFFL`). pub size: i32, } @@ -83,26 +58,20 @@ impl ChunkPart { } } -/// One entry of the FileManifestList section (`EpicDownloadManager.FileInfo`). #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct FileInfo { pub filename: String, pub parts: Vec<ChunkPart>, - /// Install-tag names; empty = required/base file. pub install_tags: Vec<String>, - /// Full-file SHA-1; `None` for JSON manifests. pub sha1: Option<[u8; 20]>, } impl FileInfo { - /// `FileInfo.fileSize()` = Σ `(part.size & 0xFFFFFFFFL)`. pub fn file_size(&self) -> u64 { self.parts.iter().map(|p| p.size as u32 as u64).sum() } } -/// Parsed manifest (`EpicDownloadManager.EpicManifest.ParsedManifest`, minus the CDN list which -/// the Java side still owns). #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Manifest { pub chunk_dir: String, @@ -111,8 +80,6 @@ pub struct Manifest { } impl Manifest { - /// `byGuid` map as `uniqueChunksForFiles` builds it: `LinkedHashMap.put` per chunk, so a - /// duplicate GUID resolves to the LAST `ChunkInfo`. pub fn chunk_index_by_guid(&self) -> HashMap<String, usize> { let mut map = HashMap::with_capacity(self.unique_chunks.len()); for (i, c) in self.unique_chunks.iter().enumerate() { @@ -126,7 +93,6 @@ pub fn guid_to_string(guid: &[u32; 4]) -> String { format!("{:08X}{:08X}{:08X}{:08X}", guid[0], guid[1], guid[2], guid[3]) } -/// `chunkDir` from the binary header version (`parseManifest`). pub fn chunk_dir_for_binary_version(version: i32) -> &'static str { if version >= 15 { "ChunksV4" @@ -139,7 +105,6 @@ pub fn chunk_dir_for_binary_version(version: i32) -> &'static str { } } -/// `chunkDir` from `ManifestFileVersion` (`parseJsonManifest`; note the different `< 3` default). pub fn chunk_dir_for_json_version(version: i32) -> &'static str { if version >= 15 { "ChunksV4" @@ -152,10 +117,7 @@ pub fn chunk_dir_for_json_version(version: i32) -> &'static str { } } -// ── little-endian cursor with ByteBuffer-like failure semantics ───────────────────────────── -/// Mirrors the `ByteBuffer` calls the Java parser makes: every under-run or out-of-range -/// `position()` is an exception there, which the Java code turns into "parse failed" (null). struct Cursor<'a> { buf: &'a [u8], pos: usize, @@ -170,7 +132,6 @@ impl<'a> Cursor<'a> { self.pos } - /// `ByteBuffer.position(p)` — `p` must be within `0..=limit`. fn set_position(&mut self, p: i64) -> Result<(), String> { if p < 0 || p as u64 > self.buf.len() as u64 { return Err(format!("position {p} out of range (limit {})", self.buf.len())); @@ -223,7 +184,6 @@ impl<'a> Cursor<'a> { Ok(out) } - /// `ByteBuffer.position(position + n)` with the same range check. fn skip(&mut self, n: i64) -> Result<(), String> { let target = self.pos as i64 + n; self.set_position(target) @@ -233,23 +193,19 @@ impl<'a> Cursor<'a> { &self.buf[self.pos..] } - /// `EpicDownloadManager.readFString`: int32 length; `0` → ""; negative → `(-len)-1` UTF-16LE - /// code units + 2-byte terminator; positive → `len-1` US-ASCII bytes + 1-byte terminator. fn fstring(&mut self) -> Result<String, String> { let length = self.i32()?; if length == 0 { return Ok(String::new()); } if length < 0 { - // Java: `int chars = (-length) - 1;` — `-Integer.MIN_VALUE` wraps to itself, then -1 - // gives Integer.MAX_VALUE; `new byte[chars * 2]` overflows negative → exception. let chars = length.wrapping_neg().wrapping_sub(1); let byte_len = chars.checked_mul(2).ok_or("fstring length overflow")?; if byte_len < 0 { return Err("negative fstring length".to_string()); } let bytes = self.take(byte_len as usize)?; - self.take(2)?; // null terminator + self.take(2)?; let units: Vec<u16> = bytes .chunks_exact(2) .map(|p| u16::from_le_bytes([p[0], p[1]])) @@ -257,8 +213,8 @@ impl<'a> Cursor<'a> { Ok(String::from_utf16_lossy(&units)) } else { let bytes = self.take((length - 1) as usize)?; - self.take(1)?; // null terminator - // `new String(bytes, US_ASCII)`: every byte > 0x7F decodes to U+FFFD. + self.take(1)?; + Ok(bytes .iter() .map(|&b| if b < 0x80 { b as char } else { '\u{FFFD}' }) @@ -267,10 +223,7 @@ impl<'a> Cursor<'a> { } } -// ── binary manifest ────────────────────────────────────────────────────────────────────── -/// `EpicDownloadManager.parseManifest(bytes)`: binary when the magic matches, otherwise the JSON -/// parser. `Err` wherever Java returns `null`. pub fn parse_manifest(bytes: &[u8]) -> Result<Manifest, String> { let mut cur = Cursor::new(bytes); let magic = cur.u32()?; @@ -284,7 +237,7 @@ fn parse_binary_after_magic(cur: &mut Cursor<'_>) -> Result<Manifest, String> { let header_size = cur.i32()?; let size_uncompressed = cur.i32()?; let _size_compressed = cur.i32()?; - cur.skip(20)?; // SHA-1 of the body, not checked by the Java parser + cur.skip(20)?; let stored_as = cur.u8()?; let version = cur.i32()?; let chunk_dir = chunk_dir_for_binary_version(version); @@ -297,8 +250,6 @@ fn parse_binary_after_magic(cur: &mut Cursor<'_>) -> Result<Manifest, String> { if size_uncompressed < 0 { return Err("negative sizeUncompressed".to_string()); } - // Java: `inflater.inflate(new byte[sizeUncompressed])` — a single call that fills at most - // `sizeUncompressed` bytes; anything else (short, corrupt) is a size mismatch → null. let mut out = Vec::with_capacity(size_uncompressed as usize); let mut dec = flate2::read::ZlibDecoder::new(body_slice).take(size_uncompressed as u64); dec.read_to_end(&mut out) @@ -317,18 +268,16 @@ fn parse_binary_after_magic(cur: &mut Cursor<'_>) -> Result<Manifest, String> { let mut b = Cursor::new(body); - // ManifestMeta: skipped by its declared size (the size field counts itself). let meta_size = b.i32()?; let meta_end = b.position() as i64 - 4 + meta_size as i64; b.set_position(meta_end)?; - // ChunkDataList let cdl_start = b.position(); let cdl_size = b.i32()?; let _cdl_version = b.u8()?; let chunk_count = b.i32()?; if chunk_count < 0 { - return Err("negative chunkCount".to_string()); // `new ArrayList<>(negative)` throws + return Err("negative chunkCount".to_string()); } let chunk_count = chunk_count as usize; let mut chunks: Vec<ChunkInfo> = (0..chunk_count) @@ -361,7 +310,6 @@ fn parse_binary_after_magic(cur: &mut Cursor<'_>) -> Result<Manifest, String> { } b.set_position(cdl_start as i64 + cdl_size as i64)?; - // FileManifestList let fml_start = b.position(); let fml_size = b.i32()?; let _fml_version = b.u8()?; @@ -375,12 +323,12 @@ fn parse_binary_after_magic(cur: &mut Cursor<'_>) -> Result<Manifest, String> { f.filename = b.fstring()?; } for _ in 0..file_count { - b.fstring()?; // symlink targets + b.fstring()?; } for f in files.iter_mut() { f.sha1 = Some(b.sha1()?); } - b.skip(file_count as i64)?; // per-file flags byte + b.skip(file_count as i64)?; for f in files.iter_mut() { let tag_count = b.i32()?; let mut j = 0; @@ -415,8 +363,6 @@ fn parse_binary_after_magic(cur: &mut Cursor<'_>) -> Result<Manifest, String> { }) } -/// `LinkedHashMap<String, ChunkInfo>` keyed by GUID string: a repeated key keeps its FIRST -/// position but takes the LAST value. fn dedup_last_wins_first_position(chunks: Vec<ChunkInfo>) -> Vec<ChunkInfo> { let mut order: Vec<ChunkInfo> = Vec::with_capacity(chunks.len()); let mut index: HashMap<String, usize> = HashMap::with_capacity(chunks.len()); @@ -433,10 +379,7 @@ fn dedup_last_wins_first_position(chunks: Vec<ChunkInfo>) -> Vec<ChunkInfo> { order } -// ── JSON manifest (older games) ────────────────────────────────────────────────────────── -/// `org.json` value → string coercion as `optString` / `getString` do on Android (numbers and -/// booleans print as-is, JSON null prints "null", containers print their JSON). fn json_to_string(v: &serde_json::Value) -> String { match v { serde_json::Value::String(s) => s.clone(), @@ -445,9 +388,6 @@ fn json_to_string(v: &serde_json::Value) -> String { } } -/// `EpicDownloadManager.blobToNum`: a JSON manifest writes every number as a "blob", each byte -/// as three decimal digits, least significant first ("013000000000" = 13). Bits past 64 are -/// dropped; `None` when a group is not a number (Java: `NumberFormatException`). pub fn blob_to_num(s: &str) -> Option<u64> { let bytes = s.as_bytes(); let mut num: u64 = 0; @@ -464,7 +404,6 @@ pub fn blob_to_num(s: &str) -> Option<u64> { Some(num) } -/// `obj.optString(key, fallback)`. fn opt_string(obj: &serde_json::Map<String, serde_json::Value>, key: &str, fallback: &str) -> String { match obj.get(key) { Some(v) => json_to_string(v), @@ -472,16 +411,12 @@ fn opt_string(obj: &serde_json::Map<String, serde_json::Value>, key: &str, fallb } } -/// `(int) Long.parseLong(hex8, 16)` — the Java GUID quarter parse; invalid hex throws out of the -/// whole parse. fn parse_guid_quarter(s: &str) -> Result<u32, String> { let v = i64::from_str_radix(s, 16).map_err(|e| format!("bad guid hex '{s}': {e}"))?; Ok(v as i32 as u32) } fn parse_guid_from_hex(s: &str) -> Result<[u32; 4], String> { - // Java substrings by UTF-16 unit and `parseLong` throws on anything non-hex; a non-ASCII - // prefix can never parse, so refuse it before slicing (keeps the slice on char boundaries). if s.len() < 32 || !s.as_bytes()[..32].is_ascii() { return Err(format!("bad guid hex '{s}'")); } @@ -493,7 +428,6 @@ fn parse_guid_from_hex(s: &str) -> Result<[u32; 4], String> { ]) } -/// `EpicDownloadManager.parseJsonManifest`. pub fn parse_json_manifest(bytes: &[u8]) -> Result<Manifest, String> { let text = String::from_utf8_lossy(bytes); let root: serde_json::Value = @@ -513,7 +447,6 @@ pub fn parse_json_manifest(bytes: &[u8]) -> Result<Manifest, String> { let data_group_list = root.get("DataGroupList").and_then(|v| v.as_object()); let chunk_filesize_list = root.get("ChunkFilesizeList").and_then(|v| v.as_object()); - // `LinkedHashMap<String, ChunkInfo>` keyed by the RAW key string (not the normalized GUID). let mut chunk_order: Vec<ChunkInfo> = Vec::new(); let mut chunk_index: HashMap<String, usize> = HashMap::new(); for (guid_hex, hash_value) in chunk_hash_list.iter() { @@ -598,12 +531,9 @@ pub fn parse_json_manifest(bytes: &[u8]) -> Result<Manifest, String> { }) } -// ── tests ──────────────────────────────────────────────────────────────────────────────── #[cfg(test)] pub(crate) mod test_support { - //! Synthetic binary-manifest builder used by this module's tests and by `plan`/`driver` - //! tests. Emits exactly the layout `parseManifest` walks (meta / CDL / FML with sizes). use super::*; use std::io::Write; @@ -621,7 +551,6 @@ pub(crate) mod test_support { pub name: String, pub sha1: [u8; 20], pub tags: Vec<String>, - /// (guid, offset, size) pub parts: Vec<([u32; 4], i32, i32)>, } @@ -648,17 +577,14 @@ pub(crate) mod test_support { v } - /// Body (meta + CDL + FML) without the outer header. pub fn build_body(chunks: &[TestChunk], files: &[TestFile], meta_padding: usize) -> Vec<u8> { let mut body = Vec::new(); - // ManifestMeta: size field counts itself. let meta_size = 4 + meta_padding; body.extend_from_slice(&(meta_size as i32).to_le_bytes()); body.extend(std::iter::repeat(0xEEu8).take(meta_padding)); - // ChunkDataList let mut cdl = Vec::new(); - cdl.push(0u8); // version + cdl.push(0u8); cdl.extend_from_slice(&(chunks.len() as i32).to_le_bytes()); for c in chunks { for g in c.guid { @@ -680,14 +606,12 @@ pub(crate) mod test_support { for c in chunks { cdl.extend_from_slice(&c.file_size.to_le_bytes()); } - // Trailing padding the parser must skip via cdlSize. cdl.extend_from_slice(&[0xAA, 0xBB, 0xCC]); body.extend_from_slice(&((cdl.len() + 4) as i32).to_le_bytes()); body.extend_from_slice(&cdl); - // FileManifestList let mut fml = Vec::new(); - fml.push(0u8); // version + fml.push(0u8); fml.extend_from_slice(&(files.len() as i32).to_le_bytes()); for f in files { if f.name.is_ascii() { @@ -697,13 +621,13 @@ pub(crate) mod test_support { } } for _ in files { - fml.extend_from_slice(&fstring_ascii("")); // symlink target + fml.extend_from_slice(&fstring_ascii("")); } for f in files { fml.extend_from_slice(&f.sha1); } for _ in files { - fml.push(0); // flags + fml.push(0); } for f in files { fml.extend_from_slice(&(f.tags.len() as i32).to_le_bytes()); @@ -714,7 +638,6 @@ pub(crate) mod test_support { for f in files { fml.extend_from_slice(&(f.parts.len() as i32).to_le_bytes()); for (guid, offset, size) in &f.parts { - // struct size 4 + 16 + 4 + 4 = 28, plus 2 bytes of padding the parser skips. fml.extend_from_slice(&30i32.to_le_bytes()); for g in guid { fml.extend_from_slice(&g.to_le_bytes()); @@ -729,7 +652,6 @@ pub(crate) mod test_support { body } - /// Full manifest bytes: header + (optionally zlib-compressed) body. pub fn build_manifest( chunks: &[TestChunk], files: &[TestFile], @@ -745,7 +667,7 @@ pub(crate) mod test_support { } else { body.clone() }; - let header_size: i32 = 41 + 3; // 3 bytes of extra header the parser must jump over + let header_size: i32 = 41 + 3; let mut out = Vec::new(); out.extend_from_slice(&MANIFEST_MAGIC.to_le_bytes()); out.extend_from_slice(&header_size.to_le_bytes()); @@ -916,13 +838,9 @@ mod tests { fn bad_uncompressed_size_is_a_parse_failure() { let good = build_manifest(&sample_chunks(), &sample_files(), 21, true); let real = i32::from_le_bytes([good[8], good[9], good[10], good[11]]); - // Claim MORE than the body inflates to: Java's single `inflate()` returns fewer bytes - // than `sizeUncompressed` → "Decomp size mismatch" → null. let mut bigger = good.clone(); bigger[8..12].copy_from_slice(&(real + 1).to_le_bytes()); assert!(parse_manifest(&bigger).is_err()); - // Claim LESS: Java fills exactly `sizeUncompressed` bytes (silently truncating), then the - // FileManifestList end position lands past the limit → exception → null. let mut smaller = good; smaller[8..12].copy_from_slice(&(real - 1).to_le_bytes()); assert!(parse_manifest(&smaller).is_err()); @@ -933,7 +851,7 @@ mod tests { let mut v = fstring_ascii("abc"); v.extend_from_slice(&fstring_utf16("dé")); v.extend_from_slice(&fstring_ascii("")); - v.extend_from_slice(&[4, 0, 0, 0, b'a', 0xFF, b'b', 0]); // non-ASCII byte → U+FFFD + v.extend_from_slice(&[4, 0, 0, 0, b'a', 0xFF, b'b', 0]); let mut c = Cursor::new(&v); assert_eq!(c.fstring().unwrap(), "abc"); assert_eq!(c.fstring().unwrap(), "dé"); diff --git a/app/src/main/rust/stores/src/epic/plan.rs b/app/src/main/rust/stores/src/epic/plan.rs index 3dc2eeeb8..5f96e338c 100644 --- a/app/src/main/rust/stores/src/epic/plan.rs +++ b/app/src/main/rust/stores/src/epic/plan.rs @@ -1,19 +1,9 @@ -//! Chunk plan — the part of `EpicDownloadManager.install` between "manifest parsed" and "submit -//! to the pool", ported 1:1: install-tag file selection (`resolveInstallFiles` / -//! `getRequiredInstallFiles`), the unique-chunk list for a file set (`uniqueChunksForFiles`), -//! the byte total (`Σ max(fileSize, 1)`), chunk cache paths and CDN URLs. -//! -//! At runtime the Java manager passes the INDICES of its `pendingFiles` (post delta/verify), so -//! the file selection functions here are exercised by the unit tests (and available to a future -//! caller) while the driver only runs `unique_chunks_for_files` on Java's pending set — the same -//! function Java runs, on the same manifest, in the same order. use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use super::manifest::{ChunkInfo, Manifest}; -/// `getRequiredInstallFiles`: files with no install tags; if NONE carries a tag, every file. pub fn required_install_files(m: &Manifest) -> Vec<usize> { let required: Vec<usize> = m .files @@ -29,11 +19,6 @@ pub fn required_install_files(m: &Manifest) -> Vec<usize> { } } -/// `resolveInstallFiles(manifest, selectedTags)`: -/// - `None` → every file (legacy / DLC / overlay path); -/// - `Some([])` → required(base) files only; -/// - otherwise → base files + any file carrying at least one selected tag, falling back to -/// required-only when nothing matched. pub fn resolve_install_files(m: &Manifest, selected_tags: Option<&[String]>) -> Vec<usize> { let Some(tags) = selected_tags else { return (0..m.files.len()).collect(); @@ -59,8 +44,6 @@ pub fn resolve_install_files(m: &Manifest, selected_tags: Option<&[String]>) -> } } -/// `uniqueChunksForFiles(manifest, files)`: chunk indices (into `unique_chunks`) in first-seen -/// order over `files[*].parts`, each GUID once, GUIDs absent from the chunk list dropped. pub fn unique_chunks_for_files(m: &Manifest, file_indices: &[usize]) -> Vec<usize> { let by_guid: HashMap<String, usize> = m.chunk_index_by_guid(); let mut seen: HashSet<String> = HashSet::new(); @@ -81,7 +64,6 @@ pub fn unique_chunks_for_files(m: &Manifest, file_indices: &[usize]) -> Vec<usiz chunks } -/// `totalBytes += Math.max(chunk.fileSize, 1)` over the needed chunks. pub fn total_credit_bytes(m: &Manifest, chunk_indices: &[usize]) -> u64 { chunk_indices .iter() @@ -90,15 +72,10 @@ pub fn total_credit_bytes(m: &Manifest, chunk_indices: &[usize]) -> u64 { .sum() } -/// `<installDir>/.chunks`. pub fn chunk_cache_dir(install_dir: &str) -> PathBuf { Path::new(install_dir).join(super::CHUNK_CACHE_DIR) } -/// Where this run's chunks go: `cache_dir` when the caller named one, else the default -/// `<installDir>/.chunks`. A separate directory exists so the cache can sit on internal storage -/// while the game installs to an SD card: a chunk is written once into the cache and once into -/// the assembled file, and with both on a 26 MB/s card the second write starved the fetchers. pub fn resolve_cache_dir(install_dir: &str, cache_dir: &str) -> PathBuf { if cache_dir.is_empty() { chunk_cache_dir(install_dir) @@ -107,37 +84,23 @@ pub fn resolve_cache_dir(install_dir: &str, cache_dir: &str) -> PathBuf { } } -/// Final cache file for a chunk: `new File(chunkCacheDir, chunk.guidStr())`. pub fn cached_chunk_path(cache_dir: &Path, chunk: &ChunkInfo) -> PathBuf { cache_dir.join(chunk.guid_str()) } -/// The Java pool's skip rule: a chunk whose cache file EXISTS (any size) is not fetched. pub fn is_chunk_cached(cache_dir: &Path, chunk: &ChunkInfo) -> bool { cached_chunk_path(cache_dir, chunk).exists() } -/// One CDN entry as the Java manager passes it: `cdn.baseUrl + cdn.cloudDir` (auth params are -/// never used on chunk URLs). Chunk URL = `prefix + "/" + chunk.getPath(chunkDir)`. pub fn chunk_url(cdn_prefix: &str, chunk_dir: &str, chunk: &ChunkInfo) -> String { format!("{}/{}", cdn_prefix, chunk.path(chunk_dir)) } -/// Undo the JSON string escaping the Java CDN scanner passes through. `EpicApiClient` re-serializes -/// the manifest API response with Android's `org.json`, which escapes every `/` as `\/`, and -/// `parseCdnUrls` takes the raw substrings — so a base URL arrives as `https:\/\/host\` and a cloud -/// dir as `/Builds\/Org\/…\/default\`. Java's `HttpURLConnection` (OkHttp) treats `\` as `/` and -/// only ever reached the first CDN, so it never noticed; reqwest's URL parser does the same -/// mapping, which leaves empty `//` path segments that Fastly/Akamai tolerate but CloudFront -/// rejects outright (immediate 4xx). Produce the URL Java intended: `\/` → `/`, any other `\` -/// dropped, and no trailing slash (the chunk path is joined with one). pub fn normalize_prefix(raw: &str) -> String { let unescaped = raw.replace("\\/", "/").replace('\\', ""); unescaped.trim_end_matches('/').to_string() } -/// Distinct CDN prefixes in first-seen order, normalized (the fetch core wants one host key per -/// distinct endpoint; Java would simply try an identical URL twice, which changes nothing). pub fn distinct_prefixes(cdn_prefixes: &[String]) -> Vec<String> { let mut seen = HashSet::new(); let mut out = Vec::new(); @@ -153,9 +116,6 @@ pub fn distinct_prefixes(cdn_prefixes: &[String]) -> Vec<String> { out } -/// Improvements round 1: split the window across the distinct CDNs so the whole ceiling is -/// reachable whatever the host count — `max(6, ceil(max_workers / hosts))` (3 Epic CDNs at a -/// ceiling of 32 → 11 each; a single host gets all 32). pub fn per_host_cap(max_workers: usize, host_count: usize) -> usize { let hosts = host_count.max(1); let workers = max_workers.max(1); @@ -176,14 +136,10 @@ mod tests { #[test] fn install_tag_rules_match_java() { let m = manifest(); - // null → all assert_eq!(resolve_install_files(&m, None), vec![0, 1, 2]); - // empty → required only (file 0 has no tags) assert_eq!(resolve_install_files(&m, Some(&[])), vec![0]); - // German → base + the German file let de = vec!["German".to_string(), "de-DE".to_string()]; assert_eq!(resolve_install_files(&m, Some(&de)), vec![0, 1]); - // unknown tag → nothing extra matched, but base files matched, so base only let xx = vec!["Klingon".to_string()]; assert_eq!(resolve_install_files(&m, Some(&xx)), vec![0]); } @@ -204,7 +160,6 @@ mod tests { let mut files = sample_files(); files[0].tags = vec!["English".to_string()]; let m = parse_manifest(&build_manifest(&sample_chunks(), &files, 21, false)).unwrap(); - // No untagged file, unknown tag selected → matched nothing → required → (none) → all. let xx = vec!["Klingon".to_string()]; assert_eq!(resolve_install_files(&m, Some(&xx)), vec![0, 1, 2]); } @@ -212,12 +167,10 @@ mod tests { #[test] fn unique_chunks_follow_first_seen_part_order() { let m = manifest(); - // file 2 first: chunk c[2] then c[0]; then file 0: c[0] (dup), c[1]. assert_eq!(unique_chunks_for_files(&m, &[2, 0]), vec![2, 0, 1]); assert_eq!(unique_chunks_for_files(&m, &[1]), vec![1]); assert!(unique_chunks_for_files(&m, &[]).is_empty()); assert!(unique_chunks_for_files(&m, &[99]).is_empty(), "out-of-range index ignored"); - // total = 700000 + max(0,1) + 9 assert_eq!(total_credit_bytes(&m, &[0, 1, 2]), 700_000 + 1 + 9); } @@ -256,13 +209,11 @@ mod tests { #[test] fn prefixes_are_unescaped_from_the_java_json_scan() { - // Exactly what Bannerlator's Epic debug log shows its Java manager extracting for a real title. let raw = "https:\\/\\/egdownload.fastly-edge.com\\/Builds\\/Org\\/o-83e8\\/04b7\\/default\\"; assert_eq!( normalize_prefix(raw), "https://egdownload.fastly-edge.com/Builds/Org/o-83e8/04b7/default" ); - // Already-clean input is untouched (minus a trailing slash). assert_eq!( normalize_prefix("https://download.epicgames.com/Builds/x/default/"), "https://download.epicgames.com/Builds/x/default" diff --git a/app/src/main/rust/stores/src/fetch_core.rs b/app/src/main/rust/stores/src/fetch_core.rs index e700ee3a2..9dc96faa6 100644 --- a/app/src/main/rust/stores/src/fetch_core.rs +++ b/app/src/main/rust/stores/src/fetch_core.rs @@ -1,28 +1,3 @@ -//! Shared adaptive HTTP fetch core for the store download engines (Epic / GOG / Amazon). -//! -//! This is the generic half of the fetch layer Bannerlator's Steam depot writer grew: an -//! adaptive in-flight window (slow-start → hold at plateau → shrink on REAL errors only), a -//! speed-ranked per-host-capped scheduler, an in-flight RAW byte budget sized from the window -//! ceiling, per-item retry with exponential back-off + host rotation, one current-thread tokio -//! runtime driving every request through a [`FuturesUnordered`], and an mpsc hand-off to a sync -//! process pool that calls the adapter's [`FetchSink::process`] (inflate + hash + write). -//! -//! The tunables come from [`crate::fetch_tuning`], copied number for number from Bannerlator's -//! depot writer, so a `fetch-window` line from either app reads against the same tuning notes. -//! -//! What the core does NOT know: manifests, resume/skip rules, output layout, hashing. The adapter -//! builds the item list (already excluding present+verified items, exactly as its Java manager -//! does), maps host index → URL, and does everything with the body inside its sink. -//! -//! Log grammar (all lines go through the `log` callback; `label` is adapter-chosen, e.g. -//! `epic app=Fortnite`): -//! - `fetch-start label=… items=… bytes_reserved=… hosts=… ceiling=… budget=…MiB tier_max=… per_host_cap=… distinct_hosts=… window=… mode=body|stream` -//! - `fetch-window label=… window=N (min=… max=…) in_flight=… last=…MB/s ewma=…MB/s best=…MB/s reason=… cooldown=…ms err_rate=…% phase=… rtt=…ms budget_stalls=… host_stalls=…` -//! (same fields as the Steam engine's `fetch-window depot=…` line, so the two are A/B-comparable) -//! - `throughput label=… overall=…MB/s total=…MB elapsed=…s used=x/y servers: [host …MB/s …MB] …` every 5 s -//! - `fetch-end label=… items_ok=… bytes=… credited=… elapsed_ms=… avg_mbps=… peak_mbps=… result=ok|cancelled|error [error=…]` -//! -//! `MB/s` here means MiB/s (1024²), matching the Steam engine's lines. use crate::fetch_tuning::{ budget_admits, connect_timeout, inflight_budget_bytes, retry_backoff_millis, AsyncFetchError, @@ -46,95 +21,42 @@ use std::thread; use std::time::{Duration, Instant}; use tokio::sync::{Notify, Semaphore}; -/// Maximum attempts per item (fetch failures AND `SinkError::Retry` both count). Same as Steam. pub const MAX_ITEM_ATTEMPTS: u32 = MAX_CHUNK_ATTEMPTS; -/// How often the throughput reporter samples (and logs) — the samples feed `peak_mbps`. pub const THROUGHPUT_SAMPLE_INTERVAL_MS: u64 = 5_000; -/// Upper bound on how long the driver sits in one `await` before re-checking cancel/error/feedback. const DRIVER_POLL_MS: u64 = 250; -// ── Delay awareness (bufferbloat) ── -// On a 5G hotspot the window kept growing (31 → 84) while the per-request delay went from 3 s to -// 11 s and throughput stayed at 10-20 MB/s: every extra request only queued behind the others in -// the link's buffer. The signal used here is time-to-first-byte (request sent → response headers), -// which on a link with a deep buffer grows with the queue, while on a fast link it stays at a few -// round trips however wide the window gets. The baseline is the lowest per-probe mean seen. -/// A probe needs at least this many TTFB samples before its mean is trusted. pub const LATENCY_MIN_SAMPLES: u32 = 3; -/// Mean TTFB at or above `baseline ×` this counts as queueing delay… pub const LATENCY_HOLD_FACTOR: f64 = 2.5; -/// …and only when it is also this far above the baseline in absolute terms: a fast link whose -/// TTFB goes from 20 ms to 60 ms (new connections' TLS handshakes) is not bufferbloat. pub const LATENCY_MIN_EXCESS_MS: f64 = 250.0; -/// Throughput above `best × (1 + this)` is a gain that justifies the extra delay: hold, but never -/// shrink, while it lasts. pub const LATENCY_GAIN_EPS: f64 = 0.25; -/// Gentle proportional shrink while the delay keeps rising (gentler than the error shrink). pub const LATENCY_SHRINK_FACTOR: f64 = 0.85; -// ── Stalled hosts ── -// A CDN host can stop delivering while its connections stay open: on the device the Fastly host -// froze at 49 MB for the rest of the run while the other host carried everything, and its requests -// sat until their idle timeout. A host whose in-flight requests receive no byte for this long, -// while another host is receiving, has its requests cancelled and re-queued and is put to rest. -/// No byte from any of a host's in-flight requests for this long (while another host delivers). pub const HOST_STALL_MS: u64 = if cfg!(test) { 1_500 } else { 10_000 }; -/// First rest for a stalled host; doubles per further stall of the same host, reset on success. pub const HOST_STALL_COOLDOWN_MS: u64 = 30_000; pub const HOST_STALL_COOLDOWN_MAX_MS: u64 = 300_000; -/// How often the driver looks for stalled hosts. const STALL_CHECK_MS: u64 = 250; -/// Stream mode: the longest a request may sit with no byte arriving before it is failed (the -/// store's own timeout when it is shorter). A dead connection on a single-host run, where the -/// stall detector has no healthy host to compare with, never holds an item longer than this. pub const STREAM_IDLE_CAP: Duration = Duration::from_secs(30); -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Public contract -// ───────────────────────────────────────────────────────────────────────────────────────────── -/// One unit of work: a body to fetch from one of the hosts and hand to the sink. #[derive(Clone, Debug)] pub struct FetchItem { - /// Adapter-defined (index into the adapter's own table). Only echoed in errors / logs. pub id: u64, - /// `len == hosts.len()` → `urls[host_idx]`; `len == 1` → the same URL on every host. Any other - /// non-zero length falls back to `urls.get(host_idx)` then `urls[0]`. Empty = fatal. pub urls: Vec<String>, - /// Expected RAW (wire) bytes, for the in-flight byte budget; `0` → nominal 1 MiB. Ignored in - /// stream mode (streams are budgeted piece by piece as they arrive). pub reserve: u64, - /// Inclusive HTTP `Range` (`bytes=a-b`); `None` = whole body. pub range: Option<(u64, u64)>, } #[derive(Clone, Debug)] pub struct FetchOptions { - /// Window ceiling (tier); the core clamps it to `hosts × per_host_cap` and the hard cap. pub max_workers: usize, - /// Max concurrent requests per distinct host (6 unless the store needs otherwise). Honoured - /// as given: 1 host × `per_host_cap` 8 = a ceiling of 8. pub per_host_cap: usize, - /// Whole-body mode: the deadline for the entire request (connect + headers + body). Stream - /// mode: the deadline for the response headers, then an IDLE deadline per received piece. pub timeout: Duration, - /// Sent on EVERY request (applied on the request builder, so they override the client-level - /// defaults — e.g. a store's own `User-Agent`, or `Authorization: Bearer …`). Validated once - /// when the client is built. pub headers: Vec<(String, String)>, - /// PEM bundle path; `""` = the platform roots reqwest/rustls ships with. pub ca_bundle_path: String, - /// Sync process-pool threads (inflate + hash + write). pub process_workers: usize, - /// Log prefix, e.g. `"epic app=Fortnite"`. pub label: String, - /// `false` (default): each item's whole body is buffered and handed to - /// [`FetchSink::process`]. `true`: the body is streamed — pieces go to - /// [`FetchSink::on_chunk`] in order as they arrive, then [`FetchSink::on_finish`]; for - /// multi-GB files (Amazon) that keeps memory at "pieces not yet written", not whole files. - /// Applies to every item of the run. pub stream: bool, } @@ -153,36 +75,20 @@ impl Default for FetchOptions { } } -/// What the sink says about a body it was handed. #[derive(Clone, Debug)] pub enum SinkError { - /// Counts as a failed attempt for that item: host cooled + demoted, back-off, re-dispatch on a - /// different host, up to [`MAX_ITEM_ATTEMPTS`]. In stream mode the whole item is re-streamed - /// (`on_chunk` sees `offset == 0` again). Retry(String), - /// Abort the whole run; this becomes the run's first error. Fatal(String), } -/// Runs on a process-pool thread. Whole-body mode uses [`FetchSink::process`]; stream mode uses -/// [`FetchSink::on_chunk`] + [`FetchSink::on_finish`] (default impls refuse, so a whole-body sink -/// is unaffected by the stream option existing). pub trait FetchSink: Send + Sync { - /// Whole-body mode: the raw wire body. Do inflate + hash verify + write here. Returns the - /// number of bytes to credit to progress (adapter's choice, e.g. decompressed bytes). fn process(&self, item: &FetchItem, body: Vec<u8>) -> Result<u64, SinkError>; - /// Stream mode: one piece of the body, delivered in order (`offset` = bytes of this attempt - /// delivered before this piece). `offset == 0` marks the (re)start of an attempt — truncate any - /// partial output from a previous attempt. Each processed piece credits `data.len()` bytes. fn on_chunk(&self, item: &FetchItem, offset: u64, data: &[u8]) -> Result<(), SinkError> { let _ = (item, offset, data); Err(SinkError::Fatal("streaming not supported by this sink".to_string())) } - /// Stream mode: the body ended after `total_len` bytes. Finalize (rename `.tmp`, verify the - /// hash, …). Returns EXTRA bytes to credit (pieces were already credited as they were - /// written; return 0 for none). fn on_finish(&self, item: &FetchItem, total_len: u64) -> Result<u64, SinkError> { let _ = (item, total_len); Err(SinkError::Fatal("streaming not supported by this sink".to_string())) @@ -191,15 +97,9 @@ pub trait FetchSink: Send + Sync { #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct FetchOutcome { - /// Sum of what the sink returned for every successfully processed item (stream mode: every - /// piece written + every `on_finish` extra; pieces of an attempt that later failed stay - /// counted). pub bytes_credited: u64, - /// First error (fetch after max attempts, sink `Fatal`, client build failure). pub error: Option<String>, - /// `cancel` was observed set. pub cancelled: bool, - /// Items the sink accepted. pub items_ok: u64, } @@ -209,10 +109,6 @@ impl FetchOutcome { } } -/// Blocking: spawns its own current-thread tokio runtime + process pool and returns when every -/// item is processed, `cancel` is set, or the first fatal error is recorded. `progress` fires from -/// a process-pool thread after each successful `process` / each written piece + `on_finish` (the -/// caller throttles). pub fn run_fetch( items: Vec<FetchItem>, hosts: &[String], @@ -254,8 +150,6 @@ mode={}", let error_slot: Mutex<Option<String>> = Mutex::new(None); let reporter_done = AtomicBool::new(false); let meter = BandwidthMeter::new(&hosts); - // Stream mode: per-item "attempt N was rejected by the sink mid-stream" flags, so the driver's - // in-flight stream stops reading instead of feeding pieces nobody wants. let abandon: Vec<AtomicU32> = if stream { items.iter().map(|_| AtomicU32::new(NO_ATTEMPT)).collect() } else { @@ -268,11 +162,6 @@ mode={}", let abandon = &abandon; thread::scope(|scope| { - // Async(fetch) → sync(process): tokio unbounded mpsc drained with `blocking_recv`. Memory is - // bounded by the RAW byte budget enforced at DISPATCH (whole-body) / per piece (stream), not - // by channel capacity. Whole-body mode: ONE channel shared by every worker (any worker takes - // the next body). Stream mode: one channel PER worker and an item's pieces always go to the - // same worker (`idx % workers`), so pieces are written in order. let channel_count = if stream { proc_count } else { 1 }; let mut txs: Vec<tokio::sync::mpsc::UnboundedSender<ProcessMsg>> = Vec::with_capacity(channel_count); @@ -283,7 +172,6 @@ mode={}", txs.push(tx); rxs.push(Arc::new(Mutex::new(rx))); } - // Sync(process) → async(driver): sink verdicts, polled non-blocking by the driver. let (fb_tx, fb_rx) = std_mpsc::channel::<Feedback>(); let bytes_credited = &bytes_credited; let items_ok = &items_ok; @@ -291,8 +179,6 @@ mode={}", let error_slot = &error_slot; let reporter_done = &reporter_done; - // Periodic throughput reporter — atomics only, never on the fetch hot path. Its samples are - // also the `peak_mbps` source. let reporter = scope.spawn(move || { crate::priority::background(); loop { @@ -312,15 +198,12 @@ mode={}", } }); - // ── Process pool ── let mut proc_handles = Vec::with_capacity(proc_count); for worker in 0..proc_count { let rx = Arc::clone(&rxs[if stream { worker } else { 0 }]); let fb = fb_tx.clone(); proc_handles.push(scope.spawn(move || { crate::priority::background(); - // Stream mode: (item → attempt, message) whose pieces the sink rejected; the rest - // of that attempt's pieces are dropped and its Finish becomes a Retry verdict. let mut poisoned: HashMap<usize, (u32, String)> = HashMap::new(); loop { if cancel.load(Ordering::Relaxed) { @@ -344,8 +227,6 @@ mode={}", body, } => { let raw_len = body.len() as u64; - // A body that was already queued when cancel / the first error landed - // must not be written any more. if cancel.load(Ordering::Relaxed) || error_slot.lock().expect("err slot poisoned").is_some() { @@ -354,7 +235,6 @@ mode={}", } let item = &items[idx]; let res = sink.process(item, body); - // Free the budget on PROCESS-COMPLETE (actual raw bytes). in_flight.fetch_sub(raw_len, Ordering::Relaxed); let verdict = match res { Ok(credited) => { @@ -390,7 +270,7 @@ mode={}", let len = data.len() as u64; in_flight.fetch_sub(len, Ordering::Relaxed); if poisoned.get(&idx).is_some_and(|(a, _)| *a == attempts) { - continue; // rest of a rejected attempt + continue; } if cancel.load(Ordering::Relaxed) || error_slot.lock().expect("err slot poisoned").is_some() @@ -471,11 +351,6 @@ mode={}", } drop(fb_tx); - // ── Async fetch driver on one current-thread runtime. `txs` are moved in and dropped when - // the driver returns, which closes the channels so the pool's `blocking_recv` yields - // `None`. ── - // The runtime drives on this (already lowered) thread; `on_thread_start` covers the - // blocking pool it starts for DNS lookups. match tokio::runtime::Builder::new_current_thread() .enable_all() .on_thread_start(crate::priority::background) @@ -515,7 +390,6 @@ mode={}", let _ = reporter.join(); }); - // Final partial-interval sample so a short run still has a peak. meter.sample(); let elapsed = started.elapsed(); let elapsed_secs = elapsed.as_secs_f64().max(0.001); @@ -552,11 +426,7 @@ avg_mbps={avg_mbps:.2} peak_mbps={peak_mbps:.2} result={result}", outcome } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Pure helpers (unit-tested) -// ───────────────────────────────────────────────────────────────────────────────────────────── -/// Bytes reserved against the in-flight budget at DISPATCH (nominal when the adapter has none). pub fn reserve_bytes(item: &FetchItem) -> u64 { if item.reserve == 0 { NOMINAL_CHUNK_RESERVE_BYTES @@ -565,8 +435,6 @@ pub fn reserve_bytes(item: &FetchItem) -> u64 { } } -/// Window bounds: `max` = tier ceiling clamped to distinct-hosts × per-host-cap and the hard cap; -/// `min` = the floor; `bootstrap` = the start. Same arithmetic as the Steam depot writer. pub fn window_bounds( max_workers: usize, distinct_hosts: usize, @@ -579,7 +447,6 @@ pub fn window_bounds( (bootstrap, min, max) } -/// Distinct host count (string equality) — the multiplier for the window's host ceiling. pub fn distinct_host_count(hosts: &[String]) -> usize { let mut keys: Vec<&str> = Vec::new(); for h in hosts { @@ -590,8 +457,6 @@ pub fn distinct_host_count(hosts: &[String]) -> usize { keys.len().max(1) } -/// The URL to use for `item` on host `host_idx` (see [`FetchItem::urls`]). `None` when the item -/// carries no URL at all. pub fn url_for_host(item: &FetchItem, host_idx: usize) -> Option<&str> { if item.urls.is_empty() { return None; @@ -607,7 +472,6 @@ pub fn url_for_host(item: &FetchItem, host_idx: usize) -> Option<&str> { ) } -/// Byte count an inclusive range must deliver; `None` for an inverted range. pub fn range_len(range: (u64, u64)) -> Option<u64> { let (a, b) = range; if b < a { @@ -617,8 +481,6 @@ pub fn range_len(range: (u64, u64)) -> Option<u64> { } } -/// Validates a received body's length: for a range request it must be exactly `b-a+1`; otherwise -/// it must match `Content-Length` when the server sent one (as the Steam client does). pub fn validate_body_len( body_len: u64, content_length: Option<u64>, @@ -653,13 +515,10 @@ pub fn validate_body_len( Ok(()) } -/// Whether an HTTP status is acceptable for this request shape (200 always; 206 only when a range -/// was asked for). pub fn status_accepted(status: u16, ranged: bool) -> bool { status == 200 || (ranged && status == 206) } -/// Classifies a rejected HTTP status the same way the Steam async client does. pub fn classify_status(status: u16) -> FetchFailKind { if status == 429 { FetchFailKind::RateLimited @@ -670,10 +529,6 @@ pub fn classify_status(status: u16) -> FetchFailKind { } } -/// The HTTP status carried by a `send_checked` rejection message, if that is what the error was. -/// A 4xx other than 429 is a CLIENT rejection (bad/expired signed URL, wrong host for this path, -/// 404): it says nothing about congestion, so the driver demotes the host but does not shrink the -/// shared window for it — one CDN that refuses a path must not throttle the healthy ones. pub fn rejected_status(message: &str) -> Option<u16> { let rest = message.strip_prefix("unexpected HTTP status (")?; let digits = rest.strip_suffix(')')?; @@ -684,8 +539,6 @@ pub fn is_client_rejection(message: &str) -> bool { matches!(rejected_status(message), Some(s) if (400..500).contains(&s) && s != 429) } -/// How many individual fetch failures a run logs verbatim before going quiet (the window and -/// throughput lines keep reporting the aggregate). pub const FETCH_ERROR_LOG_LIMIT: u32 = 12; fn record_first_error(slot: &Mutex<Option<String>>, err: String) { @@ -695,11 +548,7 @@ fn record_first_error(slot: &Mutex<Option<String>>, err: String) { } } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// HTTP client (one pooled reqwest client + per-request headers + optional Range + streaming) -// ───────────────────────────────────────────────────────────────────────────────────────────── -/// The pooled client plus the pre-validated per-request headers. struct Http { client: reqwest::Client, headers: Vec<(reqwest::header::HeaderName, reqwest::header::HeaderValue)>, @@ -732,8 +581,6 @@ fn build_http(opts: &FetchOptions, pool_max_idle_per_host: usize) -> Result<Http } impl Http { - /// `GET url` with the run's headers (set per request, so they override the client defaults — - /// a store's own `User-Agent` wins over the Steam one) and the optional `Range`. fn request(&self, url: &str, range: Option<(u64, u64)>) -> reqwest::RequestBuilder { let mut request = self.client.get(url); for (name, value) in &self.headers { @@ -756,7 +603,6 @@ fn classify_reqwest_error(err: &reqwest::Error) -> FetchFailKind { } } -/// Sends the request and checks the status; shared by both modes. async fn send_checked( http: &Http, url: &str, @@ -786,9 +632,6 @@ async fn send_checked( Ok(response) } -/// Whole-body mode: one attempt for one item (the driver owns retry/rotation). `timeout` bounds the -/// whole request (headers + body). The body is read piece by piece so `host_rx` sees every byte as -/// it arrives (the stall detector's signal); `ttfb` is set when the response headers arrive. async fn fetch_once( http: &Http, url: &str, @@ -835,10 +678,6 @@ async fn fetch_once( } } -/// Stream mode: one attempt for one item. Pieces are pushed to the item's pool worker as they -/// arrive (each one added to the in-flight budget until the pool consumes it); the future waits -/// while the budget is full, and stops early on cancel or when the sink rejected this attempt. -/// Returns the total body length after the last piece was queued. #[allow(clippy::too_many_arguments)] async fn fetch_stream( http: &Http, @@ -871,7 +710,6 @@ async fn fetch_stream( let content_length = response.content_length(); let mut offset = 0u64; loop { - // Budget gate: don't read ahead of the pool by more than the budget. while in_flight.load(Ordering::Relaxed) >= budget { if cancel.load(Ordering::Relaxed) { return Err(AsyncFetchError { @@ -931,29 +769,22 @@ async fn fetch_stream( Ok(offset) } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Driver plumbing -// ───────────────────────────────────────────────────────────────────────────────────────────── -/// `abandon` sentinel: no attempt of this item has been rejected by the sink. const NO_ATTEMPT: u32 = u32::MAX; enum ProcessMsg { - /// Whole-body mode: the complete raw body. Body { idx: usize, attempts: u32, server_idx: usize, body: Vec<u8>, }, - /// Stream mode: one in-order piece of attempt `attempts` of item `idx`. Piece { idx: usize, attempts: u32, offset: u64, data: Vec<u8>, }, - /// Stream mode: the body ended; the pool finalizes and reports a verdict. Finish { idx: usize, attempts: u32, @@ -975,7 +806,6 @@ struct Feedback { verdict: SinkVerdict, } -/// An item awaiting (re)dispatch: `attempts` already spent; not eligible before `not_before`. #[derive(Clone, Copy, Debug)] struct PendingItem { idx: usize, @@ -988,18 +818,14 @@ enum Fetched { Streamed(u64), } -/// The result the driver awaits for each dispatched fetch. struct FetchDone { idx: usize, attempts: u32, reserve: u64, server_idx: usize, elapsed: Duration, - /// Request sent → response headers, when the headers arrived. ttfb: Option<Duration>, - /// Key in the driver's registry of running requests. req_id: u64, - /// The request was cancelled because its host stalled (not a failure of the item). stalled: bool, res: Result<Fetched, AsyncFetchError>, } @@ -1061,11 +887,7 @@ async fn run_driver(ctx: DriverCtx<'_>) { let mut retry: VecDeque<PendingItem> = VecDeque::new(); let mut next_item = 0usize; let mut errors_logged: u32 = 0; - // Bodies / finishes handed to the pool whose verdict has not come back yet. let mut outstanding = 0usize; - // Stall detection: bytes received per server (bumped by the request futures as pieces - // arrive), when that count last moved, since when the server has had requests to answer, - // how many requests it is running, and each running request's abort switch. let host_rx: Vec<AtomicU64> = hosts.iter().map(|_| AtomicU64::new(0)).collect(); let mut last_rx: Vec<u64> = vec![0; hosts.len()]; let mut last_rx_at: Vec<Option<Instant>> = vec![None; hosts.len()]; @@ -1075,7 +897,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { let mut aborts: HashMap<u64, (usize, Arc<Notify>)> = HashMap::new(); let mut next_req_id: u64 = 0; let mut last_stall_check = Instant::now(); - // Declared AFTER `txs`/`http` so it is dropped BEFORE them (its futures borrow both). let mut inflight: FuturesUnordered<_> = FuturesUnordered::new(); 'outer: loop { @@ -1085,8 +906,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { break; } - // ── Sink verdicts: Retry = counts as a failed attempt (cool + demote host, back-off, - // rotate); Fatal was already recorded in the error slot by the pool. ── while let Ok(fb) = fb_rx.try_recv() { outstanding = outstanding.saturating_sub(1); match fb.verdict { @@ -1123,7 +942,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { log(&window.summary_line(label, inflight.len(), probe_now)); } - // ── Stalled hosts: requests that receive nothing while another host delivers. ── if probe_now.duration_since(last_stall_check) >= Duration::from_millis(STALL_CHECK_MS) { last_stall_check = probe_now; for s in 0..hosts.len() { @@ -1163,12 +981,10 @@ async fn run_driver(ctx: DriverCtx<'_>) { hosts[s], probe_now.duration_since(silent_since).as_millis() )); - // Do not fire again for the same silence while the cancelled requests wind down. watch_from[s] = probe_now; } } - // ── Dispatch up to the current window ── while inflight.len() < window.current { if cancel.load(Ordering::Relaxed) || error_slot.lock().expect("err slot poisoned").is_some() @@ -1177,7 +993,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { } let now = Instant::now(); - // A due retry first, else the next fresh item. let use_retry = retry.front().is_some_and(|p| p.not_before <= now); let (idx, attempts) = if use_retry { let p = *retry.front().expect("retry nonempty"); @@ -1185,23 +1000,19 @@ async fn run_driver(ctx: DriverCtx<'_>) { } else if next_item < items.len() { (next_item, 0u32) } else { - break; // nothing dispatchable now (retry not due, or all dispatched) + break; }; let item = &items[idx]; - // Byte-budget gate (hard memory bound): reserve at DISPATCH. Always admits when nothing - // is in flight, so an item bigger than the whole budget can't deadlock. Streams reserve - // nothing here — their pieces are budgeted as they arrive. let reserve = if stream { 0 } else { reserve_bytes(item) }; if !budget_admits(in_flight.load(Ordering::Relaxed), reserve, budget) { window.note_budget_stall(); - break; // wait for a completion to free budget + break; } - // Speed-ranked, per-host-capped host pick. let Some(server_idx) = sched.pick(now) else { window.note_host_stall(); - break; // every host at cap or cooling → wait for a completion + break; }; let permit = match sched.host_sem(server_idx).clone().try_acquire_owned() { Ok(permit) => permit, @@ -1218,7 +1029,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { let url = url.to_string(); let range = item.range; - // Commit: pop the work item, reserve budget, launch the fetch future. if use_retry { retry.pop_front(); } else { @@ -1231,7 +1041,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { let rx_counter = &host_rx[server_idx]; let started = Instant::now(); if running[server_idx] == 0 { - // The host had nothing to answer until now: its silence is measured from here. watch_from[server_idx] = started; } running[server_idx] += 1; @@ -1268,7 +1077,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { .map(Fetched::Body) } }; - // The stall detector's switch: dropping the fetch future closes its connection. match select(pin!(fetch), pin!(abort.notified())).await { Either::Left((res, _)) => (res, false), Either::Right(((), _)) => ( @@ -1280,7 +1088,7 @@ async fn run_driver(ctx: DriverCtx<'_>) { ), } }; - drop(permit); // RAII per-host permit release (also on future drop / cancel-abort) + drop(permit); FetchDone { idx, attempts: attempts + 1, @@ -1295,17 +1103,14 @@ async fn run_driver(ctx: DriverCtx<'_>) { }); } - // ── Nothing in flight? finished, or waiting on retry-not-due / host cooldown / the pool. ── if inflight.is_empty() { if next_item >= items.len() && retry.is_empty() && outstanding == 0 { - break; // every item fetched and processed + break; } tokio::time::sleep(Duration::from_millis(5)).await; continue; } - // ── Await one completion (drives ALL in-flight requests cooperatively), bounded so - // cancel / a pool Fatal / a Retry verdict are noticed promptly even mid-fetch. ── let done = match tokio::time::timeout(Duration::from_millis(DRIVER_POLL_MS), inflight.next()) .await { @@ -1317,9 +1122,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { aborts.remove(&done.req_id); running[done.server_idx] = running[done.server_idx].saturating_sub(1); if done.stalled { - // Not the item's fault: straight back into the queue for a healthy host, no window - // or host penalty beyond the rest the detector already gave the host. It still counts - // as an attempt, which keeps each stream attempt's pieces distinguishable. in_flight.fetch_sub(done.reserve, Ordering::Relaxed); retry.push_front(PendingItem { idx: done.idx, @@ -1340,8 +1142,6 @@ async fn run_driver(ctx: DriverCtx<'_>) { meter.record(done.server_idx, raw_len); sched.on_success(done.server_idx, raw_len, done.elapsed); window.record_ok(done.elapsed.as_secs_f64() * 1000.0); - // Reconcile the dispatch reservation to the actual raw size; the pool subtracts the - // actual size on process-complete, netting this item's budget to zero. if raw_len >= done.reserve { in_flight.fetch_add(raw_len - done.reserve, Ordering::Relaxed); } else { @@ -1357,7 +1157,7 @@ async fn run_driver(ctx: DriverCtx<'_>) { }) .is_err() { - break; // process side gone + break; } } Ok(Fetched::Streamed(total_len)) => { @@ -1374,7 +1174,7 @@ async fn run_driver(ctx: DriverCtx<'_>) { }) .is_err() { - break; // process side gone + break; } } Err(err) => { @@ -1423,14 +1223,10 @@ window_shrink={} msg={}", } } - // Dropping `inflight` aborts any still-running requests (the cancel / error / final-failure - // teardown path); dropping the senders then closes the channels so the pool drains and exits. drop(inflight); drop(txs); } -/// Since when a server has been silent: its last byte, or when it was last given work to do if -/// that came later. fn silent_since(last_rx_at: Option<Instant>, watch_from: Instant) -> Instant { match last_rx_at { Some(at) if at > watch_from => at, @@ -1438,16 +1234,12 @@ fn silent_since(last_rx_at: Option<Instant>, watch_from: Instant) -> Instant { } } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Bandwidth meter (per-host bytes + 5-s samples for peak) -// ───────────────────────────────────────────────────────────────────────────────────────────── pub struct BandwidthMeter { start: Instant, total_bytes: AtomicU64, per_server_bytes: Vec<AtomicU64>, hosts: Vec<String>, - /// (bytes, instant) at the last sample; peak = max sample rate in bytes/s. last_sample: Mutex<(u64, Instant)>, peak_bps: Mutex<f64>, } @@ -1478,8 +1270,6 @@ impl BandwidthMeter { self.total_bytes.load(Ordering::Relaxed) } - /// Take a throughput sample since the previous one (≥ 1 s apart to be counted) and fold it - /// into the peak. pub fn sample(&self) { let now = Instant::now(); let total = self.total_bytes(); @@ -1530,9 +1320,6 @@ impl BandwidthMeter { } } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Scheduler + adaptive window — a copy of the Steam depot writer's (keyed on host strings). -// ───────────────────────────────────────────────────────────────────────────────────────────── #[derive(Clone, Debug, Default)] struct ServerHealth { @@ -1542,10 +1329,6 @@ struct ServerHealth { cooldown_until: Option<Instant>, } -/// Speed-ranked, per-host-capped host picker. Cold hosts are probed first, then selection is -/// epsilon-greedy over the measured EWMA so a demoted-but-recovered host returns. Per-host caps key -/// on the host STRING (one `tokio::sync::Semaphore` each) so two entries that share a host still -/// share one cap. struct FetchScheduler { health: Vec<ServerHealth>, server_host: Vec<usize>, @@ -1635,8 +1418,6 @@ impl FetchScheduler { h.cooldown_until = None; } - /// A stalled host: out of the rotation for `rest`, and its speed estimate cut so it comes - /// back behind the hosts that kept delivering. fn rest(&mut self, server_idx: usize, now: Instant, rest: Duration) { let h = &mut self.health[server_idx]; h.cooldown_until = Some(now + rest); @@ -1710,8 +1491,6 @@ impl WindowReason { } } -/// Adaptive in-flight window: slow-start doubles every healthy probe until the first real back-off, -/// then grows additively; holds on plateau; shrinks proportionally only on real errors. #[derive(Clone, Debug)] struct AdaptiveWindow { current: usize, @@ -1733,14 +1512,10 @@ struct AdaptiveWindow { last_budget_stalls: u32, last_host_stalls: u32, latency_ewma_ms: f64, - /// TTFB samples of the current probe interval. ttfb_sum_ms: f64, ttfb_samples: u32, - /// Lowest per-probe mean TTFB seen this run (0 = none yet): the no-queue baseline. ttfb_base_ms: f64, - /// Mean TTFB of the last probe that had enough samples. ttfb_last_ms: f64, - /// Consecutive probes with queueing delay. latency_streak: u32, stalled_hosts: u32, cooldown_until: Option<Instant>, @@ -1803,7 +1578,6 @@ impl AdaptiveWindow { self.stalled_hosts = self.stalled_hosts.saturating_add(1); } - /// The latency brake's shrink: proportional and gentle, never below the floor, at least −1. fn shrink_latency(&mut self, now: Instant) { let scaled = (self.current as f64 * LATENCY_SHRINK_FACTOR).floor() as usize; self.current = scaled.min(self.current.saturating_sub(1)).max(self.min); @@ -1884,7 +1658,6 @@ impl AdaptiveWindow { let before = self.current; let cooling = self.cooldown_until.is_some_and(|t| now < t); - // Queueing delay: this probe's mean TTFB against the run's baseline. let probe_ttfb = (self.ttfb_samples >= LATENCY_MIN_SAMPLES) .then(|| self.ttfb_sum_ms / self.ttfb_samples as f64); let mut queueing = false; @@ -1909,9 +1682,6 @@ impl AdaptiveWindow { } else if cooling { self.last_reason = WindowReason::HoldCooldown; } else if queueing { - // More requests only made each one wait longer. Hold while a real gain still comes - // with the delay; once the delay keeps rising without one, give some of the window - // back, gently, and let the cooldown show whether that helped. let gain = self.bps_ewma > self.best_bps * (1.0 + LATENCY_GAIN_EPS); if gain { self.best_bps = self.bps_ewma; @@ -2012,9 +1782,6 @@ stalled_hosts={}", } } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// Tests -// ───────────────────────────────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { @@ -2154,8 +1921,6 @@ mod tests { const MIB: u64 = 1024 * 1024; - /// Drive the window through probes 2.1 s apart, each with `samples` TTFB values and the - /// bytes that arrived in it; returns (reason, window) after every probe. fn run_probes(w: &mut AdaptiveWindow, t0: Instant, probes: &[(f64, u64)]) -> Vec<(WindowReason, usize)> { let mut total = 0u64; let mut out = Vec::new(); @@ -2174,7 +1939,6 @@ mod tests { fn rising_delay_without_more_throughput_holds_then_shrinks_gently() { let t0 = Instant::now(); let mut w = AdaptiveWindow::new(8, 2, 256, t0); - // Flat ~15 MB/s while the time to first byte climbs from 0.4 s to 12 s (the 5G log). let flat = 32 * MIB; let probes: Vec<(f64, u64)> = [400.0, 400.0, 450.0, 900.0, 1500.0, 2500.0, 4000.0, 6000.0, 9000.0, 12000.0] .iter() @@ -2186,7 +1950,6 @@ mod tests { assert!(first_hold < first_shrink, "{seen:?}"); let before = seen[first_shrink - 1].1; assert_eq!(seen[first_shrink].1, (before as f64 * LATENCY_SHRINK_FACTOR).floor() as usize, "{seen:?}"); - // From the first sign of queueing on, the window never grows again. for pair in seen[first_hold..].windows(2) { assert!(pair[1].1 <= pair[0].1, "grew under queueing delay: {seen:?}"); } @@ -2199,7 +1962,6 @@ mod tests { fn a_fast_link_keeps_growing_while_its_delay_stays_low() { let t0 = Instant::now(); let mut w = AdaptiveWindow::new(8, 2, 256, t0); - // Throughput doubles with the window; TTFB wobbles between 25 and 70 ms (new connections). let probes: Vec<(f64, u64)> = (0..6) .map(|k| ([25.0, 70.0, 40.0, 60.0, 30.0, 65.0][k], (8 * MIB) << k)) .collect(); @@ -2211,8 +1973,6 @@ mod tests { assert!(w.current >= 128, "{seen:?}"); } - /// Two-behaviour stub for the stall test: `freeze` answers the headers and 100 bytes, then - /// never sends another byte; otherwise every path gets 10 000 bytes in ten paced slices. fn start_paced_stub(freeze: bool) -> String { let listener = TcpListener::bind("127.0.0.1:0").expect("bind"); let port = listener.local_addr().expect("addr").port(); @@ -2296,12 +2056,10 @@ mod tests { .unwrap_or_else(|| panic!("no host-stall line: {lines:?}")); assert!(stall.contains(&format!(" host={frozen} ")), "{stall}"); assert!(!stall.contains(" requeued=0 "), "{stall}"); - // Well inside the 10 s request timeout that would otherwise have freed the items. assert!(elapsed < Duration::from_secs(8), "took {elapsed:?}: {lines:?}"); assert!(!lines.iter().any(|l| l.contains(&format!("host={healthy}")) && l.starts_with("host-stall"))); } - // ── Minimal HTTP/1.1 stub: one thread per connection, `Connection: close`, Range-aware. ── struct Stub { base: String, hits: Arc<Mutex<HashMap<String, usize>>>, @@ -2444,11 +2202,9 @@ mod tests { } } - /// Stream-mode sink: appends pieces per item (offset 0 restarts), finalizes by length check. struct StreamSink { got: Mutex<HashMap<u64, Vec<u8>>>, finished: Mutex<HashMap<u64, u64>>, - /// Reject the first piece of this item once (exercises the mid-stream Retry path). retry_once_id: Option<u64>, retried: AtomicUsize, } @@ -2534,8 +2290,6 @@ mod tests { assert_eq!(got[&5], b"nile/0.1 Amazon".to_vec()); let finished = sink.finished.lock().unwrap(); assert_eq!(finished.len(), 5); - // Pieces credited as written + 7 extra per on_finish; the rejected attempt of /big - // contributed nothing (its first piece was refused before any write). let written: u64 = got.values().map(|b| b.len() as u64).sum(); assert_eq!(outcome.bytes_credited, written + 5 * 7); let hits = stub.hits.lock().unwrap(); @@ -2575,7 +2329,6 @@ mod tests { &sink, &cancel, &|bytes: u64, ok: u64| { - // Pool threads race on the callback order; keep the high-water mark. let mut cur = last_progress.lock().unwrap(); if bytes >= cur.0 { *cur = (bytes, ok); @@ -2595,7 +2348,6 @@ mod tests { let expected_credit: u64 = got.values().map(|b| b.len() as u64 * 2).sum(); assert_eq!(outcome.bytes_credited, expected_credit); assert_eq!(*last_progress.lock().unwrap(), (expected_credit, 4)); - // /c was served 503 once, then 200; /a was re-fetched once after the sink's Retry. let hits = stub.hits.lock().unwrap(); assert_eq!(hits["/c"], 2); assert_eq!(hits["/a"], 2); diff --git a/app/src/main/rust/stores/src/fetch_tuning.rs b/app/src/main/rust/stores/src/fetch_tuning.rs index abbe2d492..4f1f7074f 100644 --- a/app/src/main/rust/stores/src/fetch_tuning.rs +++ b/app/src/main/rust/stores/src/fetch_tuning.rs @@ -1,32 +1,12 @@ -//! The numbers and the two small types [`fetch_core`](crate::fetch_core) shares with the Steam -//! depot writer it was carved out of. -//! -//! In Bannerlator these live in `depot_writer.rs` and `cdn_client.rs`, next to the Steam CM and -//! depot code, so that the Steam engine and the store engines run on ONE set of tunables and their -//! `fetch-window` log lines stay A/B-comparable. DroidDeck ships no Steam depot engine (the Steam -//! client runs natively in the Linux runtime), so only this slice came across: the values are -//! copied verbatim and keep their names, which is what lets a GOG/Epic/Amazon log from either app -//! be read against the same tuning notes. -//! -//! The important behaviours are structural (slow-start ramp -> hold at plateau -> back off on REAL -//! errors, per-host cap, hard byte budget), not the exact numbers. The r3/r4 notes below record -//! why each number is what it is, because every one of them was arrived at by a device run that -//! went wrong the other way. use std::time::Duration; -/// Default `User-Agent` of the pooled client. Every store engine overrides it per request with -/// the agent its own launcher sends (`FetchOptions::headers`), so this value is only what a -/// request without an explicit header would carry. pub const USER_AGENT: &str = "Valve/Steam HTTP Client 1.0"; -/// TCP connect deadline for every request the pooled client makes. pub fn connect_timeout() -> Duration { Duration::from_secs(15) } -/// Classified fetch failure so the fetch driver can back off proportionally (429/5xx/timeout/reset -/// shrink the window + cool the host harder than a one-off protocol error). #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum FetchFailKind { Timeout, @@ -42,53 +22,17 @@ pub struct AsyncFetchError { pub kind: FetchFailKind, } -/// Attempts per item before the run gives up on it (fetch failures and sink retries both count). pub const MAX_CHUNK_ATTEMPTS: u32 = 5; -/// Floor for the in-flight byte budget — the pre-r4 fixed value, so no run ever gets *less* -/// buffered memory than it had before the budget started scaling. See [`inflight_budget_bytes`]. pub const FETCH_INFLIGHT_BUDGET_FLOOR_BYTES: u64 = 24 * 1024 * 1024; -/// Hard ceiling for the in-flight byte budget. This is THE OOM guard: whatever the tier, the host -/// count or the chunk size, the RAW bytes in memory for one run can never exceed this. 96 MiB = 4× -/// the old fixed budget and still a small fraction of the heap an Android game installer runs in, -/// and it is only ever reached by a fast-tier run whose CDN pool is large enough to support a 64+ -/// window. Raising this is the one change in the fetch layer that can reintroduce OOM, so it stays -/// fixed and small. pub const FETCH_INFLIGHT_BUDGET_HARD_CAP_BYTES: u64 = 96 * 1024 * 1024; -/// Assumed RAW (on-the-wire, still-compressed) size of one chunk when sizing the budget. Store -/// chunks are ~1 MiB; this is the same nominal [`NOMINAL_CHUNK_RESERVE_BYTES`] uses at dispatch. pub const TYPICAL_CHUNK_BYTES: u64 = NOMINAL_CHUNK_RESERVE_BYTES; -/// Headroom the budget carries over "one chunk per window slot" (3/2 = 1.5×), as a rational so the -/// arithmetic stays integer. The window is a count of concurrent *requests*; a slot's bytes are held -/// from DISPATCH until its WRITE completes, so at any instant some slots hold a fetched-but-not-yet- -/// written chunk while others hold a reservation for a fetch still on the wire. 1.5× covers that -/// overlap (and the occasional above-nominal chunk) without doubling the memory bound. pub const BUDGET_HEADROOM_NUM: u64 = 3; pub const BUDGET_HEADROOM_DEN: u64 = 2; -/// Byte budget for the *compressed* bytes buffered in the fetch→process channel, scaled to the -/// window ceiling this run can actually reach. -/// -/// The decouple channel is bounded by BYTES, not chunk count: a count cap makes in-flight memory -/// swing wildly with chunk size (a 4-chunk cap is ~4 MB for 1 MB chunks but ~120 MB for 30 MB -/// chunks). Budgeting bytes keeps the heap bounded regardless of game size while giving the fetch -/// pool bandwidth-delay-product headroom so the socket stays busy while process workers -/// decompress+write. -/// -/// r4: a device run proved a fixed 24 MiB budget becomes the binding constraint the moment the -/// adaptive window works — on a 1 Gbps line the window grew to 42 but the budget admitted only ~24 -/// chunks, so `in_flight` stalled at 23–28 with `budget_stalls` in the hundreds and -/// `host_stalls=0`. A budget that refuses slots the window has already decided are safe is not a -/// memory guard, it is a throttle. So the budget scales WITH the effective ceiling (tier ∧ -/// hosts×per-host-cap ∧ hard cap) and is clamped between the old fixed value and a hard cap. Low -/// tiers are unaffected; a fast link gets a budget that stops binding before the window does. -/// -/// Budget only the RAW bytes actually queued (reserved at DISPATCH, released on WRITE-COMPLETE); the -/// decoded expansion is transient inside a process worker and never accumulates. See -/// [`budget_admits`] for the single-chunk deadlock guard. pub fn inflight_budget_bytes(effective_ceiling: usize) -> u64 { (effective_ceiling as u64) .saturating_mul(TYPICAL_CHUNK_BYTES) @@ -100,71 +44,30 @@ pub fn inflight_budget_bytes(effective_ceiling: usize) -> u64 { ) } -// ─── Adaptive-window tuning ──────────────────────────────────────────────────────────────────── -// r3 retune: the first device run pinned the window at the floor on a 1 Gbps link. The controller -// shrank on throughput dips and latency jitter (normal CDN noise) as well as on errors, shrank a flat -// −6 on every single error, and only grew when throughput rose >10% in a 700 ms sample. The shrinks -// dominated, so the window ratcheted to the floor and stayed there. r3 makes growth eager and makes -// ERRORS the only shrink signal. -/// Max concurrent requests to any single CDN host. CDNs throttle/reset per host, so a big window -/// is reached by spreading across MORE hosts, never by piling onto one. pub const PER_HOST_CAP: usize = 6; -/// Initial in-flight window. Slow-start doubles from here, so this only sets how fast the first -/// couple of probes get useful — the ceiling still comes from the tier × hosts clamp. pub const BOOTSTRAP_WINDOW: usize = 8; -/// Hard safety ceiling on the window regardless of tier (also clamped to distinct-hosts × per-host). pub const WINDOW_HARD_CAP: usize = 256; -/// The window never shrinks below this (keeps a little pipelining even on a rough link). pub const WINDOW_MIN_FLOOR: usize = 2; -/// Additive growth step once slow-start has ended (after the first real back-off). pub const WINDOW_STEP_UP: usize = 4; -/// Slow-start multiplier: until the first back-off the window DOUBLES each healthy probe, so a fast -/// link reaches a useful window in seconds instead of never. pub const WINDOW_SLOW_START_FACTOR: usize = 2; -/// Proportional shrink on a real error signal (never a flat step: a flat −6 slammed a small window -/// straight to the floor). Always at least −1 while above the floor. pub const WINDOW_SHRINK_FACTOR: f64 = 0.75; -/// How often the adaptive window re-evaluates. Long enough that one jittery sample cannot drive a -/// decision; decisions use the EWMA, not the raw sample. pub const WINDOW_PROBE_INTERVAL_MS: u64 = 2_000; -/// After a back-off, hold the window (no growth) for this long — hysteresis against oscillation. pub const WINDOW_COOLDOWN_MS: u64 = 3_000; -/// EWMA smoothing for the measured throughput samples (higher = more responsive). pub const WINDOW_BPS_EWMA_ALPHA: f64 = 0.4; -/// Throughput above `best × (1 + this)` counts as a genuine improvement (keeps slow-start climbing). pub const WINDOW_IMPROVE_EPS: f64 = 0.05; -/// Throughput below `best × (1 − this)` counts as falling — growth STOPS (hold). It never shrinks: -/// only real errors shrink. pub const WINDOW_DECLINE_EPS: f64 = 0.15; -/// Error-rate thresholds over one probe window: above HIGH shrinks; growth needs at/below LOW -/// (in between = hold). pub const WINDOW_ERR_RATE_HIGH: f64 = 0.10; pub const WINDOW_ERR_RATE_LOW: f64 = 0.05; -/// Errors inside ONE probe interval that trigger an immediate (don't-wait-for-the-probe) shrink. A -/// single stray timeout no longer moves the window; a burst does. A 429 always shrinks immediately. pub const WINDOW_ERR_BURST_IMMEDIATE: u32 = 3; -/// Consecutive non-improving probes still allowed to grow before declaring a plateau — throughput -/// lags a window change by a probe or two, so one flat sample must not stop the ramp. pub const WINDOW_PLATEAU_PATIENCE: u32 = 2; -/// Once plateaued, re-arm the ramp this long later (doubling, capped) to re-probe for headroom if the -/// link improved. Bounded re-probing, not a creep: a weak link answers with errors and shrinks back. pub const WINDOW_PLATEAU_REARM_MS: u64 = 30_000; pub const WINDOW_PLATEAU_REARM_MAX_MS: u64 = 300_000; -/// Even when the window does not change, emit one `fetch-window` line every N probes so a stuck -/// window explains itself in the log (`reason=` says why it is not moving). pub const WINDOW_LOG_EVERY_PROBES: u32 = 5; -/// Every Nth exploit dispatch is instead an exploration pick (epsilon-greedy ≈ 1/N) so a -/// demoted-but-recovered host is retried rather than starved forever. pub const SERVER_EXPLORE_EVERY: u64 = 8; -/// Reservation for an item whose compressed size the manifest didn't carry (keeps the byte budget -/// honest at dispatch time). pub const NOMINAL_CHUNK_RESERVE_BYTES: u64 = 1024 * 1024; -/// Extra host cooldown floor when a host answers 429 (rate-limited) — back off harder than a -/// one-off timeout. pub const RATE_LIMIT_COOLDOWN_MS: u64 = 5_000; -/// Exponential back-off between attempts of one item: 0, 300, 600, 1200, 2400 ms … capped at 4 s. pub fn retry_backoff_millis(attempt: u32) -> u64 { if attempt == 0 { return 0; @@ -172,9 +75,6 @@ pub fn retry_backoff_millis(attempt: u32) -> u64 { (300u64 << (attempt - 1)).min(4000) } -/// Byte-budget admission test for the fetch→process channel. Always admits when nothing is in -/// flight (`in_flight == 0`) so a single item LARGER than the whole budget can never deadlock — it -/// simply waits until the channel drains, then goes through alone. #[inline] pub fn budget_admits(in_flight: u64, raw_len: u64, budget: u64) -> bool { in_flight == 0 || in_flight.saturating_add(raw_len) <= budget diff --git a/app/src/main/rust/stores/src/gog.rs b/app/src/main/rust/stores/src/gog.rs index 465d7346f..3aed66074 100644 --- a/app/src/main/rust/stores/src/gog.rs +++ b/app/src/main/rust/stores/src/gog.rs @@ -1,21 +1,3 @@ -//! GOG gen2 chunk download engine — the Rust replacement for the byte-fetching inner loop of -//! `GogDownloadManager.java` (`assembleDepotFile` / `fetchChunkVerified` driven from the -//! per-file thread pool). Everything around that loop — token/builds/manifest head, base-product -//! + language depot selection, secure-link resolution, disk guard, `_gog_manifest.json` marker, -//! prefs, exe resolution, DLC markers, redist orchestration — stays in Java and is untouched. -//! -//! Parity contract: Bannerlator's `docs/RUST_GOG_PARITY.md`. The adapter is fed the SAME inflated depot-manifest -//! JSON strings Java already parsed, the SAME resolved CDN base (secure link) and the SAME -//! install dir, and produces the SAME on-disk layout (`<file>.bhtmp` staging, atomic rename, -//! size+MD5-verified finals) so a download can be resumed by either engine. -//! -//! Layout: -//! - [`plan`] — depot-manifest JSON → file/chunk plan (mirrors `parseDepotManifest`), CDN path -//! and chunk-URL builders, largest-first ordering. -//! - [`engine`] — resume/skip pass (mirrors `fileVerified`), `FetchItem` list, the `FetchSink` -//! (compressed size/MD5 → inflate → decompressed size/MD5 → positioned write → -//! whole-file size/MD5 → rename), summary logging. -//! - [`jni`] — `GogNative` JNI exports (`nativeStart` / `nativeCancel` / `nativeRelease`). pub mod engine; pub mod jni; @@ -25,7 +7,6 @@ use std::fs::File; use std::io::Read; use std::path::Path; -/// Lowercase hex of `bytes` (matches `GogDownloadManager.toHex`). pub fn to_hex(bytes: &[u8]) -> String { const DIGITS: &[u8; 16] = b"0123456789abcdef"; let mut out = String::with_capacity(bytes.len() * 2); @@ -36,15 +17,12 @@ pub fn to_hex(bytes: &[u8]) -> String { out } -/// Lowercase hex MD5 of an in-memory buffer (`GogDownloadManager.md5Hex`). pub fn md5_hex(data: &[u8]) -> String { let mut hasher = crate::md5_small::Md5::new(); hasher.update(data); to_hex(&hasher.finalize()) } -/// Streaming lowercase hex MD5 of a file, never buffering the whole file -/// (`GogDownloadManager.md5HexFile`). `None` when the file cannot be read. pub fn md5_hex_file(path: &Path) -> Option<String> { let mut file = File::open(path).ok()?; let mut hasher = crate::md5_small::Md5::new(); @@ -59,11 +37,6 @@ pub fn md5_hex_file(path: &Path) -> Option<String> { Some(to_hex(&hasher.finalize())) } -/// zlib-inflate a chunk body (`GogDownloadManager.inflateZlib`): `None` when the body is not a -/// zlib stream (first byte != 0x78) or fails to inflate — the caller then treats the body as a -/// stored (non-compressed) chunk, exactly like Java's `if (inflated == null) inflated = raw`. -/// A truncated stream yields the partial output (Java's loop breaks on `n == 0` the same way); -/// the decompressed-size check that follows rejects it. pub fn inflate_zlib(data: &[u8]) -> Option<Vec<u8>> { if data.len() < 2 || data[0] != 0x78 { return None; @@ -96,16 +69,12 @@ mod tests { let compressed = enc.finish().unwrap(); assert_eq!(compressed[0], 0x78); assert_eq!(inflate_zlib(&compressed).unwrap(), payload); - // Not zlib → None (stored chunk path). assert!(inflate_zlib(b"\x1f\x8bnot zlib").is_none()); assert!(inflate_zlib(b"\x78").is_none()); - // Truncated zlib → partial output, like Java's `while (!inf.finished()) { if (n == 0) break; }` - // loop; the decompressed-size check downstream rejects it either way. let mut truncated = compressed.clone(); let mid = truncated.len() / 2; truncated.truncate(mid); assert!(inflate_zlib(&truncated).map(|out| out != payload).unwrap_or(true)); - // Corrupt zlib body → None (Java: DataFormatException → null → raw fallback). let mut corrupt = compressed.clone(); for b in corrupt.iter_mut().skip(2) { *b = !*b; diff --git a/app/src/main/rust/stores/src/gog/engine.rs b/app/src/main/rust/stores/src/gog/engine.rs index 0233110c3..9f2f59428 100644 --- a/app/src/main/rust/stores/src/gog/engine.rs +++ b/app/src/main/rust/stores/src/gog/engine.rs @@ -1,17 +1,3 @@ -//! The GOG gen2 fetch loop on top of the shared fetch core. One call = one Java pool run -//! (`runGen2` base loop, `doInstallDlc` loop, or one `assembleDependencyInstaller`). -//! -//! What this mirrors, and where (see Bannerlator's `docs/RUST_GOG_PARITY.md`): -//! - resume/skip: [`file_verified`] = `GogDownloadManager.fileVerified` (exists, non-empty, size -//! when known, MD5 when known) — a verified file is reported as "Verified…" progress with NO -//! bytes credited, exactly like the Java task; -//! - per chunk: compressed size → compressed MD5 → inflate (stored fallback) → decompressed size → -//! decompressed MD5 (`fetchChunkVerified`); a mismatch is a retryable failure (Java: hard fail -//! ≤3 with backoff; the core: ≤5 attempts with backoff); -//! - per file: write into `<file>.bhtmp` (positioned, so chunks may land out of order), then -//! whole-file size + MD5, delete any existing final, rename (`assembleDepotFile`); -//! - failure of any file aborts the run (`anyFailed`), cancel aborts the run; in both cases every -//! unfinished `.bhtmp` is deleted (Java deletes its tmp on the failing/cancelled thread). use std::collections::HashSet; use std::fs::{self, File, OpenOptions}; @@ -30,35 +16,25 @@ use super::plan::{ }; use super::{inflate_zlib, md5_hex, md5_hex_file}; -/// Per-request timeout on chunk fetches (Java: `TIMEOUT = 30_000` connect + read). pub const CHUNK_TIMEOUT: Duration = Duration::from_secs(30); -/// Java sends this on every content-system / CDN request (`fetchBytesEx`). pub const USER_AGENT: &str = "GOG Galaxy"; -/// Java staging suffix (`assembleDepotFile`). pub const TMP_SUFFIX: &str = ".bhtmp"; -/// Floor for the per-host cap (the core's default); a single-host store gets the whole ceiling. pub const PER_HOST_CAP_FLOOR: usize = 6; -/// `max(6, ceil(max_workers / distinct_hosts))` — one CDN host gets the whole ceiling, several -/// share it. pub fn per_host_cap_for(max_workers: usize, distinct_hosts: usize) -> usize { let hosts = distinct_hosts.max(1); let workers = max_workers.max(1); PER_HOST_CAP_FLOOR.max(workers.div_ceil(hosts)) } -/// Which Java loop this run replaces. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum PlanKind { - /// gen2: depot manifests → chunks → `assembleDepotFile` (base install, DLC, dependencies). #[default] Gen2Chunks, - /// gen1: build manifest → per-file HTTP Range GET streamed to disk (`runGen1`). Gen1Ranges, } impl PlanKind { - /// JNI mapping (`GogNative.KIND_*`): 0 = gen2 chunks, 1 = gen1 ranges, anything else → gen2. pub fn from_i32(v: i32) -> Self { if v == 1 { PlanKind::Gen1Ranges @@ -68,39 +44,21 @@ impl PlanKind { } } -/// Inputs — exactly what the Java manager has in hand after manifest + secure-link resolution. #[derive(Clone, Debug, Default)] pub struct GogRequest { pub kind: PlanKind, - /// gen2: inflated depot-manifest JSON strings, in the order Java fetched them (already filtered - /// by base-product / DLC-product and language in Java). gen1: the inflated build manifest. pub depot_manifests: Vec<String>, - /// gen2: the resolved CDN bases from the secure-link answer (every `urls[]` entry, query - /// strings kept, in the order GOG lists them), or the one unauthenticated dependency store - /// base. Several bases on distinct hosts let the fetch core spread the window across CDNs - /// with its per-host cap; a base on a host already listed is dropped (the first one wins). - /// gen1: unused (file URLs live in the manifest). pub cdn_bases: Vec<String>, pub install_dir: String, - /// Files already completed by an earlier run of this same download (secure-link refresh - /// re-run): counted as done WITHOUT re-hashing and WITHOUT a progress event. pub skip_paths: Vec<String>, pub ca_bundle_path: String, - /// Java pool size: `resolveDownloadThreads` (base), 8 (DLC), 1 (dependency). pub max_workers: usize, - /// Inflate/hash/write threads; Java did that on the same pool threads, so pass the same count. pub process_workers: usize, - /// Base install: largest-first (LPT) ordering; DLC / dependency: manifest order. pub sort_largest_first: bool, - /// Log prefix, e.g. `gog base=1207658930`. pub label: String, } -/// Callbacks out of the engine (JNI listener or a test recorder). Must be callable from the -/// process-pool threads. pub trait GogEvents: Sync { - /// One file reached its final state. `verified` = resume-skip (no bytes credited); otherwise a - /// freshly assembled + verified file (`file_bytes` credited). `files_done` counts both. #[allow(clippy::too_many_arguments)] fn on_file_done( &self, @@ -113,9 +71,6 @@ pub trait GogEvents: Sync { bytes_total: u64, ); fn on_log(&self, line: &str); - /// Periodic byte progress while files are in flight (~4/s, only when the value moved): - /// assembled bytes plus the bytes of files not yet finished. Same `bytes_total` as - /// `on_file_done`. Default: ignored (test recorders). fn on_bytes(&self, _bytes_done: u64, _bytes_total: u64) {} } @@ -123,20 +78,14 @@ pub trait GogEvents: Sync { pub struct GogRunResult { pub success: bool, pub cancelled: bool, - /// The run died on an HTTP code Java treats as an expired secure link (401/403/404/500) — - /// the Java caller refreshes the link (cap 5) and re-runs with `skip_paths` filled in. pub link_expiry: bool, pub error: String, - /// Decompressed bytes of files assembled by THIS run (Java `totalBytes`). pub bytes_written: u64, - /// Files done = verified-skipped + assembled (Java `doneCount`), excluding `skip_paths`. pub files_done: u32, pub files_verified: u32, pub files_total: u32, } -/// `GogDownloadManager.fileVerified`: true only if the file exists, is non-empty, matches the -/// expected size when known and the expected MD5 when known. pub fn file_verified(path: &Path, expected_size: u64, expected_md5: &str) -> bool { let Ok(meta) = fs::metadata(path) else { return false; @@ -156,8 +105,6 @@ pub fn file_verified(path: &Path, expected_size: u64, expected_md5: &str) -> boo true } -/// Finds an HTTP status in a fetch-core error string (`non-200 HTTP status (403)`, `status 403`, -/// `HTTP 403`, `http=403`, `code=403`). The link-expiry set is Java's `fetchChunkVerified` list. pub fn http_status_in(message: &str) -> Option<u16> { let lower = message.to_ascii_lowercase(); for marker in ["status (", "status ", "http ", "http=", "code=", "code "] { @@ -178,7 +125,6 @@ pub fn http_status_in(message: &str) -> Option<u16> { None } -/// Java: `code == 401 || code == 403 || code == 404 || code == 500` → refresh the secure link. pub fn is_link_expiry_status(code: u16) -> bool { matches!(code, 401 | 403 | 404 | 500) } @@ -186,8 +132,6 @@ pub fn is_link_expiry_status(code: u16) -> bool { struct FileState { handle: Option<Arc<File>>, chunks_done: usize, - /// Decompressed bytes of this file's finished chunks, credited to the sink's `partial` - /// counter until the file itself is finalized and moves into `bytes_done`. partial: u64, finalized: bool, failed: bool, @@ -240,11 +184,6 @@ impl SpeedSampler { } } -/// Positioned writer for ONE chunk's decompressed byte range inside the file's `.bhtmp`: hashes -/// everything it is given (the decompressed MD5) and counts it, but only writes bytes inside the -/// chunk's expected range when the size is known — an inflate overshoot then fails the size -/// check instead of clobbering the neighbouring chunk. An I/O error is remembered so the sink can -/// tell "disk failed" (fatal) from "stream failed" (retry). struct RangeWriter { file: Arc<File>, base: u64, @@ -294,13 +233,10 @@ enum ChunkMode { Stored(RangeWriter), } -/// Per-item streaming state for one attempt (reset whenever a piece arrives at `offset == 0`). struct ChunkStream { comp_md5: crate::md5_small::Md5, comp_len: u64, - /// Decided on the first non-empty piece (`0x78` → zlib, else stored — Java's `inflateZlib`). mode: Option<ChunkMode>, - /// The writer until the mode is decided (an empty body never decides). writer: Option<RangeWriter>, } @@ -315,17 +251,10 @@ impl ChunkStream { } } -/// The `FetchSink`. Stream mode (what `run_gen2` uses): pieces of a chunk are inflated as they -/// arrive straight into the chunk's byte range of the file, the compressed MD5 and the -/// decompressed size/MD5 accumulate on the fly, and `on_finish` runs the checks in Java's order -/// (compressed size → compressed MD5 → inflate → size → MD5). Body mode (`process`) is the same -/// pipeline on a whole buffer and stays available for tests / a body-mode fallback. struct GogSink<'a> { files: &'a [PlannedFile], - /// item id → (file index, chunk index) table: Vec<(usize, usize)>, states: Vec<Mutex<FileState>>, - /// item id → in-flight stream attempt (stream mode only) streams: Vec<Mutex<Option<ChunkStream>>>, files_total: u32, bytes_total: u64, @@ -333,8 +262,6 @@ struct GogSink<'a> { bytes_done: &'a AtomicU64, events: &'a dyn GogEvents, speed: Mutex<SpeedSampler>, - /// Bytes of files still in flight (finished chunks; for gen1, the current attempt's pieces). - /// `bytes_done + partial` is what the byte ticker reports between file completions. partial: AtomicU64, } @@ -343,8 +270,6 @@ impl<'a> GogSink<'a> { self.events.on_log(line); } - /// `assembleDepotFile` head: `parent.mkdirs(); tmpFile.delete(); new FileOutputStream(tmp)` — - /// once per file, on its first chunk. Returns a shared handle for positioned writes. fn file_handle(&self, file_idx: usize) -> Result<Arc<File>, SinkError> { let file = &self.files[file_idx]; let mut st = self @@ -385,9 +310,6 @@ impl<'a> GogSink<'a> { } } - /// One verified chunk landed: count it; on the file's last chunk run the whole-file verify + - /// rename and fire the per-file progress event (Java: doneCount++, totalBytes += df.totalSize, - /// "Downloading: <name> <speed>"). fn chunk_done(&self, file_idx: usize, chunk_bytes: u64) -> Result<(), SinkError> { let file = &self.files[file_idx]; let mut st = self @@ -431,7 +353,6 @@ impl<'a> GogSink<'a> { Ok(()) } - /// `assembleDepotFile` tail: whole-file size + MD5 on the tmp, delete existing final, rename. fn finalize_file(&self, file_idx: usize, st: &mut FileState) -> Result<(), String> { let file = &self.files[file_idx]; if let Some(handle) = st.handle.take() { @@ -469,7 +390,6 @@ impl<'a> GogSink<'a> { Ok(()) } - /// Delete every unfinished `.bhtmp` (Java: `tmpFile.delete()` on the failing/cancelled task). fn cleanup_partials(&self) { for st in &self.states { let Ok(mut st) = st.lock() else { @@ -496,8 +416,6 @@ impl<'a> GogSink<'a> { } impl<'a> FetchSink for GogSink<'a> { - /// Body mode: `fetchChunkVerified` on a whole buffer, then a positioned write of the inflated - /// bytes (used by tests and available as a non-stream fallback). fn process(&self, item: &FetchItem, body: Vec<u8>) -> Result<u64, SinkError> { let Some(&(file_idx, chunk_idx)) = self.table.get(item.id as usize) else { return Err(SinkError::Fatal(format!("unknown item id {}", item.id))); @@ -508,7 +426,6 @@ impl<'a> FetchSink for GogSink<'a> { speed.record(body.len() as u64); } - // fetchChunkVerified, in order: compressed size → compressed MD5 → inflate → size → MD5. if chunk.compressed_size > 0 && body.len() as u64 != chunk.compressed_size { let msg = format!( "chunk compressed-size mismatch chunk={} exp={} got={}", @@ -528,7 +445,7 @@ impl<'a> FetchSink for GogSink<'a> { } let inflated = match inflate_zlib(&body) { Some(out) => out, - None => body, // stored (non-zlib) chunk + None => body, }; if chunk.size > 0 && inflated.len() as u64 != chunk.size { let msg = format!( @@ -563,8 +480,6 @@ impl<'a> FetchSink for GogSink<'a> { Ok(credited) } - /// Stream mode: hash the compressed piece, inflate it straight into the chunk's byte range - /// (or pass it through for a stored chunk), hashing + counting the decompressed output. fn on_chunk(&self, item: &FetchItem, offset: u64, data: &[u8]) -> Result<(), SinkError> { let id = item.id as usize; let Some(&(file_idx, chunk_idx)) = self.table.get(id) else { @@ -581,7 +496,6 @@ impl<'a> FetchSink for GogSink<'a> { .and_then(|m| m.lock().ok()) .ok_or_else(|| SinkError::Fatal("chunk stream poisoned".to_string()))?; if offset == 0 || slot.is_none() { - // (Re)start of an attempt: fresh hashers + inflater; positioned writes re-cover the range. let handle = self.file_handle(file_idx)?; *slot = Some(ChunkStream::new(handle, chunk.offset, chunk.size)); } @@ -601,7 +515,7 @@ impl<'a> FetchSink for GogSink<'a> { stream.mode = Some(if data[0] == 0x78 { ChunkMode::Zlib(flate2::write::ZlibDecoder::new(writer)) } else { - ChunkMode::Stored(writer) // stored (non-zlib) chunk — Java's `inflated = raw` + ChunkMode::Stored(writer) }); } let result = match stream.mode.as_mut() { @@ -616,7 +530,6 @@ impl<'a> FetchSink for GogSink<'a> { match result { Ok(()) => Ok(()), Err((_, Some(io))) => { - // Disk, not stream: Java's `fos.write` exception → file fails → run aborts. if let Some(mut st) = self.states.get(file_idx).and_then(|m| m.lock().ok()) { st.failed = true; let _ = st.handle.take(); @@ -627,7 +540,6 @@ impl<'a> FetchSink for GogSink<'a> { Err(SinkError::Fatal(msg)) } Err((err, None)) => { - // Corrupt zlib body. Java: inflate → null → raw → decompressed-size mismatch → retry. let msg = format!("chunk inflate failed chunk={} err={err}", chunk.hash); self.log(&msg); *slot = None; @@ -636,7 +548,6 @@ impl<'a> FetchSink for GogSink<'a> { } } - /// Stream mode: the body ended — run Java's checks in order and count the chunk. fn on_finish(&self, item: &FetchItem, _total_len: u64) -> Result<u64, SinkError> { let id = item.id as usize; let Some(&(file_idx, chunk_idx)) = self.table.get(id) else { @@ -652,7 +563,6 @@ impl<'a> FetchSink for GogSink<'a> { .take(); let stream = match taken { Some(s) => s, - // Empty body with no pieces: same checks on zero bytes. None => ChunkStream::new(self.file_handle(file_idx)?, chunk.offset, chunk.size), }; @@ -717,11 +627,8 @@ impl<'a> FetchSink for GogSink<'a> { } } -/// How often the byte ticker reports while files are in flight. pub const BYTE_TICK: Duration = Duration::from_millis(250); -/// The CDN bases to fetch from: empty entries dropped, then one per distinct host (`host_key`), -/// the first base seen for a host kept, order preserved. pub fn distinct_bases(bases: &[String]) -> Vec<String> { let mut seen: HashSet<String> = HashSet::new(); bases @@ -732,10 +639,6 @@ pub fn distinct_bases(bases: &[String]) -> Vec<String> { .collect() } -/// Runs `fetch` with a reporter thread beside it that calls `on_bytes(bytes_done + partial, -/// bytes_total)` every [`BYTE_TICK`] when the value moved. `on_file_done` only fires when a whole -/// file is finished, so a game made of a few large files gave the UI a progress bar that sat -/// still and then jumped; this fills the gaps. One last report is made when `fetch` returns. fn with_byte_ticker<T>( events: &dyn GogEvents, bytes_done: &AtomicU64, @@ -765,9 +668,6 @@ fn with_byte_ticker<T>( }) } -/// Runs one download loop. Blocking; returns when every pending file is finalized, on the first -/// fatal file failure, or on cancel. Safe to call again with `skip_paths` extended by the files -/// this run reported done (that is how the Java side re-runs after a secure-link refresh). pub fn run(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> GogRunResult { match req.kind { PlanKind::Gen2Chunks => run_gen2(req, cancel, events), @@ -808,8 +708,6 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go let files_verified = AtomicU32::new(0); if files.is_empty() { - // Java: the caller already returned "no depot files collected" before the loop; reaching - // here means the two parsers disagree — surface it loudly rather than pretend success. return GogRunResult { success: false, error: "no files in plan (manifest parse mismatch)".to_string(), @@ -818,8 +716,6 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go }; } - // Resume / repair pass — Java does this per task on the pool threads; here it runs up front - // on `process_workers` threads so the fetch item list can exclude verified files. let pending: Vec<AtomicBool> = files.iter().map(|_| AtomicBool::new(false)).collect(); let next = AtomicUsize::new(0); let verify_threads = process_workers.min(files.len().max(1)); @@ -870,7 +766,6 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go }; } - // Fetch items: one per chunk of every pending file, in plan order (chunks in manifest order). let mut table: Vec<(usize, usize)> = Vec::new(); let mut items: Vec<FetchItem> = Vec::new(); let mut states: Vec<Mutex<FileState>> = Vec::new(); @@ -884,7 +779,6 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go handle: None, chunks_done: 0, partial: 0, - // Files not pending (verified or skip-listed) are already final for this run. finalized: !is_pending, failed: false, tmp_path, @@ -899,7 +793,6 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go let path = build_cdn_path(&chunk.hash); items.push(FetchItem { id: table.len() as u64, - // One URL per host, in `hosts` order: the core picks `urls[host_idx]`. urls: bases.iter().map(|b| build_chunk_url(b, &path)).collect(), reserve: chunk.compressed_size, range: None, @@ -943,15 +836,12 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go }; let opts = FetchOptions { max_workers, - // The ceiling split across the distinct CDN hosts (floor 6); one host gets all of it. per_host_cap, timeout: CHUNK_TIMEOUT, headers: vec![("User-Agent".to_string(), USER_AGENT.to_string())], ca_bundle_path: req.ca_bundle_path.clone(), process_workers, label: req.label.clone(), - // Stream mode: pieces inflate straight to disk; no whole-body byte budget (the 24 MiB - // body budget + largest-first big chunks left 1-3 requests in flight at window 16). stream: true, }; let log = |line: &str| events.on_log(line); @@ -1010,23 +900,14 @@ fn run_gen2(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go } } -// ───────────────────────────────────────────────────────────────────────────────────────────── -// gen1: per-file HTTP Range GET streamed to disk (`runGen1` / `downloadRange`) -// ───────────────────────────────────────────────────────────────────────────────────────────── struct Gen1State { handle: Option<File>, - /// Bytes this file's current attempt has written (credited to the sink's `partial`); a new - /// attempt starts over, so a retry never counts its bytes twice. attempt_bytes: u64, out_path: PathBuf, finalized: bool, } -/// Stream-mode sink for gen1. Java's `downloadRange`: `outFile.delete()` before EVERY attempt, -/// `new FileOutputStream(out)`, copy the Range body, no size/hash check; success → `doneG1++`, -/// `totalBytesG1 += size`, `"Downloading: <name> <speed>"`. Pieces arrive in order with their -/// offset; `offset == 0` marks the (re)start of an attempt, exactly where Java deletes the file. struct Gen1Sink<'a> { files: &'a [Gen1File], states: Vec<Mutex<Gen1State>>, @@ -1036,8 +917,6 @@ struct Gen1Sink<'a> { bytes_done: &'a AtomicU64, events: &'a dyn GogEvents, speed: Mutex<SpeedSampler>, - /// Bytes of files still in flight (finished chunks; for gen1, the current attempt's pieces). - /// `bytes_done + partial` is what the byte ticker reports between file completions. partial: AtomicU64, } @@ -1061,7 +940,6 @@ impl<'a> FetchSink for Gen1Sink<'a> { .lock() .map_err(|_| SinkError::Fatal("gen1 state poisoned".to_string()))?; if offset == 0 { - // New attempt: Java `outFile.delete()` + `new FileOutputStream(out)`. self.partial .fetch_sub(std::mem::take(&mut st.attempt_bytes), Ordering::Relaxed); st.handle = None; @@ -1109,7 +987,7 @@ impl<'a> FetchSink for Gen1Sink<'a> { let mut st = state .lock() .map_err(|_| SinkError::Fatal("gen1 state poisoned".to_string()))?; - st.handle = None; // close + st.handle = None; st.finalized = true; let attempt = std::mem::take(&mut st.attempt_bytes); self.bytes_done.fetch_add(file.size, Ordering::Relaxed); @@ -1130,10 +1008,6 @@ impl<'a> FetchSink for Gen1Sink<'a> { } } -/// gen1 loop. Resume = `exists && length == size` ("Resuming…", no bytes). Items = one Range GET -/// per pending file; retries/back-off by the core (Java: 3 attempts, 1 s / 2 s). Failed or -/// cancelled attempts leave whatever was written, like Java (a wrong-size partial fails the -/// size-match resume and is re-pulled next time; a base-install cancel deletes the whole dir). fn run_gen1(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> GogRunResult { let mut files: Vec<Gen1File> = Vec::new(); for manifest in &req.depot_manifests { @@ -1298,7 +1172,7 @@ fn run_gen1(req: &GogRequest, cancel: &AtomicBool, events: &dyn GogEvents) -> Go GogRunResult { success, cancelled, - link_expiry: false, // gen1 has no secure-link refresh in Java + link_expiry: false, error: if cancelled && error.is_empty() { "cancelled".to_string() } else { error }, bytes_written: bytes_done.load(Ordering::Relaxed), files_done: files_done.load(Ordering::Relaxed), @@ -1412,8 +1286,6 @@ mod tests { assert!(peak >= 8.0, "2 MB across at most two buckets → ≥8 Mbps peak, got {peak}"); } - /// Whole-file finalize through the sink with a synthetic body (no network): verifies the - /// chunk → positioned write → whole-file MD5 → rename path and the progress event shape. #[test] fn sink_assembles_verifies_and_renames() { use flate2::write::ZlibEncoder; @@ -1468,10 +1340,8 @@ mod tests { }; let item = |id: u64| FetchItem { id, urls: vec!["x".into()], reserve: 0, range: None }; - // Second chunk first: positioned write must still produce the right bytes. assert_eq!(sink.process(&item(1), cb.clone()).unwrap(), part_b.len() as u64); assert!(tmp_path.exists() && !out_path.exists(), "still staging after 1 of 2 chunks"); - // A corrupt body is a retryable failure, not fatal. match sink.process(&item(0), b"\x78garbage".to_vec()) { Err(SinkError::Retry(msg)) => assert!(msg.contains("mismatch"), "{msg}"), Err(SinkError::Fatal(msg)) => panic!("expected Retry, got Fatal({msg})"), @@ -1580,16 +1450,14 @@ mod tests { assert_eq!(per_host_cap_for(0, 0), 6); } - /// Stream mode: zlib chunk split into pieces (with a poisoned first attempt restarting at - /// offset 0), a stored chunk, corrupt data → Retry, then the whole-file finalize. #[test] fn stream_sink_inflates_pieces_verifies_and_finalizes() { use flate2::write::ZlibEncoder; use flate2::Compression; let dir = temp_dir("stream"); - let part_a = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".repeat(50); // 2000 bytes, compresses well - let part_b = b"\x01\x02stored bytes".to_vec(); // first byte != 0x78 → stored + let part_a = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".repeat(50); + let part_b = b"\x01\x02stored bytes".to_vec(); let whole = [part_a.clone(), part_b.clone()].concat(); let mut enc = ZlibEncoder::new(Vec::new(), Compression::default()); enc.write_all(&part_a).unwrap(); @@ -1632,7 +1500,6 @@ mod tests { }; let item = |id: u64| FetchItem { id, urls: vec!["x".into()], reserve: 0, range: None }; - // Attempt 1 of the zlib chunk: corrupt tail → Retry from on_chunk. let mut corrupt = ca.clone(); for b in corrupt.iter_mut().skip(2) { *b = !*b; @@ -1642,7 +1509,6 @@ mod tests { Err(SinkError::Retry(m)) => assert!(m.contains("inflate failed"), "{m}"), Err(SinkError::Fatal(m)) => panic!("expected Retry, got Fatal({m})"), Ok(()) => { - // Some corruptions only surface at finish: that must also be a Retry. match sink.on_finish(&item(0), corrupt.len() as u64) { Err(SinkError::Retry(_)) => {} other => panic!("expected Retry at finish, got {}", match other { @@ -1653,7 +1519,6 @@ mod tests { } } } - // Attempt 2: restart at offset 0 in three pieces. let cut1 = ca.len() / 3; let cut2 = 2 * ca.len() / 3; sink.on_chunk(&item(0), 0, &ca[..cut1]).unwrap(); @@ -1661,7 +1526,6 @@ mod tests { sink.on_chunk(&item(0), cut2 as u64, &ca[cut2..]).unwrap(); assert_eq!(sink.on_finish(&item(0), ca.len() as u64).unwrap(), 0); assert!(tmp_path.exists() && !out_path.exists()); - // Stored chunk, one piece; wrong length first (→ Retry), then the real one. sink.on_chunk(&item(1), 0, &part_b[..3]).unwrap(); match sink.on_finish(&item(1), 3) { Err(SinkError::Retry(m)) => assert!(m.contains("compressed-size mismatch"), "{m}"), @@ -1705,7 +1569,6 @@ mod tests { partial: AtomicU64::new(0), }; let item = FetchItem { id: 0, urls: vec!["x".into()], reserve: 8, range: Some((100, 107)) }; - // Attempt 1 writes a partial, attempt 2 restarts from offset 0 and truncates it. sink.on_chunk(&item, 0, b"ZZZZZZZZZZZZ").unwrap(); sink.on_chunk(&item, 0, b"abcd").unwrap(); sink.on_chunk(&item, 4, b"efgh").unwrap(); @@ -1737,11 +1600,7 @@ mod tests { ..Default::default() }; let rec = Recorder { events: StdMutex::new(Vec::new()) }; - // Cancel before the fetch so no network is touched: the resume pass still runs. let cancel = AtomicBool::new(false); - // Use a pre-set cancel via a second request: run the resume pass, then expect the fetch - // to be attempted for short.bin only. We can't reach the network here, so assert on the - // plan log line instead of the outcome. cancel.store(true, Ordering::Relaxed); let res = run(&req, &cancel, &rec); assert!(res.cancelled, "{res:?}"); diff --git a/app/src/main/rust/stores/src/gog/jni.rs b/app/src/main/rust/stores/src/gog/jni.rs index eee602a6b..9fa1fbf01 100644 --- a/app/src/main/rust/stores/src/gog/jni.rs +++ b/app/src/main/rust/stores/src/gog/jni.rs @@ -1,15 +1,3 @@ -//! JNI facade for the GOG Rust download engine — the native half of -//! `com.droiddeck.launcher.stores.gog.GogNative` (Kotlin). Symbol names are bound to that -//! package/class; the `@JvmStatic external` declarations there are the other side of this file. -//! -//! Shape mirrors the Steam download JNI (`jni.rs` `nativeDownloadApp`): `nativeStart` validates -//! its inputs, takes a `GlobalRef` on the listener, spawns the worker thread and returns a handle -//! immediately; callbacks are delivered from the worker / process-pool threads through a -//! `JavaVM` attach; `nativeCancel` flips an `AtomicBool`; `nativeRelease` drops the Java side's -//! reference to the handle (the worker keeps its own until it exits). -//! -//! Log tag `GogNative`: every engine log line goes to logcat here AND to the listener's `onLog` -//! (which the Kotlin manager folds into its own debug log). use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -29,7 +17,6 @@ unsafe extern "C" { fn __android_log_write(prio: i32, tag: *const i8, text: *const i8) -> i32; } -/// `android.util.Log.i(LOG_TAG, message)`; a no-op on non-Android hosts (cargo test). pub fn android_log(message: &str) { #[cfg(target_os = "android")] { @@ -51,8 +38,6 @@ pub fn android_log(message: &str) { } } -/// One in-flight run. The Java side owns a `Box<Arc<..>>` (the `jlong`); the worker thread owns a -/// second `Arc` so `nativeRelease` is safe at any time after `nativeStart` returned. pub struct GogRunHandle { pub cancel: AtomicBool, } @@ -137,8 +122,6 @@ impl GogEvents for JniEvents { } fn on_bytes(&self, bytes_done: u64, bytes_total: u64) { - // A listener without `onBytes` (an app built against the older contract) makes this a - // NoSuchMethodError, which clear_exception drops: the run is unaffected. self.with_env(|env| { let _ = env.call_method( self.listener.as_obj(), @@ -209,7 +192,6 @@ unsafe fn handle_ref(handle: jlong) -> Option<&'static Arc<GogRunHandle>> { } } -/// Symbol-binding probe (see `GogNative.isAvailable`). Returns 1. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativeProbe( _env: JNIEnv, @@ -218,9 +200,6 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativePr 1 } -/// Starts one download loop on a worker thread and returns a handle (0 = could not start; the -/// listener then receives NO callbacks). Inputs are exactly what `GogDownloadManager` holds after -/// manifest + secure-link resolution — see `GogRequest`. #[no_mangle] #[allow(clippy::too_many_arguments)] pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativeStart( @@ -290,8 +269,6 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativeSt Box::into_raw(Box::new(handle)) as jlong } -/// Requests cancellation; the run stops at its next scheduling point and `onComplete` fires with -/// `cancelled = true`. Idempotent. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativeCancel( _env: JNIEnv, @@ -303,8 +280,6 @@ pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativeCa } } -/// Drops the Java side's reference. Call once, after `onComplete` (or after cancelling — the worker -/// keeps the run alive until it exits either way). #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_gog_GogNative_nativeRelease( _env: JNIEnv, diff --git a/app/src/main/rust/stores/src/gog/plan.rs b/app/src/main/rust/stores/src/gog/plan.rs index 830106dc1..50f833814 100644 --- a/app/src/main/rust/stores/src/gog/plan.rs +++ b/app/src/main/rust/stores/src/gog/plan.rs @@ -1,50 +1,24 @@ -//! Depot-manifest JSON → download plan. Mirrors `GogDownloadManager.parseDepotManifest` rule for -//! rule (see Bannerlator's `docs/RUST_GOG_PARITY.md` §3): -//! - root `depot.items[]`; `path` with `\` → `/` and one leading `/` stripped; -//! - a file is kept only when `path` is non-empty AND `chunks[]` is non-empty; -//! - per chunk: CDN hash = `compressedMd5` when non-empty, else `md5`; a chunk with neither is -//! dropped; `compressedSize` / `size` default 0; -//! - file `md5` kept when non-empty; `total_size` = Σ chunk `size` (decompressed); -//! - a file whose chunks all dropped is not added. -//! `org.json` coercion (`optString` turns numbers/bools into their text, `optLong` parses numeric -//! strings) is reproduced by [`opt_string`] / [`opt_u64`]. -//! -//! Additionally each chunk carries `offset` = prefix-sum of the preceding chunks' decompressed -//! sizes — Java appends sequentially so it never needs one; the Rust sink writes chunks at that -//! offset as they arrive so the final bytes are identical. use serde_json::Value; #[derive(Clone, Debug, PartialEq, Eq)] pub struct ChunkRef { - /// CDN-path hash (`compressedMd5` preferred, else `md5`). pub hash: String, - /// Verify compressed bytes before inflate (may be empty). pub compressed_md5: String, - /// Verify decompressed bytes after inflate (may be empty). pub md5: String, - /// Expected compressed size, 0 if absent. pub compressed_size: u64, - /// Expected decompressed size, 0 if absent. pub size: u64, - /// Decompressed offset inside the file (prefix-sum of preceding `size`s). pub offset: u64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct PlannedFile { - /// Install-dir-relative path, forward slashes, no leading slash. pub relative_path: String, - /// Whole-file MD5 from the manifest ("" when absent). pub md5: String, - /// Σ chunk decompressed sizes (0 if unknown). pub total_size: u64, pub chunks: Vec<ChunkRef>, } -/// `JSONObject.optString(key, "")` semantics: strings as-is, numbers/bools rendered, null/absent -/// → "". (`org.json` renders `JSONObject.NULL` as "null"; GOG manifests never carry JSON nulls in -/// these fields, and treating one as absent is the only safe reading.) pub fn opt_string(obj: &Value, key: &str) -> String { match obj.get(key) { Some(Value::String(s)) => s.clone(), @@ -54,8 +28,6 @@ pub fn opt_string(obj: &Value, key: &str) -> String { } } -/// `JSONObject.optLong(key, 0)` semantics for a size field (numbers, or numeric strings; a -/// negative/float value that Java would truncate is clamped to 0 — sizes are never negative). pub fn opt_u64(obj: &Value, key: &str) -> u64 { match obj.get(key) { Some(Value::Number(n)) => n @@ -68,8 +40,6 @@ pub fn opt_u64(obj: &Value, key: &str) -> u64 { } } -/// Parses one inflated gen2 depot manifest and appends its files to `out` -/// (`GogDownloadManager.parseDepotManifest`). Malformed JSON adds nothing, like Java's catch-all. pub fn parse_depot_manifest(json: &str, out: &mut Vec<PlannedFile>) { let Ok(root) = serde_json::from_str::<Value>(json) else { return; @@ -137,20 +107,14 @@ pub fn parse_depot_manifest(json: &str, out: &mut Vec<PlannedFile>) { } } -/// One gen1 file: a byte range of a (pre-signed) depot blob (`GogDownloadManager.Gen1File`). #[derive(Clone, Debug, PartialEq, Eq)] pub struct Gen1File { - /// Install-dir-relative path, used verbatim (Java's gen1 path does not normalise slashes). pub path: String, pub url: String, pub offset: u64, pub size: u64, } -/// Parses a gen1 build manifest (`runGen1`): every `depot[]` entry that is not `support: true`, -/// its `files[]` → (`path`, `url`, `offset` default 0, `size` default 0); an entry with -/// `size == 0` is dropped (Java's `path == null || url == null` checks never fire — `optString` -/// returns "" — so `size == 0` is the only effective filter, mirrored here). pub fn parse_gen1_manifest(json: &str, out: &mut Vec<Gen1File>) { let Ok(root) = serde_json::from_str::<Value>(json) else { return; @@ -184,10 +148,6 @@ pub fn parse_gen1_manifest(json: &str, out: &mut Vec<Gen1File>) { } } -/// Parses every manifest in order (the order Java fetched the depots) and optionally applies the -/// base-install largest-first (LPT) ordering — a STABLE sort by descending `total_size`, exactly -/// `Collections.sort(files, (a, b) -> Long.compare(b.totalSize, a.totalSize))`. The DLC and -/// dependency paths in Java do not sort, so they pass `sort_largest_first = false`. pub fn build_plan(manifests: &[String], sort_largest_first: bool) -> Vec<PlannedFile> { let mut files = Vec::new(); for manifest in manifests { @@ -199,8 +159,6 @@ pub fn build_plan(manifests: &[String], sort_largest_first: bool) -> Vec<Planned files } -/// `GogDownloadManager.buildCdnPath`: `"ab/cd/abcdef..."`. A hash shorter than 4 chars would -/// throw in Java (and fail that file); here it is returned unchanged and fails at fetch time. pub fn build_cdn_path(hash: &str) -> String { if !hash.is_char_boundary(2) || !hash.is_char_boundary(4) || hash.len() < 4 { return hash.to_string(); @@ -208,8 +166,6 @@ pub fn build_cdn_path(hash: &str) -> String { format!("{}/{}/{}", &hash[0..2], &hash[2..4], hash) } -/// `GogDownloadManager.buildChunkUrl`: appends the chunk path BEFORE any query string so the -/// secure-link token survives. pub fn build_chunk_url(base: &str, chunk_path: &str) -> String { match base.find('?') { Some(q) => format!("{}/{}{}", &base[..q], chunk_path, &base[q..]), @@ -217,8 +173,6 @@ pub fn build_chunk_url(base: &str, chunk_path: &str) -> String { } } -/// Per-host key for the fetch core's host semaphores: `scheme://host[:port]` of `url` (lowercased -/// host). Java has one resolved CDN base per run, so the host list has exactly one entry. pub fn host_key(url: &str) -> String { let (scheme, rest) = match url.find("://") { Some(i) => (&url[..i], &url[i + 3..]), diff --git a/app/src/main/rust/stores/src/lib.rs b/app/src/main/rust/stores/src/lib.rs index 963826c40..87d278ab0 100644 --- a/app/src/main/rust/stores/src/lib.rs +++ b/app/src/main/rust/stores/src/lib.rs @@ -1,21 +1,4 @@ -//! DroidDeck's native store download engines (`libdroiddeckstores.so`). -//! -//! The GOG, Epic Games Store and Amazon Games download loops, ported from Bannerlator's -//! `bl-steam-client` crate (GPL-3.0-or-later, itself derived from WinNative's `wn-steam-client`). -//! Only the store engines and the fetch core they run on came across: DroidDeck's Steam client is -//! the real one, running in the Linux runtime, so none of the Steam CM / depot code is here. -//! -//! Each store module owns its manifest → plan → `FetchItem` mapping, its `FetchSink` (inflate + -//! hash + write in the exact output layout of the Kotlin manager that drives it) and its JNI facade. -//! The JNI contract — classes, method names, signatures, threading — is written down in `JNI.md` -//! next to `Cargo.toml`; the Kotlin side mirrors it exactly. -//! -//! What a run looks like: Kotlin hands over a manifest it already parsed (or, for Amazon, the -//! plan it built from one), the resolved CDN base(s) and the install directory, calls `nativeStart` -//! and gets a handle back at once. Everything after that happens on native threads: the fetch core -//! keeps an adaptive window of HTTP requests in flight on one tokio runtime, hands bodies to a sync -//! process pool that inflates, hashes and writes them, and reports progress through the listener -//! object Kotlin passed in. `nativeCancel` flips a flag; `onComplete` fires exactly once. +//! Ported from Bannerlator's bl-steam-client crate (GPL-3.0-or-later), itself derived from WinNative's wn-steam-client. #![allow(clippy::missing_safety_doc, clippy::result_large_err)] diff --git a/app/src/main/rust/stores/src/md5_small.rs b/app/src/main/rust/stores/src/md5_small.rs index 8abd58bcc..943785b3f 100644 --- a/app/src/main/rust/stores/src/md5_small.rs +++ b/app/src/main/rust/stores/src/md5_small.rs @@ -1,17 +1,10 @@ -//! Minimal MD5 (RFC 1321) for the store download engines (GOG chunk / file verification). -//! -//! In-crate on purpose: the crate is built `--locked` in CI with no way to add a dependency from -//! this environment, and the algorithm is 100 lines. Not for anything security-sensitive — it only -//! mirrors the checksum the Java managers already compare against. -/// One-shot digest. pub fn md5(data: &[u8]) -> [u8; 16] { let mut h = Md5::new(); h.update(data); h.finalize() } -/// Lower-case hex of a 16-byte digest. pub fn md5_hex(digest: &[u8; 16]) -> String { const HEX: &[u8; 16] = b"0123456789abcdef"; let mut out = String::with_capacity(32); @@ -22,7 +15,6 @@ pub fn md5_hex(digest: &[u8; 16]) -> String { out } -/// Streaming MD5: `update` any number of times, then `finalize`. #[derive(Clone, Debug)] pub struct Md5 { state: [u32; 4], @@ -93,7 +85,6 @@ impl Md5 { let bit_len = self.total_len.wrapping_mul(8); let mut pad = [0u8; 72]; pad[0] = 0x80; - // Pad to 56 mod 64, then append the 64-bit little-endian bit length. let pad_len = if self.buffer_len < 56 { 56 - self.buffer_len } else { @@ -189,7 +180,6 @@ mod tests { #[test] fn padding_edge_lengths() { - // 55 / 56 / 64 bytes exercise the "pad fits in one block" vs "needs a second block" paths. assert_eq!(hex(&[b'a'; 55]), "ef1772b6dff9a122358552954ad0df65"); assert_eq!(hex(&[b'a'; 56]), "3b0c8ac703f828b04c6c197006d17218"); assert_eq!(hex(&[b'a'; 64]), "014842d480b571495a4a0363793f7367"); diff --git a/app/src/main/rust/stores/src/priority.rs b/app/src/main/rust/stores/src/priority.rs index 916008fac..7ea7bb2ea 100644 --- a/app/src/main/rust/stores/src/priority.rs +++ b/app/src/main/rust/stores/src/priority.rs @@ -1,22 +1,5 @@ -//! Background priority for every thread the store engines create. -//! -//! A download keeps several threads busy at once (the run's worker, the tokio runtime and its -//! blocking pool for DNS, the inflate/hash/write pool, the throughput reporter, GOG's verify pass, -//! the Epic assembler). At the default nice 0 they compete as equals with the app's UI and input -//! threads, and on the device the menus' animations and controller navigation slowed visibly for -//! as long as a store download ran. At nice +10 the scheduler hands them only what the UI leaves, -//! which for a download bound by network and storage costs next to nothing. -//! -//! Linux applies `setpriority(PRIO_PROCESS, tid, …)` to the one thread `tid`, not the process, and -//! a new thread inherits its creator's nice value; every thread is still lowered explicitly at its -//! start so the guarantee does not depend on who spawned it. Only threads this library creates are -//! touched: the JNI calling thread belongs to the app and keeps its priority. - -/// The nice value store-engine threads run at. pub const BACKGROUND_NICE: i32 = 10; -/// Lower the calling thread to [`BACKGROUND_NICE`]. Best effort: a refusal leaves the thread at -/// its current priority and the download runs as before. pub fn background() { #[cfg(any(target_os = "android", target_os = "linux"))] unsafe { @@ -25,7 +8,6 @@ pub fn background() { } } -/// The calling thread's nice value as a log field, e.g. `priority=nice10`. pub fn log_field() -> String { #[cfg(any(target_os = "android", target_os = "linux"))] { diff --git a/app/src/main/rust/stores/src/stores_jni.rs b/app/src/main/rust/stores/src/stores_jni.rs index e69b7f5bd..ea86fad3f 100644 --- a/app/src/main/rust/stores/src/stores_jni.rs +++ b/app/src/main/rust/stores/src/stores_jni.rs @@ -1,19 +1,10 @@ -//! The one export that is not a store engine: `com.droiddeck.launcher.stores.StoresNative`. -//! -//! `nativeVersion()` exists so the app can prove, at Setup time, that `libdroiddeckstores.so` -//! loaded and its JNI exports bind — before a user is mid-download and an -//! `UnsatisfiedLinkError` would surface as a failed install. It returns the crate version from -//! `Cargo.toml`, which the app can show next to the other component versions. use jni::objects::JClass; use jni::sys::jstring; use jni::JNIEnv; -/// The crate version, as `Cargo.toml` declares it. pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -/// `StoresNative.nativeVersion(): String` — the crate version (`"0.1.0"`). A null return means the -/// JVM could not allocate the string, which the caller treats like a failed load. #[no_mangle] pub extern "system" fn Java_com_droiddeck_launcher_stores_StoresNative_nativeVersion( env: JNIEnv, diff --git a/app/src/test/java/com/droiddeck/launcher/core/LogRedactorTest.kt b/app/src/test/java/com/droiddeck/launcher/core/LogRedactorTest.kt index b1c97962d..41f6f9d4e 100644 --- a/app/src/test/java/com/droiddeck/launcher/core/LogRedactorTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/core/LogRedactorTest.kt @@ -88,7 +88,6 @@ class LogRedactorTest { } } - // The Steam UI's own login line (webhelper_js.txt), as the privacy scan found it. @Test fun loginStateLineLosesAnEmailAccount() { val line = "[2026-10-07 21:14:03] SteamUI: INFO: Login: OnLoginStateChange someone.masked@example.com 2 1 0 0" val out = LogRedactor.redact(line) diff --git a/app/src/test/java/com/droiddeck/launcher/core/SecretScrubTest.kt b/app/src/test/java/com/droiddeck/launcher/core/SecretScrubTest.kt index 86ff6ece8..61133f8a4 100644 --- a/app/src/test/java/com/droiddeck/launcher/core/SecretScrubTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/core/SecretScrubTest.kt @@ -13,7 +13,6 @@ class SecretScrubTest { "0024:warn:seh:dispatch_exception code=c0000005 flags=0 addr=0000000140012345", "0030:trace:seh:handle_syscall_fault code=80000003 flags=0", "exception code=0xC0000005 at 0x140012345", - // An exception code stays one even on a line that also carries OAuth keys. "client_id=abc code=c0000005", )) assertEquals(line, share(line)) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedExesTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedExesTest.kt index 528e97d42..2d0a29de4 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedExesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedExesTest.kt @@ -13,7 +13,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** The Games tab's +: one picked .exe becomes a game, once, wherever the session can see it. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class AddedExesTest { @@ -54,7 +53,6 @@ class AddedExesTest { assertEquals(Library.ADDED, game.source) assertTrue(File(app.filesDir, "session/added-games.json").readText().contains("Celeste.exe")) - // The same exe again: the game it already is, no second entry. val again = AddedGames.addExe(app, pick) assertTrue(again is AddedGames.AddResult.Existing) assertEquals(game.appId, (again as AddedGames.AddResult.Existing).game.appId) @@ -83,7 +81,6 @@ class AddedExesTest { val result = AddedGames.addExe(app, deep) assertTrue(result is AddedGames.AddResult.Switched) assertEquals(deep.path, AddedGames.scan(app).single().exe.path) - // The Games folder's game is an entry of its own (GamesFolderMigration); no second one. assertEquals(listOf(File(games, "Example").path), AddedExes.list(app).map { it.folder }) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameArtLookupTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameArtLookupTest.kt index 1c2978602..e0c7f3eb1 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameArtLookupTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameArtLookupTest.kt @@ -20,7 +20,6 @@ import java.io.File import java.security.SecureRandom import javax.crypto.spec.SecretKeySpec -/** Where an added game's art comes from: its own Steam appid, a matching name, SteamGridDB's key. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class AddedGameArtLookupTest { @@ -39,7 +38,6 @@ class AddedGameArtLookupTest { val match = SteamMatch.resolve(app, dir.path, exe, "Palworld") { fail("searched"); null } assertEquals(1623730, match.appId) assertEquals(SteamMatch.Certainty.FILES, match.certainty) - // The same record for everyone who asks. assertEquals(1623730, SteamMatch.get(app, dir.path)!!.appId) } @@ -68,7 +66,6 @@ class AddedGameArtLookupTest { } @Test fun aStoreResultIsTakenOnlyWhenItsNameMatches() { - // The first result is no longer taken for being first. assertNull(AddedGameArt.pickStoreResult("My Obscure Game", listOf("10" to "Totally Different", "11" to "Another One"))) assertEquals("20", AddedGameArt.pickStoreResult("Hades", listOf("19" to "Hades II Soundtrack Bundle Extra", "20" to "Hades"))) assertEquals("30", AddedGameArt.pickStoreResult("Skyrim Special Edition Complete Edition", listOf("1" to "Unrelated", "30" to "Skyrim Special Edition"))) @@ -95,7 +92,6 @@ class AddedGameArtLookupTest { assertEquals("build", SteamGridDb.pick("", "build")) assertEquals("build", SteamGridDb.pick(" ", "build")) assertEquals("", SteamGridDb.pick("", "")) - // This test build carries no key. assertEquals("", SteamGridDb.builtInKey()) assertEquals("", SteamGridDb.key(app)) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameEditsTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameEditsTest.kt index f83a082d1..3697ef9a7 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameEditsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGameEditsTest.kt @@ -18,7 +18,6 @@ import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config import java.io.File -/** "Add all games in this folder", the editor's writes to the shortcut listing, art choices. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class AddedGameEditsTest { @@ -52,14 +51,11 @@ class AddedGameEditsTest { val alpha = result.added.first { it.name == "Alpha" } assertEquals("Alpha.exe", alpha.exe.name) assertFalse(alpha.exeUncertain) - // Two lookalikes: the best guess, marked. assertTrue(result.added.first { it.name == "Beta" }.exeUncertain) - // Entries, not a Games folder. assertEquals(emptyList<String>(), SessionPrefs.addedGamesDirs(app)) assertEquals(3, AddedExes.list(app).size) assertTrue(listing().map { it.getString("name") }.containsAll(listOf("Alpha", "Beta", "Celeste"))) - // Again: nothing new, all three there already. val again = AddedGames.addFolder(app, dir) assertEquals(emptyList<String>(), again.added.map { it.name }) assertEquals(3, again.already) @@ -84,7 +80,6 @@ class AddedGameEditsTest { assertEquals(game.appId, entry.getLong("appid")) assertEquals(game.appId, AddedGameEdits.game(app, folder)!!.appId) - // Back to automatic. AddedGameEdits.setName(app, folder, "") assertTrue(AddedGameEdits.setStartIn(app, folder, "")) val auto = listing().single() @@ -109,8 +104,6 @@ class AddedGameEditsTest { assertEquals("/g/Game/data", SessionPrefs.addedGameStartIn(app, folder.path)) assertEquals("-safe", SessionPrefs.addedGameLaunch(app, folder.path)) - // The user renames it in the app before Steam's shortcut is written again: the same Steam - // value is not taken a second time. SessionPrefs.setAddedGameName(app, folder.path, "Mine") AddedGames.adoptSteamEdits(app, folder, record) assertEquals("Mine", SessionPrefs.addedGameName(app, folder.path)) diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesCandidatesTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesCandidatesTest.kt index 6771a4a18..f2881c712 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesCandidatesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesCandidatesTest.kt @@ -13,7 +13,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** The .exe picker below a game folder (five levels, past installer folders, ranked) and the names it finds. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class AddedGamesCandidatesTest { @@ -65,7 +64,6 @@ class AddedGamesCandidatesTest { val game = tmp.newFolder("Huge") for (i in 0 until GameExePicker.WALK_BUDGET + 50) File(game, "data/d%04d".format(i)).mkdirs() exe(game, "zzz/Late.exe") - // data/ alone fills the budget before zzz/ is listed. assertEquals(emptyList<String>(), names(game)) val small = tmp.newFolder("Small") exe(small, "x/y/Found.exe") @@ -136,7 +134,6 @@ class AddedGamesCandidatesTest { game.setLastModified(game.lastModified() + 10_000) val after = AddedGames.scan(app).single() assertEquals("The Witcher 3 - Wild Hunt", after.name) - // Already added under the folder name: the shortcut keeps its id. assertEquals(before.appId, after.appId) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesRemoveTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesRemoveTest.kt index e98d08031..00da0980e 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesRemoveTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesRemoveTest.kt @@ -12,7 +12,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** Removing an added game stops listing it and keeps it off after a rescan, until it is added again; nothing on disk goes. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class AddedGamesRemoveTest { @@ -47,7 +46,6 @@ class AddedGamesRemoveTest { SessionPrefs.setAddedGameRemoved(app, File(games, "Beta").path, true) assertEquals(listOf("Alpha", "Gamma"), AddedGames.scan(app).map { it.name }) - // A rescan does not bring it back. assertEquals(listOf("Alpha", "Gamma"), AddedGames.scan(app).map { it.name }) SessionPrefs.setAddedGameRemoved(app, File(games, "Beta").path, false) @@ -66,7 +64,6 @@ class AddedGamesRemoveTest { assertTrue(!listing.contains("\"Beta\"")) assertTrue(File(beta, "Beta.exe").isFile) - // Added again with the +: it comes back. assertTrue(AddedGames.addExe(app, File(beta, "Beta.exe")) is AddedGames.AddResult.Switched) assertEquals(emptyList<String>(), SessionPrefs.removedAddedGames(app)) assertTrue(File(app.filesDir, "session/added-games.json").readText().contains("\"Beta\"")) diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesStoreScanTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesStoreScanTest.kt index 6284086c7..38d3760c1 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesStoreScanTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesStoreScanTest.kt @@ -13,7 +13,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** A store install is one more folder the scan finds, named and aimed by its sidecar. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class AddedGamesStoreScanTest { @@ -38,7 +37,6 @@ class AddedGamesStoreScanTest { assertEquals("HOMM3.exe", game.exe.name) assertEquals(Store.GOG.id, game.source) assertEquals("1207658924", game.storeId) - // Inside the runtime's tree the folder needs no bind: its guest path is its path from the tree's root. assertEquals("/root/Games/Stores/GOG/Heroes of Might and Magic 3 Complete/HOMM3.exe", game.guestExe) assertEquals("/root/Games/Stores/GOG/Heroes of Might and Magic 3 Complete", game.guestDir) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesTest.kt index 244f66373..b42748f18 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedGamesTest.kt @@ -19,7 +19,6 @@ class AddedGamesTest { "\"AppState\"\n{\n\t\"appid\"\t\t\"17410\"\n\t\"installdir\"\t\t\"Mirrors Edge\"\n}\n", ) assertEquals(setOf("monster_hunter_stories_3_twisted_reflection", "mirrors edge"), AddedGames.steamInstallDirs(common)) - // Loose game folders at the library root are never Steam's. assertEquals(emptySet<String>(), AddedGames.steamInstallDirs(library)) } } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/GameFileSyncTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/GameFileSyncTest.kt index 09245502a..a4e3b0028 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/GameFileSyncTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/GameFileSyncTest.kt @@ -30,7 +30,6 @@ class GameFileSyncTest { assertEquals("8400", File(folder, "Geometry Wars.steam").readText()) assertEquals(folder.path, GameFileSync.folder(context)) assertEquals(1, folder.listFiles()!!.count { it.extension == GameFiles.EXTENSION }) - // An unavailable source must leave the existing export intact. val unavailable = File(steamapps.parentFile, "steamapps-unavailable") assertTrue(steamapps.renameTo(unavailable)) assertThrows(IllegalStateException::class.java) { GameFileSync.syncConfigured(context) } @@ -38,11 +37,9 @@ class GameFileSyncTest { assertTrue(unavailable.renameTo(steamapps)) GameFileSync.syncConfigured(context) assertTrue(File(folder, "Geometry Wars.steam").exists()) - // A genuine uninstall removes the owned export at the next tick. assertTrue(manifest.delete()) GameFileSync.syncConfigured(context) assertEquals(0, folder.listFiles()!!.count { it.extension == GameFiles.EXTENSION }) - // Disabled sync does not write files for new installs. GameFileSync.disable(context) manifest.writeText("\"name\" \"Geometry Wars\"\n\"StateFlags\" \"4\"") GameFileSync.syncConfigured(context) diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/GameFilesTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/GameFilesTest.kt index 8bf61b338..d37689e29 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/GameFilesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/GameFilesTest.kt @@ -38,7 +38,6 @@ class GameFilesTest { val provider = LaunchFileProvider(file) provider.attachInfo(context, android.content.pm.ProviderInfo().apply { authority = "frontend" }) ShadowContentResolver.registerProviderInternal("frontend", provider) - // Providers often give opaque document IDs with no filename extension. val uri = Uri.parse("content://frontend/document/opaque%3A42") assertEquals("18446744073709551615", GameFiles.readIntent(context, Intent(Intent.ACTION_VIEW, uri))) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/GameIdentifierTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/GameIdentifierTest.kt index 9f6a6820b..f7ddc0212 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/GameIdentifierTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/GameIdentifierTest.kt @@ -9,18 +9,13 @@ import org.junit.Test import org.junit.rules.TemporaryFolder import java.io.File -/** - * Signal-chain coverage for [GameIdentifier]. Pure JVM: builds - * fake game layouts in a temp dir and asserts the right appId/name/source/confidence come back. - * The PE version-resource path is exercised on-device (real .exe), not here. - */ class GameIdentifierTest { @get:Rule val tmp = TemporaryFolder() private fun exeIn(dir: File, name: String = "game.exe"): File = - File(dir, name).apply { parentFile?.mkdirs(); writeBytes(byteArrayOf(0x4D, 0x5A)) } // "MZ" — not a real PE + File(dir, name).apply { parentFile?.mkdirs(); writeBytes(byteArrayOf(0x4D, 0x5A)) } @Test fun steamAppIdTxt_besideExe_givesHighConfidenceAppId() { @@ -51,7 +46,6 @@ class GameIdentifierTest { @Test fun steamManifestAcf_matchedByInstalldir_givesAppIdAndName() { - // steamapps/common/Palworld/Pal/Binaries/Win64/Palworld-Win64-Shipping.exe val steamapps = tmp.newFolder("steamapps") File(steamapps, "appmanifest_1623730.acf").writeText( """ @@ -63,7 +57,6 @@ class GameIdentifierTest { } """.trimIndent() ) - // a decoy manifest that must NOT win File(steamapps, "appmanifest_570.acf").writeText( """"AppState" { "appid" "570" "name" "Dota 2" "installdir" "dota 2 beta" }""" ) @@ -81,9 +74,6 @@ class GameIdentifierTest { """{"gameId":"1207658691","name":"The Witcher 3: Wild Hunt","version":1}""" ) val id = GameIdentifier.identify(exeIn(dir)) - // normalizeName() deliberately converts ":" to " - " (a colon is illegal in a Windows - // filename), so the identified name never carries one. The test asserted the raw GOG - // string and had never passed. assertEquals("The Witcher 3 - Wild Hunt", id.name) assertEquals("1207658691", id.gogId) assertNull(id.appId) @@ -103,7 +93,6 @@ class GameIdentifierTest { @Test fun noMarkers_fallsBackToCleanedFolderName_forEngineBinary() { - // UE shipping binary → the folder name is the real title, exe basename is noise. val dir = tmp.newFolder("Hades") val id = GameIdentifier.identify(exeIn(dir, "Hades-Win64-Shipping.exe")) assertEquals("Hades", id.name) @@ -115,18 +104,16 @@ class GameIdentifierTest { fun fallbackName_stripsRepackTagsAndVersionTokens() { val dir = tmp.newFolder("Stardew Valley [FitGirl Repack] v1.5.4") val id = GameIdentifier.identify(exeIn(dir, "Stardew.exe")) - assertEquals("Stardew", id.name) // exe basename wins (not an engine binary); folder tags irrelevant here - // and a repack-style exe basename is cleaned too: + assertEquals("Stardew", id.name) val id2 = GameIdentifier.identify(exeIn(tmp.newFolder("g2"), "Game.CODEX.exe")) assertEquals(GameIdentifier.Source.FILENAME, id2.source) } @Test fun fltIni_gameSettingsAppId_isParsed() { - // Real God of War (FLT release) layout: flt.ini holds the appid under [GameSettings]. val dir = tmp.newFolder("GodOfWar") File(dir, "flt.ini").writeText("[GameSettings]\nAppId=1593500\nUserName=X\nBuildId=7969425\n") - File(dir, "settings.ini").writeText("[Settings]\nVideoDevice=Wrapper\nMonitor=0\n") // no AppId -> ignored + File(dir, "settings.ini").writeText("[Settings]\nVideoDevice=Wrapper\nMonitor=0\n") val id = GameIdentifier.identify(exeIn(dir, "GoW.exe")) assertEquals(1593500, id.appId) assertEquals(GameIdentifier.Confidence.HIGH, id.confidence) @@ -141,12 +128,10 @@ class GameIdentifierTest { @Test fun bestName_prefersFileDescriptionWhenProductNameIsAbbreviation() { - // God of War ships ProductName "GoW" but FileDescription "God of War". assertEquals( "God of War", PeVersionInfo.bestName(mapOf("ProductName" to "GoW", "FileDescription" to "God of War")), ) - // Normal case: ProductName is the full title -> keep it. assertEquals( "Cyberpunk 2077", PeVersionInfo.bestName(mapOf("ProductName" to "Cyberpunk 2077", "FileDescription" to "Cyberpunk 2077")), @@ -156,18 +141,15 @@ class GameIdentifierTest { @Test fun name_isNormalizedToBeFilesystemSafeAndTidy() { - // Colon → " - " (would otherwise be sanitized to '_' by the shortcut writer). val d1 = tmp.newFolder("ds") File(d1, "goggame-1.info").writeText("""{"name":"Dark Souls: Remastered"}""") assertEquals("Dark Souls - Remastered", GameIdentifier.identify(exeIn(d1)).name) - // Trademark marks (symbol and text forms) stripped. val d2 = tmp.newFolder("gow2") File(d2, "goggame-2.info").writeText("""{"name":"God of War™"}""") assertEquals("God of War", GameIdentifier.identify(exeIn(d2)).name) val d3 = tmp.newFolder("ds3") File(d3, "goggame-3.info").writeText("""{"name":"DARK SOULS(TM): REMASTERED"}""") assertEquals("DARK SOULS - REMASTERED", GameIdentifier.identify(exeIn(d3)).name) - // Trailing PE descriptors dropped (DiRT 3's FileDescription is "DiRT 3 Executable"). val d4 = tmp.newFolder("dirt") File(d4, "goggame-4.info").writeText("""{"name":"DiRT 3 Executable"}""") assertEquals("DiRT 3", GameIdentifier.identify(exeIn(d4)).name) @@ -175,8 +157,6 @@ class GameIdentifierTest { @Test fun launcherExe_prefersFolderName_overLauncher() { - // GTA V Enhanced: user picks the Rockstar launcher PlayGTAV.exe (no Steam id, PE = junk on - // a real exe). Fallback must use the folder title, not "PlayGTAV". val dir = tmp.newFolder("Grand Theft Auto V Enhanced") val id = GameIdentifier.identify(exeIn(dir, "PlayGTAV.exe")) assertEquals("Grand Theft Auto V Enhanced", id.name) @@ -187,12 +167,11 @@ class GameIdentifierTest { fun launcherExe_withSeparator_prefersFolder_andStripsSiteTag() { val dir = tmp.newFolder("Grand-Theft-Auto-IV-AnkerGames") val id = GameIdentifier.identify(exeIn(dir, "Play - GTA IV.exe")) - assertEquals("Grand Theft Auto IV", id.name) // "AnkerGames" site tag stripped + assertEquals("Grand Theft Auto IV", id.name) } @Test fun nonLauncherExe_startingWithPlay_keepsItsOwnName() { - // "PlayerUnknown" must NOT be treated as a launcher (would wrongly use the folder name). val dir = tmp.newFolder("SomeInstallFolder") val id = GameIdentifier.identify(exeIn(dir, "PlayerUnknown.exe")) assertEquals("PlayerUnknown", id.name) @@ -211,8 +190,7 @@ class GameIdentifierTest { @Test fun emptyWhenNothingIdentifiable() { val dir = tmp.newFolder("x") - val id = GameIdentifier.identify(exeIn(dir, "Game.exe")) // engine-ish base → folder "x" - // "x" is a valid (if useless) fallback name; the point is it must not crash and appId is null. + val id = GameIdentifier.identify(exeIn(dir, "Game.exe")) assertNull(id.appId) assertTrue(id.confidence == GameIdentifier.Confidence.LOW) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/GamesFolderMigrationTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/GamesFolderMigrationTest.kt index 1d81b1af4..eb101d293 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/GamesFolderMigrationTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/GamesFolderMigrationTest.kt @@ -16,7 +16,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** Games folders from earlier builds become entries, every game keeping what it had. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class GamesFolderMigrationTest { @@ -40,7 +39,6 @@ class GamesFolderMigrationTest { StoreGameSidecar.file(store).writeText("""{"store":"gog","id":"1","title":"Store Game","exe":"Store.exe"}""") File(pc, "Notes").mkdirs() SessionPrefs.setAddedGamesDirs(app, listOf(pc.path)) - // What earlier scans had kept, by folder. SessionPrefs.setAddedGameExe(app, beta.path, pick.path) SessionPrefs.addedGameAppId(app, alpha.path, 0x8123_4567L) SessionPrefs.addedGameAppId(app, beta.path, 0x8765_4321L) @@ -63,11 +61,9 @@ class GamesFolderMigrationTest { assertEquals(0x8765_4321L, games.getValue(beta.path).appId) assertEquals("Alpha GOTY", games.getValue(alpha.path).name) assertEquals(pick.path, games.getValue(beta.path).exe.path) - // The same path inside the session as before: the shortcut keeps its target. assertEquals(AddedGames.GUEST_DIR + "/PC/Alpha/Alpha.exe", games.getValue(alpha.path).guestExe) assertEquals(AddedGames.GUEST_DIR + "/PC/Beta/bin/Beta64.exe", games.getValue(beta.path).guestExe) assertTrue(SessionPrefs.removedAddedGames(app).contains(gone.path)) - // Not a library any more. assertTrue(GameStorage.gamesFolderLibraries(app).isEmpty()) } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/SteamPicsTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/SteamPicsTest.kt index 60c6b96c5..aa05b1b7a 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/SteamPicsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/SteamPicsTest.kt @@ -17,13 +17,11 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** Steam's app info asked anonymously: the answer read, batched, kept, and asked only when needed. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class SteamPicsTest { private val app get() = RuntimeEnvironment.getApplication() - /** Dragon's Dogma 2's app info as Steam's anonymous PICS answer carried it (recorded 2026-10-10). */ private val recorded: String get() = javaClass.classLoader!!.getResource("steam-pics/app_2054970.vdf")!!.readText() @After fun tearDown() { SteamAppInfo.pics = { SteamPics.lookup(it) } } @@ -32,7 +30,6 @@ class SteamPicsTest { val info = SteamPics.infoFromKeyValues(recorded)!! assertEquals("Dragon's Dogma 2", info.name) assertEquals(listOf("228988"), info.sharedDepots) - // VC++ 2019 for the Windows components. assertEquals(listOf("vcredist2019_dll" to "VC++ 2019"), WinCompSources.steamDepots(info.sharedDepots)) } @@ -66,11 +63,9 @@ class SteamPicsTest { val calls = ArrayList<List<Int>>() SteamAppInfo.pics = { ids -> calls.add(ids); mapOf(2054970 to SteamPics.infoFromKeyValues(recorded), 7 to null) } assertTrue(SteamAppInfo.lookup(app, listOf(2054970, 7))) - // One connection for both. assertEquals(listOf(listOf(2054970, 7)), calls) assertEquals(listOf("228988"), SteamAppInfo.info(app, 2054970)!!.sharedDepots) assertNull(SteamAppInfo.info(app, 7)) - // Kept: nothing asked again (the miss waits a week). assertFalse(SteamAppInfo.lookup(app, listOf(2054970, 7))) assertEquals(1, calls.size) assertEquals("hit", SteamAppInfo.state(app, 2054970)) @@ -78,7 +73,6 @@ class SteamPicsTest { } @Test fun noNetworkWhenTheClientsCacheHasTheApp() { - // An appinfo.vdf with the app in it (built as in WinCompSourcesTest). val file = SteamAppInfo.appinfoFile(app) WinCompSourcesTest.writeAppinfo(file, 1145360) SteamAppInfo.pics = { fail("asked Steam"); null } diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/WinCompSourcesTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/WinCompSourcesTest.kt index 476428a90..78ac65445 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/WinCompSourcesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/WinCompSourcesTest.kt @@ -20,7 +20,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** Each source's list of Windows components, how it maps to the catalog, and what turns on by itself. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class WinCompSourcesTest { @@ -35,7 +34,6 @@ class WinCompSourcesTest { private fun auto(findings: List<WinCompSources.Finding>) = findings.filter { it.auto }.map { it.component to it.origin } - // ---- mapping tables ---- @Test fun gogDependencyIdsMapToComponents() { assertEquals(listOf("vcredist2015"), WinCompSources.gog("MSVC2017_x64")) @@ -62,7 +60,6 @@ class WinCompSourcesTest { assertEquals(listOf("vcredist2015_dll" to "VC++ 2015", "d3dx9" to "DirectX (June 2010)"), WinCompSources.steamDepots(listOf("228986", "228990", "999"))) } - // ---- per source ---- @Test fun steamTakesItsSharedDepotsAndTurnsThemOn() { val steamapps = tmp.newFolder("steamapps") @@ -80,7 +77,6 @@ class WinCompSourcesTest { sidecar(folder, Store.GOG, mapOf(WinCompSources.GOG_DEPENDENCIES to "MSVC2017_x64,DirectX,ISI")) val findings = WinCompSources.forGame(app, game(folder, "gog")) assertEquals(listOf("vcredist2015" to WinCompSources.Origin.GOG, "d3dx9" to WinCompSources.Origin.GOG), auto(findings)) - // ISI is none of ours: a suggestion under its own name. assertEquals(listOf("ISI"), findings.filter { it.component == null }.map { it.original }) } @@ -105,12 +101,10 @@ class WinCompSourcesTest { assertEquals(listOf("Activate.exe"), findings.filter { it.component == null }.map { it.original }) } - // ---- Custom: certainty decides ---- private fun appinfo(file: File, appId: Int) = writeAppinfo(file, appId) companion object { - /** A version 29 appinfo.vdf with one app, whose depots point at two Steamworks Shared ones. */ fun writeAppinfo(file: File, appId: Int) { val strings = listOf("appinfo", "common", "name", "depots", "228986", "depotfromapp", "228990", "1001", "maxsize") fun key(s: String) = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(strings.indexOf(s)).array() @@ -173,7 +167,6 @@ class WinCompSourcesTest { assertEquals(listOf("vcredist2013" to WinCompSources.Origin.FOLDER), findings.map { it.component to it.origin }) } - // ---- what turns on, and the user's switches ---- private fun ready(name: String) = WinComponents.Component(name, "", "", "ready", emptyList(), listOf(WinComponents.Step("override_dll", JSONObject().put("dll", "x").put("type", "native")))) @@ -187,7 +180,6 @@ class WinCompSourcesTest { WinCompSources.Finding("d3dx9", "d3dx9_43.dll", WinCompSources.Origin.FOLDER, false), ) val picks = AutoComponents.autoPicks(findings, catalog) - // The detector's name installs as its DLL twin; physx cannot install; the folder scan only recommends. assertEquals(listOf("vcredist2015_dll"), picks.keys.toList()) assertEquals("GOG", picks.getValue("vcredist2015_dll").kind) } @@ -199,11 +191,9 @@ class WinCompSourcesTest { WinComponents.setUser(app, key, "d3dx9", false) WinComponents.setUser(app, key, "oalinst_dll", true) assertEquals(listOf("xact", "oalinst_dll"), WinComponents.picks(app, key)) - // A new automatic list keeps the user's switches. WinComponents.setAuto(app, key, mapOf("d3dx9" to WinComponents.AutoReason("GOG", listOf("DirectX")))) assertEquals(listOf("oalinst_dll"), WinComponents.picks(app, key)) assertEquals(mapOf("d3dx9" to false, "oalinst_dll" to true), WinComponents.selection(app, key).user) - // What the launch reads stays version 1 with the effective list. val text = File(app.filesDir, "wincomponents.json").readText() assertEquals(1, JSONObject(text).getInt("version")) assertEquals("oalinst_dll", JSONObject(text).getJSONObject("games").getJSONArray(key).getString(0)) @@ -219,10 +209,8 @@ class WinCompSourcesTest { val folder = tmp.newFolder("Shared") val exe = File(folder, "Shared.exe").apply { writeText("x") } val art = AddedGames.Game(folder, "Shared", exe, "/g/Shared.exe", "/g", 0x81234567L, 0L, emptyList()) - // The art fetch finds it by name ... val match = AddedGameArt.steamMatch(app, art) { 4242 } assertEquals(4242, match.appId) - // ... and the components read the same record. assertEquals(4242, SteamMatch.get(app, folder.path)!!.appId) assertEquals(SteamMatch.Certainty.NAME, SteamMatch.get(app, folder.path)!!.certainty) assertFalse(WinCompSources.forGame(app, game(folder, Library.ADDED)).any { it.auto }) @@ -268,7 +256,6 @@ class WinCompSourcesTest { @Test fun aSteamListedComponentNotYetDownloadedTurnsOnWhenItLands() = withFakeWorker { installedNow.add("d3dx9") val game = payback() - // The user had switched one off: it is never downloaded for an automatic pick. WinComponents.setUser(app, "1262580", "vcredist2019_dll", false) AutoComponents.queueGame(app, game, asked = true) assertEquals(listOf("vcredist2013_dll"), downloaded) @@ -276,7 +263,6 @@ class WinCompSourcesTest { assertEquals(setOf("d3dx9", "vcredist2013_dll", "vcredist2019_dll"), sel.auto.keys) assertEquals(setOf("d3dx9", "vcredist2013_dll"), WinComponents.picks(app, "1262580").toSet()) assertEquals("STEAM", sel.auto.getValue("vcredist2013_dll").kind) - // The list is kept for the page. assertEquals(3, AutoComponents.cachedFindings(app, "1262580")!!.count { it.auto }) } @@ -297,7 +283,6 @@ class WinCompSourcesTest { AutoComponents.unfavourable = { true } AutoComponents.queueGame(app, game, asked = false) assertTrue(downloaded.isEmpty()) - // The user opens the page: asked for, it goes. AutoComponents.queueGame(app, game, asked = true) assertEquals(3, downloaded.size) } @@ -311,7 +296,6 @@ class WinCompSourcesTest { assertEquals(listOf("g:1262580"), AutoComponents.queued()) session = false AutoComponents.drain(app) - // Each component once, though two games could want it. assertEquals(downloaded.distinct(), downloaded) } @@ -319,7 +303,6 @@ class WinCompSourcesTest { val game = payback() AutoComponents.online = { false } AutoComponents.queueGame(app, game, asked = true) - // The list is worked out offline; the downloads wait. assertTrue(downloaded.isEmpty()) assertTrue(AutoComponents.queued().all { it.startsWith("c:") }) AutoComponents.online = { true } @@ -327,7 +310,6 @@ class WinCompSourcesTest { assertEquals(3, downloaded.size) } - // ---- older store installs: the list read once from the store ---- @Test fun anOlderGogInstallGetsItsListFromTheStoreOnce() { val folder = tmp.newFolder("DOOM I Enhanced") @@ -338,7 +320,6 @@ class WinCompSourcesTest { try { assertTrue(StoreListBackfill.fill(app, folder)) assertEquals("MSVC2019_x64,DirectX", StoreGameSidecar.read(folder)!!.extra[WinCompSources.GOG_DEPENDENCIES]) - // Kept like a fresh install: not fetched again. assertFalse(StoreListBackfill.fill(app, folder)) assertEquals(1, fetches) assertEquals(listOf("vcredist2019" to WinCompSources.Origin.GOG, "d3dx9" to WinCompSources.Origin.GOG), auto(WinCompSources.forGame(app, game(folder, "gog")))) @@ -377,7 +358,6 @@ class WinCompSourcesTest { AutoComponents.online = { true } assertFalse(StoreListBackfill.fill(app, folder)) assertFalse(StoreListBackfill.fill(app, folder)) - // Signed out or the store refused: once, then a week's rest; the sidecar is untouched. assertEquals(1, fetches) assertTrue(StoreListBackfill.needs(StoreGameSidecar.read(folder)!!)) } finally { @@ -386,16 +366,13 @@ class WinCompSourcesTest { } } - // ---- apps the client never saw ---- @Test fun anAppTheClientLearnsLaterIsReadOnceItsCacheIsWritten() { val file = SteamAppInfo.appinfoFile(app) appinfo(file, 1) file.setLastModified(1_000_000_000_000L) assertEquals(null, SteamAppInfo.info(app, 1145360)) - // Unchanged file: the miss stands (no re-read). assertEquals(null, SteamAppInfo.info(app, 1145360)) - // The client wrote its cache with the app in it: read at once, not a week later. appinfo(file, 1145360) file.setLastModified(1_000_000_100_000L) assertEquals(listOf("228986", "228990"), SteamAppInfo.info(app, 1145360)!!.sharedDepots) @@ -418,7 +395,6 @@ class WinCompSourcesTest { assertTrue(StoreListBackfill.needs(StoreGameSidecar.read(older)!!)) } - // ---- a game installed during the session ---- private fun manifest(dir: File, appId: Int, state: Int, time: Long) = File(dir, "appmanifest_$appId.acf").apply { @@ -431,7 +407,6 @@ class WinCompSourcesTest { manifest(steamapps, 12900, 1026, 1_000_000L) manifest(steamapps, 570, 4, 1_000_000L) val watch = SessionInstalls.Watch { listOf(steamapps) } - // What was there when the session started is not new. assertTrue(watch.poll().isEmpty()) assertTrue(watch.poll().isEmpty()) manifest(steamapps, 12900, 4, 2_000_000L) @@ -448,14 +423,12 @@ class WinCompSourcesTest { gameUp = true AutoComponents.queueInSession(app, installed) assertTrue(downloaded.isEmpty()) - // Other work stays paused in the session. val other = game(tmp.newFolder("Other"), Library.ADDED) AutoComponents.queueGame(app, other, asked = false) gameUp = false AutoComponents.drain(app) assertEquals(3, downloaded.size) assertEquals(setOf("d3dx9", "vcredist2013_dll", "vcredist2019_dll"), WinComponents.picks(app, "1262580").toSet()) - // Nothing left pending for the launch to wait on; the other game still waits for the session's end. assertEquals(0, AutoComponents.Pending.read(app).length()) assertEquals(listOf("g:${AutoComponents.appKey(other)}"), AutoComponents.queued()) session = false diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/CardboardTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/CardboardTest.kt index c388569cb..a54bc216f 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/CardboardTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/CardboardTest.kt @@ -21,7 +21,6 @@ class CardboardTest { private fun assertVec(expected: FloatArray, actual: FloatArray) = assertArrayEquals(expected, actual, eps) - /** Rotation about a unit axis by an angle, as a quaternion (x, y, z, w). */ private fun quat(ax: Float, ay: Float, az: Float, deg: Double): FloatArray { val h = deg * PI / 360.0 val s = sin(h).toFloat() @@ -43,11 +42,8 @@ class CardboardTest { } @Test fun lensInsetLinesTheEyesUpWithTheLenses() { - // A 128 mm panel with 64 mm lenses: each viewport centre is 32 mm out, exactly under its lens. assertEquals(0f, Cardboard.lensInset(128f, 64f), eps) - // A wider 150 mm panel: viewport centres 37.5 mm out, lenses 32 mm: 5.5 mm of a 75 mm viewport inwards. assertEquals(5.5f / 75f, Cardboard.lensInset(150f, 64f), eps) - // A narrow panel puts the lens centres outside the viewport centres (negative inset). assertEquals(true, Cardboard.lensInset(110f, 64f) < 0f) assertEquals(0f, Cardboard.lensInset(0f, 64f), eps) assertEquals(0f, Cardboard.lensInset(150f, 0f), eps) @@ -62,7 +58,6 @@ class CardboardTest { assertEquals(SteamVr.Status.READY, SteamVr.parse(answer("ok" to true, "installed" to true, "driver" to true))) assertEquals(SteamVr.Status.RUNNING, SteamVr.parse(answer("ok" to true, "installed" to true, "driver" to true, "running" to true))) - // The helper itself failed: the agent still says ok. assertEquals(SteamVr.Status.UNAVAILABLE, SteamVr.parse(answer("ok" to true, "installed" to true, "driver" to true).put("exitCode", 1))) } @@ -75,7 +70,6 @@ class CardboardTest { } assertTrue(SteamVr.succeeded("start", answer(true, exitCode = 0))) assertTrue(SteamVr.succeeded("stop", answer(true, exitCode = 0))) - // The agent ran the request, but droiddeck-steamvr start failed (not installed, no driver). assertFalse(SteamVr.succeeded("start", answer(true, exitCode = 1))) assertFalse(SteamVr.succeeded("stop", answer(true, exitCode = 2))) assertFalse(SteamVr.succeeded("start", answer(true))) @@ -86,19 +80,14 @@ class CardboardTest { } @Test fun quaternionMatchesKnownRotations() { - // 90 degrees about z: x -> y. val m = matrix(quat(0f, 0f, 1f, 90.0)) assertVec(floatArrayOf(0f, 1f, 0f), HeadPose.apply(m, floatArrayOf(1f, 0f, 0f))) - // Rotation vector without w: computed from the other three. val q = HeadPose.quaternionFromRotationVector(floatArrayOf(0f, 0f, sin(PI / 4).toFloat())) assertEquals(cos(PI / 4).toFloat(), q[3], eps) } @Test fun recenterMakesTheCurrentViewStraightAhead() { - // Phone upright in landscape, screen facing the viewer, who faces north-east-ish. - // Upright device: screen up = world up; looking direction = the screen's -z. for (heading in listOf(0.0, 37.0, 140.0, -100.0)) { - // Start from "screen faces south, viewer looks north": device x = east, y = up, z = south. val upright = floatArrayOf(1f, 0f, 0f, 0f, 0f, -1f, 0f, 1f, 0f) val yaw = matrix(quat(0f, 0f, 1f, heading)) val screenToWorld = HeadPose.multiply(yaw, upright) @@ -108,7 +97,6 @@ class CardboardTest { @Test fun recenterKeepsPitchButDropsHeading() { val upright = floatArrayOf(1f, 0f, 0f, 0f, 0f, -1f, 0f, 1f, 0f) - // Looking 30 degrees down, then turning 50 degrees: after a recentre the view is still 30 degrees down. val pitched = HeadPose.multiply(upright, matrix(quat(1f, 0f, 0f, -30.0))) val turned = HeadPose.multiply(matrix(quat(0f, 0f, 1f, 50.0)), pitched) val pose = HeadPose.relative(HeadPose.recenter(turned), turned) @@ -121,23 +109,18 @@ class CardboardTest { @Test fun turningTheHeadTurnsTheView() { val upright = floatArrayOf(1f, 0f, 0f, 0f, 0f, -1f, 0f, 1f, 0f) val ref = HeadPose.recenter(upright) - // Turn the head 90 degrees to the left (counter-clockwise seen from above). val turned = HeadPose.multiply(matrix(quat(0f, 0f, 1f, 90.0)), upright) val forward = HeadPose.apply(HeadPose.relative(ref, turned), floatArrayOf(0f, 0f, -1f)) - // The view now points to the left of the recentred forward direction: -x. assertVec(floatArrayOf(-1f, 0f, 0f), forward) } @Test fun heldFlatTheViewStartsStraightAhead() { - // Screen facing up (device on a table, held like a tray, or lying back looking up at it). for (m in listOf(HeadPose.IDENTITY, HeadPose.multiply(matrix(quat(0f, 0f, 1f, 30.0)), matrix(quat(1f, 0f, 0f, 20.0))))) { assertVec(HeadPose.IDENTITY, HeadPose.relative(HeadPose.recenter(m), m)) } } @Test fun heldFlatTurningAtTheHipsPans() { - // Held flat, then the body turns 40 degrees to the left about the real vertical: the view - // turns left about its own up axis - it must not roll (the bug: yaw came out as a tilt). val flat = HeadPose.IDENTITY val ref = HeadPose.recenter(flat) val turned = HeadPose.multiply(matrix(quat(0f, 0f, 1f, 40.0)), flat) @@ -146,7 +129,6 @@ class CardboardTest { val up = HeadPose.apply(pose, floatArrayOf(0f, 1f, 0f)) assertVec(floatArrayOf(-sin(PI * 40 / 180).toFloat(), 0f, -cos(PI * 40 / 180).toFloat()), forward) assertVec(floatArrayOf(0f, 1f, 0f), up) - // Tipping the top edge down (pitching the tray towards you) looks down. val tipped = HeadPose.multiply(flat, matrix(quat(1f, 0f, 0f, -20.0))) val down = HeadPose.apply(HeadPose.relative(ref, tipped), floatArrayOf(0f, 0f, -1f)) assertEquals(-sin(PI / 9).toFloat(), down[1], eps) @@ -154,7 +136,6 @@ class CardboardTest { @Test fun fieldOfViewComesFromThePanelSeenThroughTheLens() { val v2 = Cardboard.Viewer.CARDBOARD_V2 - // A 74.7 mm tall panel 39.3 mm behind the lens: atan(37.35 / 39.3) = 43.5 degrees each way. assertEquals(87.07f, Cardboard.fovDeg(v2, 74.7f), 0.05f) assertEquals(v2.fallbackFovDeg, Cardboard.fovDeg(v2, 0f), eps) assertEquals(Cardboard.Viewer.NONE.fallbackFovDeg, Cardboard.fovDeg(Cardboard.Viewer.NONE, 74.7f), eps) @@ -165,11 +146,9 @@ class CardboardTest { val n = sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]); floatArrayOf(q[0] / n, q[1] / n, q[2] / n, q[3] / n) }) assertVec(r, HeadPose.toScreen(r, 0)) - // ROTATION_90 (as input/PadMotion maps it): screen x is the device's -y, screen y the device's x. val s90 = HeadPose.toScreen(r, 1) assertVec(HeadPose.apply(r, floatArrayOf(0f, -1f, 0f)), HeadPose.apply(s90, floatArrayOf(1f, 0f, 0f))) assertVec(HeadPose.apply(r, floatArrayOf(1f, 0f, 0f)), HeadPose.apply(s90, floatArrayOf(0f, 1f, 0f))) - // ROTATION_270 is the opposite turn; four quarter turns of 90 give the original back. val s270 = HeadPose.toScreen(r, 3) assertVec(HeadPose.apply(r, floatArrayOf(0f, 1f, 0f)), HeadPose.apply(s270, floatArrayOf(1f, 0f, 0f))) assertVec(r, HeadPose.toScreen(HeadPose.toScreen(HeadPose.toScreen(HeadPose.toScreen(r, 1), 1), 1), 1)) diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/DriverBundleTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/DriverBundleTest.kt index 434e87d09..6082d3ddf 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/DriverBundleTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/DriverBundleTest.kt @@ -34,7 +34,6 @@ class DriverBundleTest { SessionPrefs.setLinuxDriver(context, "") } - /** Enough of an AArch64 ELF for the importers: the header they read, then the libc they look for. */ private fun library(libc: String) = ByteArray(64).also { byteArrayOf(0x7f, 'E'.code.toByte(), 'L'.code.toByte(), 'F'.code.toByte(), 2, 1).copyInto(it) it[18] = 0xB7.toByte() diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairsTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairsTest.kt index b5d632fdc..ba2a53f4e 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/DriverPairsTest.kt @@ -60,7 +60,6 @@ class DriverPairsTest { listOf( asset("Turnip-r4.zip", "r4", false, DriverPairs.BANNER), asset("Turnip-r4-Linux.zip", "r4", true, DriverPairs.BANNER), - // The A8xx set's newest display build is r4, but its Linux half is only in r3. asset("Turnip-r4-A8xx.zip", "r4", false, DriverPairs.BANNER_A8XX), asset("Turnip-r3-A8xx-Linux.zip", "r3", true, DriverPairs.BANNER_A8XX), asset("WN-Turnip-1.19-b_Axxx.zip", "v1.19", false, DriverPairs.WN_BALANCED), diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt index 10a0fd971..0ce3bdc6c 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt @@ -5,7 +5,6 @@ import org.junit.Test class GpuModelFallbackTest { @Test fun aPlatformCodeNameGivesItsAdreno() { - // AYANEO's Pocket FIT: KGSL says "Adreno33v2", the platform says pineapple. assertEquals(750, GpuInfo.platformModel("pineapple")) assertEquals(740, GpuInfo.platformModel("KALAMA")) assertEquals(830, GpuInfo.platformModel("sun")) diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt index d90e261d0..8919b79a7 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt @@ -8,7 +8,6 @@ class SocNamesTest { private fun props(vararg pairs: Pair<String, String>): (String) -> String = { pairs.toMap()[it].orEmpty() } @Test fun theModelIsFoundWhereVendorsPutIt() { - // AYANEO Pocket FIT: ro.soc.model blank, the vendor property set, ro.fota.platform a branch name. assertEquals("SG8350P", SocNames.model(props("ro.vendor.qti.soc_model" to "SG8350P", "ro.fota.platform" to "MSM_13.0"))) assertEquals("QCS8550", SocNames.model(props("ro.soc.model" to "QCS8550", "ro.vendor.qti.soc_model" to "other"))) assertEquals("", SocNames.model(props("ro.fota.platform" to "MSM_13.0"))) diff --git a/app/src/test/java/com/droiddeck/launcher/input/FakeInputRingPathTest.kt b/app/src/test/java/com/droiddeck/launcher/input/FakeInputRingPathTest.kt index d1d2b8d49..e0199e6ca 100644 --- a/app/src/test/java/com/droiddeck/launcher/input/FakeInputRingPathTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/input/FakeInputRingPathTest.kt @@ -7,12 +7,6 @@ import org.junit.Assert.* import org.junit.Before import org.junit.Test -/** - * The ring paths handed to the guest (FAKE_EVDEV_MEMFD_PATHS) must keep the spelling the session - * tree is bound under. On ColorOS 16 /data/user/0 resolves to /data/data, the guest only has the - * first bound, and a canonical export left every pad dead (#166). A symlinked directory stands in - * for that here. - */ class FakeInputRingPathTest { private lateinit var base: File private lateinit var real: File @@ -37,7 +31,6 @@ class FakeInputRingPathTest { @Test fun theExportKeepsTheBoundSpellingThroughALinkedDataDirectory() { val fakeInputDir = File(link, "files/session/dev/input").apply { mkdirs() } val ring = FakeInputRingPaths.ringFile(fakeInputDir, 0)!!.apply { parentFile!!.mkdirs(); writeText("") } - // The link really does resolve elsewhere, as /data/user/0 does on the affected devices. assertTrue(ring.canonicalPath.startsWith(real.canonicalPath + "/")) val exported = FakeInputRingPaths.exportPath(ring) assertEquals(File(link, "files/session/dev/fakeinput-rings/ring0").path, exported) diff --git a/app/src/test/java/com/droiddeck/launcher/input/PadBridgeRoutingTest.kt b/app/src/test/java/com/droiddeck/launcher/input/PadBridgeRoutingTest.kt index f3f0e64b0..8ff62e170 100644 --- a/app/src/test/java/com/droiddeck/launcher/input/PadBridgeRoutingTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/input/PadBridgeRoutingTest.kt @@ -148,8 +148,6 @@ class PadBridgeRoutingTest { private companion object { const val COMPOSITE = 0x1002313 } - // Robolectric's default InputEvent shadow has its own nullable device field. Use Android's - // original lookup so key and motion events resolve the registered device by id. @Implements(KeyEvent::class) class RegisteredKeyEvent : ShadowKeyEvent() { @RealObject private lateinit var event: KeyEvent diff --git a/app/src/test/java/com/droiddeck/launcher/input/SteamTouchConfigTest.kt b/app/src/test/java/com/droiddeck/launcher/input/SteamTouchConfigTest.kt index 51c6742cd..8addf24e0 100644 --- a/app/src/test/java/com/droiddeck/launcher/input/SteamTouchConfigTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/input/SteamTouchConfigTest.kt @@ -6,7 +6,6 @@ import org.junit.Assert.assertTrue import org.junit.Test class SteamTouchConfigTest { - // A layout DroidDeck saved for Geometry Wars on the Thor, as Steam keeps it. private val saved = "0aec0310012218080010011d0000703d255555d53d2d0000803f350000803f2218080110011d6666fe3e255555d53d2d0000803f350000803f2218080210011dba490c3e255c8f423f2d0000803f350000803f2218080410011d9a99593f255c8f423f2d0000803f350000803f2218080510011d8fc2353f25ae47613f2d0000803f350000803f2218080610011d2b87163e25d7a3f03e2d0000803f350000803f2218080710011dba49003f25f958053f2d293ab23f35293ab23f2218080810011d9a99683f25d227dd3e2d0000803f350000803f2218080910011d9a99483f25d227dd3e2d0000803f350000803f2218080a10011d9a99583f254444a43e2d0000803f350000803f2218080b10011d6666ce3e255555d53d2d0000803f350000803f2218080c10011d3333173f255555d53d2d0000803f350000803f2218080d10011d0ad7233e2585eb513e2d0000803f350000803f2218080e10011d3d0a573f2585eb513e2d0000803f350000803f2218080f10011d8fc2753d2585eb513e2d0000803f350000803f2218081010011dd7a3703f2585eb513e2d0000803f350000803f2218081c10011d0000713f255555d53d2d0000803f350000803f2218080310001de17a943e25ae47613f2d0000803f350000803f2a140d0000803f150000803f1d0000803f25cdcccc3e1002" @Test diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/AppImageManagerTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/AppImageManagerTest.kt index 5f753715f..ab620b385 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/AppImageManagerTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/AppImageManagerTest.kt @@ -16,7 +16,6 @@ class AppImageManagerTest { @Before fun setUp() { dir = Files.createTempDirectory("appimage").toFile() } @After fun tearDown() { dir.deleteRecursively() } - /** An ELF header with the AppImage magic at byte 8 and [machine] at byte 18. */ private fun image(machine: Int, type: Int = 2, magic: Boolean = true): File { val b = ByteArray(64) b[0] = 0x7f; b[1] = 'E'.code.toByte(); b[2] = 'L'.code.toByte(); b[3] = 'F'.code.toByte() diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/BwrapSpawnerTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/BwrapSpawnerTest.kt index e1e9fd9e9..e1e6827ab 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/BwrapSpawnerTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/BwrapSpawnerTest.kt @@ -68,7 +68,6 @@ class BwrapSpawnerTest { } } -/** The sandbox a Flatpak request describes, built on disk under a temporary directory. */ @RunWith(RobolectricTestRunner::class) class BwrapSpawnerPlanTest { private lateinit var dir: File @@ -76,7 +75,6 @@ class BwrapSpawnerPlanTest { @Before fun setUp() { dir = Files.createTempDirectory("bwrap-plan").toFile() - // Flatpak names the runtime by its path inside the rootfs. runtime = File(LinuxRuntime.rootDir(RuntimeEnvironment.getApplication()), "runtime-files") File(runtime, "lib").mkdirs() } @@ -107,18 +105,14 @@ class BwrapSpawnerPlanTest { op("t" to "bind", "src" to "/nonexistent-${System.nanoTime()}", "dest" to "/run/x", "try" to true), ), dir) - // The runtime is bound at /usr, not written into. assertTrue(plan.binds.any { it.guest == "/usr" && File(it.host).canonicalPath == runtime.canonicalPath }) assertFalse(File(runtime, "lib/GL").exists()) - // The tmpfs under /usr is a directory of ours, and the link Flatpak makes inside it lands there. val gl = plan.binds.single { it.guest == "/usr/lib/GL" } assertTrue(gl.host.startsWith(dir.path)) assertEquals("default/lib/libEGL.so", Files.readSymbolicLink(File(gl.host, "libEGL.so").toPath()).toString()) - // Top-level links, directories and files are made in the sandbox root itself. assertEquals("usr/lib", Files.readSymbolicLink(File(plan.root, "lib").toPath()).toString()) assertTrue(File(plan.root, "tmp").isDirectory) assertEquals("[Application]\nname=org.example.Game\n", File(plan.root, ".flatpak-info").readText()) - // A --bind-try whose source is missing is left out. assertFalse(plan.binds.any { it.guest == "/run/x" }) assertEquals(listOf("/app/bin/game"), plan.argv) assertEquals("/app", plan.cwd) diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/DeckyPluginInstallTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/DeckyPluginInstallTest.kt index 5edb3451d..080828ba9 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/DeckyPluginInstallTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/DeckyPluginInstallTest.kt @@ -69,7 +69,6 @@ class DeckyPluginInstallTest { val plugins = temporary.newFolder() val old = plugin(plugins, "Unifideck", "Unifideck") File(old, "old-version").writeText("keep") - // An empty staged directory makes the plugin activation fail after backing up the old one. val staged = plugin(temporary.newFolder(), "Unifideck", "Unifideck") staged.deleteRecursively() val settings = File(temporary.newFolder(), "loader.json").apply { writeText("{}") } diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/ProotFastPathTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/ProotFastPathTest.kt index ce129f7c0..5d4eb1d8e 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/ProotFastPathTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/ProotFastPathTest.kt @@ -9,7 +9,6 @@ class ProotFastPathTest { private val root = File("/data/data/com.droiddeck.launcher/files/linuxfs") @Test fun aSessionsUsualBindsGetAKey() { - // 87 on a device with two Games folders: past the old limit of 64, which dropped them. val binds = (1..87).map { "/host/$it:/guest/$it" } assertNotNull(ProotFastPath.key(root, binds)) } diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalTest.kt index 178e34d29..318e6b980 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalTest.kt @@ -107,7 +107,6 @@ class RuntimeRemovalTest { follower.join(5000) assertEquals(true, result.get()) assertEquals(true, followerResult.get()) - // The last stage is "Linux runtime removed", the only one reported at 100. assertEquals(100, percents.last()) assertFalse(root.exists()) assertFalse(File(dir, "linuxfs.removing").exists()) @@ -131,7 +130,6 @@ class RuntimeRemovalTest { val root = runtime() try { val removal = LinuxRuntimeInstaller.beginUninstall(null, root)!! - // The first stage names the removal, the second counts its first entry. var stages = 0 assertFalse(removal.run { _, _ -> if (++stages == 2) @@ -170,12 +168,10 @@ class RuntimeRemovalTest { assertNull(LinuxRuntimeInstaller.removalError()) assertFalse(root.exists()) assertFalse(File(dir, "linuxfs.removing").exists()) - // Only the undeletable entry and its parents remain, out of the way. val trash = File(dir, LinuxRuntimeInstaller.TRASH) assertEquals(listOf(".steampath"), trash.walk().filter { it.isFile }.map { it.name }.toList()) assertEquals(setOf(LinuxRuntimeInstaller.TRASH), dir.list()!!.toSet()) - // Once it can be deleted, the next removal clears the trash too. RuntimeFileTree.refuseForTest = null assertTrue(LinuxRuntimeInstaller.beginUninstall(null, root)!!.run(null)) assertFalse(trash.exists()) diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalUiTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalUiTest.kt index 748549560..8f690302d 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalUiTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/RuntimeRemovalUiTest.kt @@ -35,7 +35,6 @@ class RuntimeRemovalUiTest { try { val activity = Robolectric.buildActivity(SessionActivity::class.java).create() assertTrue(activity.get().isFinishing) - // Even lifecycle callbacks delivered after finish must not touch uninitialised UI. activity.start().resume().pause().stop().destroy() assertFalse(SessionState.running) assertEquals(SessionPhase.IDLE, SessionState.phase) diff --git a/app/src/test/java/com/droiddeck/launcher/session/BatteryComponentTest.kt b/app/src/test/java/com/droiddeck/launcher/session/BatteryComponentTest.kt index fd820e22b..05c17988e 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/BatteryComponentTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/BatteryComponentTest.kt @@ -61,7 +61,6 @@ class BatteryComponentTest { val discovered = supplies.listFiles()!!.filter { it.name.contains("BAT") } assertEquals(listOf("BAT1"), discovered.map { it.name }) fun read(name: String) = File(discovered.single(), name).readText().trim().toLong() - // One 4 Ah battery at 4 V drawing 2 A: 8 W and two hours, regardless of vendor current sign. assertEquals(8_000_000L, read("power_now")) assertEquals(16_000_000L, read("energy_now")) assertEquals(7200L, read("time_to_empty_now")) diff --git a/app/src/test/java/com/droiddeck/launcher/session/GameEnvironmentStoreTest.kt b/app/src/test/java/com/droiddeck/launcher/session/GameEnvironmentStoreTest.kt index d499437e0..6645c6ee1 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/GameEnvironmentStoreTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/GameEnvironmentStoreTest.kt @@ -60,7 +60,6 @@ class GameEnvironmentStoreTest { "d3d9.samplerAnisotropy = 16; d3d11.samplerAnisotropy = 16; d3d9.samplerLodBias = -0.58; d3d11.samplerLodBias = -0.58", json.getString(GameEnvironmentStore.DXVK_CONFIG), ) - // The user's own entry is untouched; the launcher appends the options at launch. assertEquals("dxvk.tearFree = True", json.getJSONObject("shared").getString("DXVK_CONFIG")) assertEquals(GameEnvironment.Config(shared = mapOf("DXVK_CONFIG" to "dxvk.tearFree = True")), GameEnvironmentStore.read(context)) SessionPrefs.setTextureAnisotropy(context, 0) diff --git a/app/src/test/java/com/droiddeck/launcher/session/ProtonExtrasTest.kt b/app/src/test/java/com/droiddeck/launcher/session/ProtonExtrasTest.kt index e551d3052..a2a528f1e 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/ProtonExtrasTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/ProtonExtrasTest.kt @@ -102,7 +102,6 @@ class ProtonExtrasTest { assertTrue(ProtonExtras.compareVersions("DroidDeck-Proton 11-7", "DroidDeck-Proton 11-7.1") < 0) assertTrue(ProtonExtras.compareVersions("cachyos-11.0-20260703-slr", "cachyos-11.0-20261005-slr") < 0) assertTrue(ProtonExtras.compareVersions("cachyos-10.0-sunset-slr", "cachyos-11.0-20260601-slr") < 0) - // The architecture in a folder name is not part of its version. assertEquals(listOf(11L, 7L), ProtonExtras.versionKey("GE-Proton11-7-aarch64")) assertEquals(listOf(11L, 0L, 20261005L), ProtonExtras.versionKey("proton-cachyos-11.0-20261005-slr-arm64")) assertEquals(listOf(11L, 7L, 5L), ProtonExtras.versionKey("DroidDeck-Proton 11-7.5")) @@ -114,9 +113,8 @@ class ProtonExtrasTest { val tools = temp.newFolder("compatibilitytools.d") for (name in listOf("GE-Proton11-10", "GE-Proton11-9", "DroidDeck-Proton 11-7.10", "DroidDeck-Proton 11-7.5", "droiddeck-proton-arm64", "droiddeck-proton-11-arm64", "proton-cachyos-11.0-20261005-slr-arm64")) installTool(tools, name) - File(tools, "DroidDeck-Proton 11-6").mkdirs() // no manifest: half unpacked, not installed + File(tools, "DroidDeck-Proton 11-6").mkdirs() assertEquals(listOf("GE-Proton11-9", "GE-Proton11-10"), ProtonExtras.installedIn(tools, tool("ge"))) - // Ours, not DroidDeck's own droiddeck-proton-* tools: the prefix is matched with its case. assertEquals(listOf("DroidDeck-Proton 11-7.5", "DroidDeck-Proton 11-7.10"), ProtonExtras.installedIn(tools, tool("droiddeck"))) assertEquals(listOf("proton-cachyos-11.0-20261005-slr-arm64"), ProtonExtras.installedIn(tools, tool("cachyos"))) assertEquals(emptyList<String>(), ProtonExtras.installedIn(File(tools, "missing"), tool("ge"))) @@ -149,13 +147,11 @@ class ProtonExtrasTest { .put("cachyos", JSONObject().put("tag", "cachyos-11.0-20261005-slr").put("at", now - 60_000)) .put("droiddeck", JSONObject().put("tag", "DroidDeck-Proton-11-7.6").put("at", now - ProtonExtras.CHECK_EVERY_MS + 60_000)) File(context.filesDir, "proton-latest.json").writeText(latest.toString()) - // Every source was looked up within the last six hours: nothing goes to GitHub. assertFalse(ProtonExtras.checkLatest(context, now = now)) assertEquals("DroidDeck-Proton-11-7.6", ProtonExtras.update(context, tool("droiddeck"))) assertNull(ProtonExtras.update(context, tool("ge"))) assertNull(ProtonExtras.update(context, tool("cachyos"))) assertEquals(listOf("DroidDeck-Proton 11-7.5"), ProtonExtras.installedTags(context, tool("droiddeck"))) - // The tag recorded at install stands for the folder. File(context.filesDir, "proton-extras.json").writeText(JSONObject().put("DroidDeck-Proton 11-7.5", "DroidDeck-Proton-11-7.6").toString()) assertEquals(listOf("DroidDeck-Proton-11-7.6"), ProtonExtras.installedTags(context, tool("droiddeck"))) assertNull(ProtonExtras.update(context, tool("droiddeck"))) @@ -168,7 +164,6 @@ class ProtonExtrasTest { for (name in listOf("DroidDeck-Proton 11-7.5", "DroidDeck-Proton 11-7.6", "DroidDeck-Proton 11-7.10", "GE-Proton11-7")) installTool(tools, name) ProtonExtras.remove(context, tool("droiddeck"), "DroidDeck-Proton 11-7.6") assertEquals(listOf("DroidDeck-Proton 11-7.5", "DroidDeck-Proton 11-7.10"), ProtonExtras.installedAll(context, tool("droiddeck"))) - // A folder of another source is never removed through this one. ProtonExtras.remove(context, tool("droiddeck"), "GE-Proton11-7") assertTrue(File(tools, "GE-Proton11-7").isDirectory) assertEquals(listOf("DroidDeck-Proton 11-7.5", "DroidDeck-Proton 11-7.10"), ProtonExtras.installedAll(context, tool("droiddeck"))) diff --git a/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt b/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt index 3f19a2a2f..add675539 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/QuestExperimentalTest.kt @@ -36,7 +36,6 @@ class QuestExperimentalTest { @Test fun quest2SelectsA6xxInsteadOfQuest3DriverAndPreservesLaterChoices() { ShadowBuild.setModel("Quest 2") - // A prior Quest 3 flag must not suppress Quest 2's own migration. context.getSharedPreferences("session", Context.MODE_PRIVATE).edit() .putBoolean("questDefaultsV1", true).commit() QuestExperimental.initialize(context) diff --git a/app/src/test/java/com/droiddeck/launcher/session/RumbleComponentTest.kt b/app/src/test/java/com/droiddeck/launcher/session/RumbleComponentTest.kt index 55bb4f0d1..b388c1a53 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/RumbleComponentTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/RumbleComponentTest.kt @@ -164,7 +164,6 @@ class RumbleComponentTest { } private fun effect(strong: Int = 65535, weak: Int = 65535, slot: Int = 0) { - // Inject a decoded force-feedback packet; transport is outside these tests. val int = Int::class.javaPrimitiveType RumbleComponent::class.java.getDeclaredMethod("buzz", int, int, int, int) .apply { isAccessible = true }.invoke(rumble, strong, weak, 5000, slot) diff --git a/app/src/test/java/com/droiddeck/launcher/session/SessionDisplayTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SessionDisplayTest.kt index 1c26f7814..a68458125 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SessionDisplayTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SessionDisplayTest.kt @@ -12,16 +12,12 @@ class SessionDisplayTest { } @Test fun heightPresetsKeepThePanelShapeWithASixteenByNineFloor() { - // 20:9 phone: edge to edge, no bars for the client's interface. assertEquals(1600 to 720, SessionDisplay.resolveChoice(2400 to 1080, SessionDisplay.DEFAULT_RESOLUTION)) assertEquals(1608 to 720, SessionDisplay.resolveChoice(2412 to 1080, "720p")) - // 1440p phone: bounded by the preset height, not the panel. assertEquals(1560 to 720, SessionDisplay.resolveChoice(3120 to 1440, "720p")) assertEquals(2340 to 1080, SessionDisplay.resolveChoice(3120 to 1440, "1080p")) - // Foldable inner panel and 4:3 handhelds: never narrower than 16:9. assertEquals(1280 to 720, SessionDisplay.resolveChoice(2208 to 1840, "720p")) assertEquals(1280 to 720, SessionDisplay.resolveChoice(1440 to 1080, "720p")) - // Never taller than the panel. assertEquals(1280 to 720, SessionDisplay.resolveChoice(1280 to 720, "1080p")) } @@ -39,10 +35,8 @@ class SessionDisplayTest { } @Test fun unfoldedControlsKeepTheGameSixteenByNineAcrossTheWidth() { - // Fold inner screen held as a tablet, and upright: 16:9 either way, the controls the rest. assertEquals(1376, SessionDisplay.unfoldedGameHeight(2448, 1848)) assertEquals(1038, SessionDisplay.unfoldedGameHeight(1848, 2448)) - // A window near 16:9 itself still leaves the controls a fifth of it. assertEquals(864, SessionDisplay.unfoldedGameHeight(1920, 1080)) } diff --git a/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareTest.kt index 1d58f179f..3e5a71d12 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareTest.kt @@ -27,7 +27,6 @@ class SessionLogShareTest { z.entries().toList().associate { it.name.substringAfter('/') to z.getInputStream(it).bufferedReader().readText() } } - /** A record as the session's ending writes it, listing [files] as they stand now. */ private fun record(folder: File, vararg files: String, rules: Int = LogRedactor.RULES_VERSION) { File(folder, SessionArtifacts.SCRUBBED_TREE_MARKER).writeText( "rules $rules\n" + files.joinToString("") { val f = File(folder, it); "${f.length()}\t${f.lastModified()}\t$it\n" }) @@ -35,8 +34,6 @@ class SessionLogShareTest { @Test fun aRecordedUnchangedFileIsScrubbedAgainOnTheWayIntoTheZip() { val folder = tmp.newFolder("2026-10-06-01-steam") - // A file the ending recorded as scrubbed still gets the share's final pass: the record - // may predate a rule, and a zip is the one way a log leaves the device. File(folder, "session.log").writeText("$secret\n") record(folder, "session.log") assertFalse(zip(folder).getValue("session.log").contains(leaked)) @@ -50,7 +47,6 @@ class SessionLogShareTest { File(folder, "steam/deep/nested.txt").writeText("$secret\n") File(folder, ".hidden.txt").writeText("$secret\n") record(folder, "app.log", "session.log") - // Appended to after the ending (the app's log capture), and touched with an older time. File(folder, "app.log").appendText("$secret\n") File(folder, "session.log").apply { writeText("$secret\n".padEnd(3, ' ')); setLastModified(1_000L) } File(folder, "events.jsonl").writeText("$secret\n") @@ -109,7 +105,6 @@ class SessionLogShareTest { @Test fun aFileThatCouldNotBeScrubbedLeavesTheFolderUnrecorded() { val folder = tmp.newFolder("2026-10-06-06-steam") File(folder, "session.log").writeText("$secret\n") - // A folder the scrubbed copy cannot be written into: the scrub fails, the original stays. folder.setWritable(false) try { SessionArtifacts.scrubAndMark(folder) diff --git a/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareZipTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareZipTest.kt index 81f230d66..6ee341bc8 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareZipTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SessionLogShareZipTest.kt @@ -12,7 +12,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** A shared zip is scrubbed on the way in, whatever the files on disk still hold. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class SessionLogShareZipTest { @@ -21,7 +20,6 @@ class SessionLogShareZipTest { @Test fun everyTextFileInTheZipIsScrubbedAndTheStoresAndToolsLogsComeAlong() { val logs = LinuxRuntime.logDir(app) val session = File(logs, "2026-10-08-01-steam").apply { mkdirs() } - // Written raw, as a file from before a rule existed would be. File(session, "session.log").writeText( "fetch https://gog-cdn-fastly.gog.com/token=nva=1791500000~dirs=/x~token=0a1b2c3d4e5f/content-system/v2/store/1/ab/cd/f\n" + "fetch https://epicgames-download1.akamaized.net/Builds/Org/a/ChunksV4/12/ABCD.chunk?__token__=exp=1~hmac=deadbeefcafe\n" + @@ -40,7 +38,6 @@ class SessionLogShareZipTest { for (secret in listOf("0a1b2c3d4e5f", "deadbeefcafe", "abcdef123456", "someone@example.com", "AUTHCODE123", "abc.def.ghi", "zzzzzzzz12", "eyabc")) { assertFalse("$secret survived", all.contains(secret)) } - // What makes a log worth reading stays. assertTrue(all.contains("kept: EResult 5, app 1086940, steamapps/common/Game")) assertTrue(all.contains("https://gog-cdn-fastly.gog.com/")) @@ -53,9 +50,6 @@ class SessionLogShareZipTest { assertEquals(line, com.droiddeck.launcher.core.LogRedactor.redactForShare(line)) } - // Folders an earlier build scrubbed and marked under older rules, their copied Steam UI log - // still naming the account: one app start (the move out of Download/, then the pass over - // older folders) leaves every one scrubbed on disk under the current rules. private fun olderFolder(parent: File, name: String, marker: String = ".scrubbed-2"): File { val dir = File(parent, name).apply { mkdirs() } File(dir, marker).writeText("scrubbed earlier\n") @@ -91,12 +85,10 @@ class SessionLogShareZipTest { val legacy = LinuxRuntime.legacyLogDir().apply { mkdirs() } val names = (1..9).map { "2026-10-0${it}-01-steam" } names.forEachIndexed { i, name -> - // Old markers of every kind an earlier build left, one even claiming the current rules. olderFolder(legacy, name, marker = listOf(".scrubbed-2", ".scrubbed-1", ".scrubbed-r${com.droiddeck.launcher.core.LogRedactor.RULES_VERSION}")[i % 3]) } File(legacy, "Saves").apply { mkdirs() }.resolve("keep.txt").writeText("save\n") - // What MainActivity runs at start. LogMigration.run(app) SessionArtifacts.scrubOlder(app) diff --git a/app/src/test/java/com/droiddeck/launcher/session/SteamDownloadMonitorTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SteamDownloadMonitorTest.kt index 86cf32009..28d5a7720 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SteamDownloadMonitorTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SteamDownloadMonitorTest.kt @@ -79,13 +79,11 @@ class SteamDownloadMonitorTest { var now = 1_000L assertTrue(monitor.poll(root, now, received)) - // Five minutes of a download Steam no longer logs, at 4 MB/s. while (now < 300_000L) { now += 2_000L received += 8_000_000L assertTrue(monitor.poll(root, now, received)) } - // Traffic stops: the session may suspend once the grace runs out. assertTrue(monitor.poll(root, now + 90_000L, received)) assertFalse(monitor.poll(root, now + 92_001L, received)) diff --git a/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt b/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt index 8046e7794..0c2a990d8 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/SteamLanguageTest.kt @@ -42,7 +42,6 @@ class SteamLanguageTest { assertEquals("latam", steam("es-MX")) assertEquals("latam", steam("es-419")) assertEquals("spanish", steam("es-ES")) - // The app's Spanish has no region; the system's says where the player is. assertEquals("latam", steam("es", region = "AR")) assertEquals("spanish", steam("es", region = "ES")) } diff --git a/app/src/test/java/com/droiddeck/launcher/session/WinComponentsSupportTest.kt b/app/src/test/java/com/droiddeck/launcher/session/WinComponentsSupportTest.kt index 31dad07df..656c56c9a 100644 --- a/app/src/test/java/com/droiddeck/launcher/session/WinComponentsSupportTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/session/WinComponentsSupportTest.kt @@ -23,7 +23,6 @@ class WinComponentsSupportTest { @Test fun windowsInstallerPackagesInstallHere() { assertEquals(Support.READY, support(component("xna40", step("install_msi", "url" to "$base/xna40.msi")))) - // The catalog names some .msi packages install_exe. assertEquals(Support.READY, support(component("XLiveRedist", step("install_exe", "url" to "$base/XLiveRedist.msi", "file_name" to "XLiveRedist1.2.0238.msi")))) assertEquals(Support.READY, support(component("msxml3", step("delete_dlls", "dest" to "win32"), @@ -33,12 +32,10 @@ class WinComponentsSupportTest { } @Test fun installerWrappersInstallHere() { - // A WiX bundle, a self-extracting cabinet or 7-Zip archive: the packages inside are laid out. assertEquals(Support.READY, support(component("vcredist2022", step("install_exe", "url" to "$base/vcredist2022__VC_redist.x86.exe", "file_name" to "VC_redist.x86.exe"), step("install_exe", "url" to "$base/vcredist2022__VC_redist.x64.exe", "file_name" to "VC_redist.x64.exe"), step("override_dll", "dll" to "vcruntime140", "type" to "native,builtin")))) - // .NET Framework: "uninstall Wine Mono", the Windows version, the installer, a registry key. assertEquals(Support.READY, support(component("dotnet40", step("uninstall", "file_name" to "Wine Mono"), step("set_windows", "version" to "win7"), step("install_exe", "url" to "https://download.microsoft.com/x/dotNetFx40_Full_x86_x64.exe", "file_name" to "dotNetFx40_Full_x86_x64.exe"), @@ -48,7 +45,6 @@ class WinComponentsSupportTest { } @Test fun nestedCatalogKeysAreRead() { - // A few .NET entries keep url and file_name under "environment". val nested = WinComponents.Step("install_exe", JSONObject().put("environment", JSONObject() .put("WINEDLLOVERRIDES", "fusion=b").put("file_name", "ndp48-x86-x64-allos-enu.exe") .put("url", "https://download.visualstudio.microsoft.com/x/ndp48-x86-x64-allos-enu.exe"))) @@ -67,14 +63,11 @@ class WinComponentsSupportTest { @Test fun recordedInstallersInstallHere() { val snapshot = "https://github.com/Droid-Deck/DroidDeck-Components/releases/download/component-snapshots-v1/K-Lite.snapshot.json" - // A setup program with a recording of its result installs from the recording. val klite = WinComponents.Component("K-Lite", "", "", "ready", emptyList(), listOf(step("install_exe", "url" to "$base/K-Lite.exe", "file_name" to "K-Lite_1960.exe")), snapshot) assertEquals(Support.READY, WinComponents.support(klite, mapOf(klite.name to klite))) - // Only a recording on the release counts. val elsewhere = WinComponents.Component("K-Lite", "", "", "ready", emptyList(), klite.steps, "http://example.com/K-Lite.snapshot.json") assertEquals(Support.NEEDS_INSTALLER, WinComponents.support(elsewhere, mapOf(elsewhere.name to elsewhere))) - // Its prerequisites still have to be installable. val sp1 = WinComponents.Component("dotnet20sp1", "", "", "ready", listOf("dotnet20"), emptyList(), snapshot.replace("K-Lite", "dotnet20sp1")) val base20 = WinComponents.Component("dotnet20", "", "", "ready", emptyList(), listOf(step("install_exe", "url" to "$base/dotnet20__dotnetfx.exe", "file_name" to "dotnetfx.exe"))) @@ -82,7 +75,6 @@ class WinComponentsSupportTest { } @Test fun setupProgramsStillWait() { - // NSIS / InnoSetup installers hold no package to lay out. assertEquals(Support.NEEDS_INSTALLER, support(component("K-Lite", step("install_exe", "url" to "$base/K-Lite.exe", "file_name" to "K-Lite_1960.exe")))) assertEquals(Support.NEEDS_INSTALLER, support(component("plain", step("install_msi", "url" to "http://example.com/a.msi")))) diff --git a/app/src/test/java/com/droiddeck/launcher/store/FlathubApiTest.kt b/app/src/test/java/com/droiddeck/launcher/store/FlathubApiTest.kt index df2b3acac..2682c19fc 100644 --- a/app/src/test/java/com/droiddeck/launcher/store/FlathubApiTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/store/FlathubApiTest.kt @@ -12,7 +12,6 @@ class FlathubApiTest { } @Test fun refsReadAsWords() { - // The words come from string resources; stand in for them with their English. val english = mapOf( R.string.flatpakmgr_ref_runtime_branch to "%1\$s runtime %2\$s", R.string.flatpakmgr_ref_graphics_drivers to "graphics drivers", diff --git a/app/src/test/java/com/droiddeck/launcher/stores/CloudPlanTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/CloudPlanTest.kt index 854c04a9c..5338fd343 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/CloudPlanTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/CloudPlanTest.kt @@ -10,7 +10,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -/** The rules that keep a cloud sync from replacing real saves. (Robolectric for org.json.) */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class CloudPlanTest { @@ -42,20 +41,16 @@ class CloudPlanTest { @Test fun theSideThatChangedWins() { val base = mapOf("s" to Entry("a", "a", 1000)) - // Only local changed: up sends it, down leaves it. assertEquals(listOf("s"), CloudPlan.up(mapOf("s" to Local("b")), mapOf("s" to Remote("a", 1000)), base).transfer) assertTrue(CloudPlan.down(mapOf("s" to Local("b")), mapOf("s" to Remote("a", 1000)), base).transfer.isEmpty()) - // Only the cloud changed: down takes it, up leaves it. assertEquals(listOf("s"), CloudPlan.down(mapOf("s" to Local("a")), mapOf("s" to Remote("c", 9000)), base).transfer) assertTrue(CloudPlan.up(mapOf("s" to Local("a")), mapOf("s" to Remote("c", 9000)), base).transfer.isEmpty()) - // The same bytes on both sides: nothing moves. assertEquals(listOf("s"), CloudPlan.up(mapOf("s" to Local("c")), mapOf("s" to Remote("c", 9000)), base).same) } @Test fun timesAloneDecideWhenTheServiceGivesNoHash() { val base = mapOf("s" to Entry("a", null, 1000)) assertEquals(listOf("s"), CloudPlan.down(mapOf("s" to Local("a")), mapOf("s" to Remote(null, 60_000)), base).transfer) - // Within two seconds of the baseline: the same cloud copy. assertTrue(CloudPlan.down(mapOf("s" to Local("a")), mapOf("s" to Remote(null, 2500)), base).transfer.isEmpty()) } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/CloudSavePathsTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/CloudSavePathsTest.kt index 5258c19ad..a88c8b954 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/CloudSavePathsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/CloudSavePathsTest.kt @@ -6,7 +6,6 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test -/** A store's save-location template, made concrete inside the game's prefix (Bannerlator's cases). */ class CloudSavePathsTest { private val root = Files.createTempDirectory("cloud").toFile() private val prefix = File(root, "compatdata/3785150007/pfx").apply { mkdirs() } @@ -45,7 +44,6 @@ class CloudSavePathsTest { CloudSavePaths.epic("{UserSavedGames}/Studio/{EpicId}/{AppName}", prefix, install, "acc", "app")!!.path) assertEquals(File(profile, "Documents/G").path, CloudSavePaths.epic("{UserDir}/G", prefix, install, "acc", "app")!!.path) assertNull(CloudSavePaths.epic("{Nope}/G", prefix, install, "acc", "app")) - // Epic's folder names are matched by letters and digits alone when that names exactly one. File(profile, "AppData/Local/My-Game").mkdirs() assertEquals(File(profile, "AppData/Local/My-Game").path, CloudSavePaths.epic("{AppData}/MyGame", prefix, install, "acc", "app")!!.path) } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/CloudSavesTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/CloudSavesTest.kt index a85ee7c18..023013ed5 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/CloudSavesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/CloudSavesTest.kt @@ -10,7 +10,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** A sync against a cloud held in memory: the baseline gate, conflicts, backups and the no-prefix deferral. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class CloudSavesTest { @@ -59,7 +58,6 @@ class CloudSavesTest { assertEquals(1, up.conflicts) assertEquals("cloud v2", String(cloud.files.getValue("slot0.sav").first)) assertEquals(listOf("slot0.sav"), CloudSaves.conflicts(app, Store.GOG, game)) - // Keep local resolves it. CloudSaves.upWith(app, Store.GOG, game, dir, cloud, force = true) assertEquals("local v2", String(cloud.files.getValue("slot0.sav").first)) assertTrue(CloudSaves.conflicts(app, Store.GOG, game).isEmpty()) @@ -70,17 +68,14 @@ class CloudSavesTest { val folder = File(StoreInstallRoot.storeDir(StoreInstallRoot.internalRoot(app), Store.GOG), "ELDERBORN-mark").apply { mkdirs() } File(folder, "ELDERBORN.exe").writeText("MZ") StoreGameSidecar(Store.GOG, gameId, "ELDERBORN", "ELDERBORN.exe").write(folder) - // A save location the store gave, but no prefix yet: the upload cannot run, the mark stays. com.droiddeck.launcher.stores.gog.GogPrefs.get(app).edit().putString("cloud_template_$gameId", "<?SAVED_GAMES?>/ELDERBORN").apply() CloudSaves.markDirty(app, Store.GOG, gameId) assertTrue(CloudSaves.dirty(app).contains(Store.GOG to gameId)) val first = CloudSaves.uploadDirty(app, Store.GOG, gameId, "session-end")!! assertEquals("no-prefix", first.reason) assertTrue(CloudSaves.dirty(app).contains(Store.GOG to gameId)) - // Recovery waits while a session runs. CloudSaves.uploadAllDirty(app, "recovery", running = true) assertTrue(CloudSaves.dirty(app).contains(Store.GOG to gameId)) - // The store has no cloud saves for it: a final skip, the mark goes; a second trigger finds nothing. com.droiddeck.launcher.stores.gog.GogPrefs.get(app).edit().putString("cloud_template_$gameId", "").apply() assertEquals("no-cloud-saves", CloudSaves.uploadDirty(app, Store.GOG, gameId, "exit")!!.reason) assertTrue(CloudSaves.dirty(app).none { it.second == gameId }) diff --git a/app/src/test/java/com/droiddeck/launcher/stores/GameFactsTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/GameFactsTest.kt index 5e2cc02f3..c8834783b 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/GameFactsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/GameFactsTest.kt @@ -7,7 +7,6 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test -/** The name match that finds a store game on Steam and HowLongToBeat, and how its facts read. */ class GameFactsTest { private val saved = Locale.getDefault() @@ -42,7 +41,6 @@ class GameFactsTest { val results = listOf("Hades II", "The Witcher 3: Wild Hunt — Songs of the Past", "Hades", "Hades") assertEquals(2, TitleMatch.pick("Hades", results.withIndex().toList()) { listOf(it.value) }?.index) assertNull(TitleMatch.pick("Hades", listOf("Hades II", "Hadesville")) { listOf(it) }) - // An alias counts as a name. assertEquals("x", TitleMatch.pick("The Witcher III: Wild Hunt", listOf("x" to "The Witcher 3: Wild Hunt, The Witcher III: Wild Hunt")) { it.second.split(',') }?.first) } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/InstalledCardsTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/InstalledCardsTest.kt index 090280f77..da3b873e1 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/InstalledCardsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/InstalledCardsTest.kt @@ -4,18 +4,15 @@ import java.io.File import org.junit.Assert.assertEquals import org.junit.Test -/** The Installed tab draws what its count counts: the installs on disk. */ class InstalledCardsTest { private fun install(store: Store, id: String, title: String) = InstalledStoreGame(StoreGameSidecar(store, id, title, "game.exe", cover = "https://img/$id.jpg"), File("/x/$id")) @Test fun everyInstallIsACardWhetherOrNotTheLibraryHasLoaded() { val installed = listOf(install(Store.GOG, "1", "DOOM + DOOM II"), install(Store.GOG, "2", "DOOM I Enhanced"), install(Store.EPIC, "e", "DOOMBLADE")) - // Library not loaded: cards from the sidecars. val bare = installedCards(Store.GOG, installed, emptyList()) assertEquals(listOf("DOOM + DOOM II", "DOOM I Enhanced"), bare.map { it.title }) assertEquals("https://img/1.jpg", bare.first().tallImageUrl) - // Loaded: the library item wins, by id, or by title when the ids differ. val library = listOf( CatalogItem(Store.GOG, "1", "DOOM + DOOM II", imageUrl = "wide1", owned = true), CatalogItem(Store.GOG, "999", "DOOM I Enhanced", imageUrl = "wide2", owned = true), diff --git a/app/src/test/java/com/droiddeck/launcher/stores/MatureContentTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/MatureContentTest.kt index 2380b692e..ec6b2062b 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/MatureContentTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/MatureContentTest.kt @@ -9,7 +9,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -/** Mature means what the store's own ratings or tags say; a title without them stays visible. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class MatureContentTest { @@ -20,7 +19,6 @@ class MatureContentTest { assertTrue(GogStoreCatalog.isMature(gog("""{"name":"gogRating","ageRating":"18"}""", ""))) assertTrue(GogStoreCatalog.isMature(gog("""{"name":"pegiRating","ageRating":"12"}""", """{"name":"NSFW","slug":"nsfw"}"""))) assertFalse(GogStoreCatalog.isMature(gog("""{"name":"esrbRating","ageRating":"13"},{"name":"uskRating","ageRating":"16"}""", """{"name":"Puzzle","slug":"puzzle"}"""))) - // No rating data at all: visible. assertFalse(GogStoreCatalog.isMature(JSONObject("""{"id":"2","title":"U"}"""))) } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreAccountsCipherTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreAccountsCipherTest.kt index b4346c7ca..aed81f155 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreAccountsCipherTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreAccountsCipherTest.kt @@ -17,7 +17,6 @@ import java.security.SecureRandom import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec -/** The stores' sign-in files sealed at rest; a plain key stands in for the AndroidKeyStore. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class StoreAccountsCipherTest { @@ -44,7 +43,6 @@ class StoreAccountsCipherTest { assertEquals("AES/GCM", envelope.getString("alg")) assertFalse(envelope.toString().contains("secret")) assertEquals(creds.toString(), CredentialCipher.open(envelope, fake)) - // A fresh IV every time. assertFalse(envelope.getString("iv") == CredentialCipher.seal(creds.toString(), fake).getString("iv")) } @@ -87,7 +85,6 @@ class StoreAccountsCipherTest { @Test fun aFileSealedUnderALostKeyReadsAsSignedOutAndIsDeleted() { StoreAccounts.write(app, Store.AMAZON, creds) - // Data cleared, or a backup restored on another device: the Keystore key is a new one. val other = newKey() StoreAccounts.keys = CredentialCipher.KeyProvider { other } assertNull(StoreAccounts.read(app, Store.AMAZON)) @@ -108,7 +105,6 @@ class StoreAccountsCipherTest { assertEquals(creds.toString(), StoreAccounts.read(app, Store.EPIC).toString()) assertTrue(StoreAccounts.keystoreFailed) - // The next start, on a Keystore that works. StoreAccounts.keys = fake StoreAccounts.keystoreFailed = false StoreAccounts.encryptAll(app) @@ -123,7 +119,6 @@ class StoreAccountsCipherTest { assertNull(StoreAccounts.read(app, Store.GOG)) } - /** A Keystore whose key is there but which fails to hand it over [failures] times first. */ private class Flaky(private val key: SecretKey, var failures: Int, private val error: () -> Exception) : CredentialCipher.KeyProvider { override fun key(): SecretKey = key override fun existing(): SecretKey { if (failures > 0) { failures--; throw error() }; return key } @@ -142,10 +137,8 @@ class StoreAccountsCipherTest { assertNull(StoreAccounts.read(app, Store.EPIC)) assertTrue(StoreAccounts.isUnavailable(Store.EPIC)) assertEquals(sealed, file(Store.EPIC).readText()) - // Still signed in for the page: no sign-in card, and an action says why it failed. assertEquals("Epic sign-in could not be read: try again.", StoresState.notSignedInLine(Store.EPIC)) - // The next access, once the Keystore answers. assertEquals(creds.toString(), StoreAccounts.read(app, Store.EPIC).toString()) assertFalse(StoreAccounts.isUnavailable(Store.EPIC)) assertEquals("Someone", StoreAccounts.signedInAs(app, Store.EPIC)) diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreCacheTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreCacheTest.kt index a8e4dd20b..c01e4202f 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreCacheTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreCacheTest.kt @@ -11,7 +11,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -/** Sizes kept per version, and a refresh that touches only the cards that changed. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class StoreCacheTest { @@ -20,7 +19,6 @@ class StoreCacheTest { @Test fun aSizeIsKeptForItsVersionAndAskedAgainForANewOne() { val cache = SizeCache.forStore(dir, Store.EPIC) cache.put("Metalstorm", "1.0.3", 4_400_000_000L) - // Read back from disk by a new instance, as the next app start does. val again = SizeCache.forStore(dir, Store.EPIC) assertEquals(4_400_000_000L, again.get("Metalstorm", "1.0.3")) assertNull(again.get("Metalstorm", "1.0.4")) @@ -36,14 +34,11 @@ class StoreCacheTest { val a = CatalogItem(Store.GOG, "1", "A", imageUrl = "x", owned = true) val b = CatalogItem(Store.GOG, "2", "B", imageUrl = "y", owned = true) val old = listOf(a, b) - // Nothing changed: the very list shown stays. assertSame(old, mergeItems(old, listOf(a.copy(), b.copy()))) - // One card changed: the other is the same object, the changed one is new. val merged = mergeItems(old, listOf(a.copy(), b.copy(title = "B2"))) assertSame(a, merged[0]) assertNotSame(b, merged[1]) assertEquals("B2", merged[1].title) - // A first load is the new list as it is. val fresh = listOf(a) assertSame(fresh, mergeItems(null, fresh)) } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreGameSidecarTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreGameSidecarTest.kt index ae966ca9b..d0f2de6ca 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreGameSidecarTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreGameSidecarTest.kt @@ -9,7 +9,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -/** Under Robolectric for a real org.json; the plain unit-test android.jar stubs it out. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class StoreGameSidecarTest { @@ -37,11 +36,9 @@ class StoreGameSidecarTest { @Test fun defaultsWhenFieldsAreAbsent() { val back = StoreGameSidecar.parse("""{"store":"gog","id":"1","title":"T","exe":"bin\\game.exe","addToSteam":false}""")!! - // An older sidecar's Steam switch is read past, not acted on. assertEquals("bin/game.exe", back.exe) assertNull(back.launcher) assertTrue(back.args.isEmpty()) - // No state field = an earlier build's finished install. assertTrue(back.isInstalled) } @@ -53,9 +50,7 @@ class StoreGameSidecarTest { val back = StoreGameSidecar.parse(set.toJson().toString())!! assertEquals(EpicOptions(eos = true, offline = true), back.epic) assertFalse(back.epic.wantsCode) - // A sidecar from before the choices existed reads as the defaults. assertEquals(EpicOptions(), StoreGameSidecar.parse("""{"store":"epic","id":"m","title":"M","exe":"M.exe"}""")!!.epic) - // An earlier build's overlay field is read past. assertEquals(EpicOptions(), StoreGameSidecar.parse("""{"store":"epic","id":"m","title":"M","exe":"M.exe","epic":{"v":2,"eos":true,"offline":false,"overlay":true}}""")!!.epic) } @@ -64,12 +59,10 @@ class StoreGameSidecarTest { StoreGameSidecar(Store.EPIC, "m", "Metalstorm", "Metalstorm.exe", launcher = ".droiddeck-launch.bat").write(folder) val written = StoreGameSidecar.updateEpic(folder) { it.copy(offline = true) }!! assertTrue(written.epic.offline) - // What the Steam-launch helper reads: the file itself. val onDisk = org.json.JSONObject(java.io.File(folder, StoreGameSidecar.FILE_NAME).readText()) assertTrue(onDisk.getJSONObject("epic").getBoolean("offline")) assertTrue(onDisk.getJSONObject("epic").getBoolean("eos")) assertFalse(onDisk.getJSONObject("epic").has("overlay")) - // Everything else in it stays. assertEquals(".droiddeck-launch.bat", StoreGameSidecar.read(folder)!!.launcher) assertEquals(null, StoreGameSidecar.updateEpic(java.io.File(folder, "missing")) { it }) } @@ -89,7 +82,6 @@ class StoreGameSidecarTest { val back = StoreGameSidecar.parse(started.toJson().toString())!! assertFalse(back.isInstalled) assertEquals("", back.exe) - // A finished one must still name its exe. assertNull(StoreGameSidecar.parse("""{"store":"gog","id":"1","title":"T","exe":""}""")) assertTrue(StoreGameSidecar.parse(back.copy(exe = "Metalstorm.exe", state = StoreGameSidecar.STATE_INSTALLED).toJson().toString())!!.isInstalled) } @@ -98,7 +90,6 @@ class StoreGameSidecarTest { assertNull(StoreGameSidecar.parse("not json")) assertNull(StoreGameSidecar.parse("""{"store":"steam","id":"1","title":"T","exe":"a.exe"}""")) assertNull(StoreGameSidecar.parse("""{"store":"gog","id":"","title":"T","exe":"a.exe"}""")) - // An exe outside the folder is not trusted: a sidecar must not point the shortcut anywhere. assertNull(StoreGameSidecar.parse("""{"store":"gog","id":"1","title":"T","exe":"../other/a.exe"}""")) assertNull(StoreGameSidecar.parse("""{"store":"gog","id":"1","title":"T","exe":"/abs/a.exe"}""")) assertNull(StoreGameSidecar.parse("""{"store":"gog","id":"1","title":"T","exe":"C:\\a.exe"}""")) @@ -116,10 +107,7 @@ class StoreGameSidecarTest { assertTrue(text.contains("cd /d \"%~dp0Binaries\\Win64\"\r\n")) assertTrue(text.contains("set \"FOO=bar\"\r\n")) assertTrue(text.contains("\r\n\"%~dp0Binaries\\Win64\\Celeste.exe\" -EpicPortal -epicusername=\"Some One\"\r\n")) - // Epic reads the one-shot exchange code the app leaves beside the launcher, then deletes it. assertTrue(text.contains("set /p DD_X=<\"%~dp0.droiddeck-epic-code\"")) - // The code goes on the command line only: the variable is cleared on the very line that - // starts the game (cmd has already expanded it there), so the game inherits none of it. assertTrue(text.contains("set \"DD_X=\" & \"%~dp0Binaries\\Win64\\Celeste.exe\" -EpicPortal -epicusername=\"Some One\" -AUTH_LOGIN=unused -AUTH_PASSWORD=%DD_X% -AUTH_TYPE=exchangecode\r\n")) assertFalse(text.contains("AUTH=")) assertFalse(text.contains("DD_AUTH")) diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt index 3856ed562..217bc7d5a 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreInstallRootTest.kt @@ -12,7 +12,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config -/** Where a store game goes: internal storage unless the user picks a card, and back to its own folder on a rerun. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class StoreInstallRootTest { @@ -22,7 +21,6 @@ class StoreInstallRootTest { val card = File(app.filesDir, "fake-card/steam").apply { mkdirs() } SessionPrefs.setGameStorage(app, card.path, "Card") assertEquals(File(LinuxRuntime.rootDir(app), "root/Games/Stores").absolutePath, StoreInstallRoot.installRoot(app).absolutePath) - // The library's Games folder is still scanned, for installs an earlier build put there. assertTrue(StoreInstallRoot.roots(app).any { it.absolutePath == File(card, "Games").absolutePath }) assertFalse(StoreInstallRoot.targets(app).first().removable) } @@ -44,9 +42,7 @@ class StoreInstallRootTest { File(half, "Metalstorm.exe").writeText("MZ") assertFalse(StoreInstallRoot.isFinished(half)) assertTrue(StoreInstallRoot.unfinished(app).any { it.store == Store.EPIC && it.id == null && it.folder.absolutePath == half.absolutePath }) - // Install again, even with another root picked: the same folder, no duplicate. assertEquals(half.absolutePath, StoreInstallRoot.folderFor(app, Store.EPIC, "abc", "Metalstorm", File(app.filesDir, "card/Games")).absolutePath) - // Begun and finished: the folder becomes a game. StoreInstalls.begin(half, Store.EPIC, "abc", "Metalstorm", null, null) assertTrue(StoreInstallRoot.unfinished(app).any { it.id == "abc" }) StoreGameSidecar(Store.EPIC, "abc", "Metalstorm", "Metalstorm.exe").write(half) diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreLogTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreLogTest.kt index 0b72b771b..4295cc557 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreLogTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreLogTest.kt @@ -4,7 +4,6 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Test -/** Store lines reach the Downloads log and logcat; nothing in them may carry a token. */ class StoreLogTest { @Test fun gogSecureLinkKeepsOnlyTheHost() { val line = "gog: window 32 in_flight 3 https://gog-cdn-fastly.gog.com/token=nva=1791500000~dirs=/content-system/v2/store/1207658924~token=0a1b2c3d4e5f/content-system/v2/store/1207658924/ab/cd/abcdef 12.1 MB/s" @@ -24,7 +23,6 @@ class StoreLogTest { assertEquals("refresh access_token=… refresh_token: '…' code=… hdnts=… __token__=…", out) assertEquals("Authorization: …", StoreLog.redactLine("Authorization: Bearer eyJhbGciOi")) assertEquals("""{"access_token":"…","expires_in":7200}""", StoreLog.redactLine("""{"access_token":"eyJ.abc","expires_in":7200}""")) - // Ordinary numbers stay: "HTTP code 403" has no separator to read as a secret. assertEquals("HTTP code 403 from https://api.gog.com/products/…", StoreLog.redactLine("HTTP code 403 from https://user:pw@api.gog.com/products/1?expand=x")) } } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadEntryTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadEntryTest.kt index 23c6b93db..46b38647c 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadEntryTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadEntryTest.kt @@ -6,23 +6,19 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test -/** The Downloads row reads the active stage's own progress, and which stages are behind it. */ class DownloadEntryTest { private val base = DownloadEntry(Store.EPIC, "doomblade", "DOOMBLADE", state = DownloadState.RUNNING) @Test fun downloadRunsOnBytesAndInstallOnItsOwnCount() { val dl = base.copy(stage = DownloadStage.DOWNLOAD, bytesDone = 50, bytesTotal = 200) assertEquals(0.25f, dl.stageFraction, 0.001f) - // Past Download the bytes no longer drive the bar; the stage's own count does. val inst = dl.copy(stage = DownloadStage.INSTALL, stageDone = 30, stageTotal = 40, stageItems = 3, stageItemsTotal = 4) assertEquals(0.75f, inst.stageFraction, 0.001f) assertEquals(75, inst.percent) - // Nothing counted yet: unknown, not empty and not full. assertEquals(-1f, base.copy(stage = DownloadStage.INSTALL).stageFraction, 0f) } @Test fun passedStagesComeFromTheBitsNotTheOrder() { - // Epic checks files before it fetches: Verify is behind Download there. val e = base.copy(stage = DownloadStage.DOWNLOAD, stagesPassed = (1 shl DownloadStage.MANIFEST.ordinal) or (1 shl DownloadStage.VERIFY.ordinal)) assertTrue(e.passed(DownloadStage.VERIFY)) assertFalse(e.passed(DownloadStage.INSTALL)) diff --git a/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadNotificationTextTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadNotificationTextTest.kt index 8bd9870ad..aca4c436d 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadNotificationTextTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/download/DownloadNotificationTextTest.kt @@ -4,7 +4,6 @@ import com.droiddeck.launcher.stores.Store import org.junit.Assert.assertEquals import org.junit.Test -/** The notification's line: what the Downloads row says, on one line. */ class DownloadNotificationTextTest { private val base = DownloadEntry(Store.EPIC, "m", "Metalstorm", state = DownloadState.RUNNING) diff --git a/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicJsonManifestTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicJsonManifestTest.kt index 4d085013f..669e0adc4 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicJsonManifestTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/epic/EpicJsonManifestTest.kt @@ -8,7 +8,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -/** The JSON manifest's numbers are blobs; the same values as the Rust engine's manifest tests. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class EpicJsonManifestTest { diff --git a/app/src/test/java/com/droiddeck/launcher/stores/gog/GogCdnTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/gog/GogCdnTest.kt index 22b418da6..324338e17 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/gog/GogCdnTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/gog/GogCdnTest.kt @@ -6,7 +6,6 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -/** Every CDN GOG's secure link offers is used, not only the first. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class GogCdnTest { diff --git a/app/src/test/java/com/droiddeck/launcher/ui/FlathubFocusTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/FlathubFocusTest.kt index b98cd7660..a268c46a8 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/FlathubFocusTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/FlathubFocusTest.kt @@ -79,14 +79,11 @@ class FlathubFocusTest { compose.waitForIdle() val field = editText(root)!! compose.runOnIdle { - // Keep a nonempty query, so clearing focus cannot simply dispose the search field. - // Whitespace skips the network search while retaining the field just like typed text. field.setText(" ") field.requestFocus() assertTrue(field.isFocused) } compose.waitForIdle() - // Touching a card does not implicitly focus a Compose control or release an AndroidView. compose.onNodeWithText("Datcord").performSemanticsAction(SemanticsActions.OnClick) { it() } compose.onNodeWithText(context.getString(R.string.store_install)).assertExists() compose.runOnIdle { diff --git a/app/src/test/java/com/droiddeck/launcher/ui/GamepadGeometryTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/GamepadGeometryTest.kt index 77a8d5b8e..e60f12f44 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/GamepadGeometryTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/GamepadGeometryTest.kt @@ -8,7 +8,6 @@ import org.junit.Assert.assertTrue import org.junit.Test class GamepadGeometryTest { - /** A foldable's unfolded panel (px) and density, as the gamepad gets it under a 16:9 game. */ private data class Panel(val name: String, val width: Int, val height: Int, val density: Float) { val controlsHeight get() = height - SessionDisplay.unfoldedGameHeight(width, height) } @@ -68,7 +67,6 @@ class GamepadGeometryTest { val right = b.getValue(Slot.RIGHT_MAIN) assertEquals(panel.name, left.cy, right.cy, 0.5f) assertEquals(panel.name, left.cx, panel.width - right.cx, 0.5f) - // In the bottom half of their side, nearer the corner than the second controls. assertTrue(panel.name, left.cx < b.getValue(Slot.LEFT_SECOND).cx) assertTrue(panel.name, left.cy > panel.controlsHeight / 2f) } @@ -77,8 +75,6 @@ class GamepadGeometryTest { @Test fun middleButtonsKeepTheDeckOrder() { for (panel in panels) { val b = boxes(panel) - // View before Menu and Steam before QAM: left of it in a row or two by two, above it - // in a single column. fun before(a: Slot, c: Slot) = b.getValue(a).let { x -> b.getValue(c).let { y -> x.cx < y.cx - 0.5f || (kotlin.math.abs(x.cx - y.cx) <= 0.5f && x.cy < y.cy) } } assertTrue(panel.name, before(Slot.VIEW, Slot.MENU)) diff --git a/app/src/test/java/com/droiddeck/launcher/ui/LiftSheetTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/LiftSheetTest.kt index 36c206f05..241230027 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/LiftSheetTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/LiftSheetTest.kt @@ -10,7 +10,6 @@ class LiftSheetTest { } @Test fun theButtonKeeps16dpAboveIt() { - // A short pane: the edge comes down so the button riding it still clears the top by 16. val top = sheetTop(150f, 56f, 16f, 3f) assertEquals(41f, top, 0.01f) assertEquals(16f, top + 3f - 56f / 2f, 0.01f) diff --git a/app/src/test/java/com/droiddeck/launcher/ui/MotionTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/MotionTest.kt index b16d89efd..669410a9c 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/MotionTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/MotionTest.kt @@ -31,7 +31,6 @@ class MotionTest { Motion.refresh(context) assertEquals(0f, Motion.scale, 0f) assertEquals(0, Motion.ms(300)) - // The app's toggle never writes Android's preference. assertEquals(1.5f, Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE), 0f) SessionPrefs.setAnimationsEnabled(context, true) Motion.refresh(context) diff --git a/app/src/test/java/com/droiddeck/launcher/ui/PageExitTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/PageExitTest.kt index b54248f40..ad1541685 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/PageExitTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/PageExitTest.kt @@ -13,7 +13,6 @@ class PageExitTest { } @Test fun anotherPageSinks() { - // The GPU drivers row in Steam's settings opens Components over the same rail item. assertEquals(PageExit.Sink, pageExit("steam", "components", PageLeave.Ordinary)) } diff --git a/app/src/test/java/com/droiddeck/launcher/ui/PickListingTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/PickListingTest.kt index af7b6f50d..9ed572671 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/PickListingTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/PickListingTest.kt @@ -12,7 +12,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import java.io.File -/** What the in-app picker lists in a folder: every folder A-Z, then only the files asked for. */ @RunWith(RobolectricTestRunner::class) @Config(sdk = [33]) class PickListingTest { diff --git a/app/src/test/java/com/droiddeck/launcher/ui/PresetScaleTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/PresetScaleTest.kt index d498d7f44..244918291 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/PresetScaleTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/PresetScaleTest.kt @@ -21,7 +21,6 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) -// Render the component without starting runtime services or installing the app crash handler. @Config(sdk = [28], application = android.app.Application::class) class PresetScaleTest { @get:Rule val compose = createRobolectricComposeRule() @@ -39,7 +38,6 @@ class PresetScaleTest { } } val originalWidth = compose.onNodeWithTag("page-size").fetchSemanticsNode().boundsInRoot.width - // Notify changes driven by the test before asking Compose to become idle again. compose.runOnIdle { scale.intValue = 75 Snapshot.sendApplyNotifications() diff --git a/app/src/test/java/com/droiddeck/launcher/ui/RobolectricComposeRule.kt b/app/src/test/java/com/droiddeck/launcher/ui/RobolectricComposeRule.kt index e6efbb130..b21ffa60d 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/RobolectricComposeRule.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/RobolectricComposeRule.kt @@ -9,11 +9,8 @@ import org.robolectric.Shadows.shadowOf fun createRobolectricComposeRule() = createComposeRule().also { val context = RuntimeEnvironment.getApplication() - // Register only in Robolectric, without adding a host activity to either APK variant. shadowOf(context.packageManager).addActivityIfNotPresent( ComponentName(context, ComponentActivity::class.java), ) - // Pure state tests can leave unapplied snapshots before a Compose host starts its snapshot - // manager. Apply them before the rule's initial idle check, which runs before the test body. Snapshot.sendApplyNotifications() } diff --git a/app/src/test/java/com/droiddeck/launcher/ui/StoreTabsTest.kt b/app/src/test/java/com/droiddeck/launcher/ui/StoreTabsTest.kt index b8af29326..2be67a4b6 100644 --- a/app/src/test/java/com/droiddeck/launcher/ui/StoreTabsTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/ui/StoreTabsTest.kt @@ -4,13 +4,11 @@ import com.droiddeck.launcher.stores.Store import org.junit.Assert.assertEquals import org.junit.Test -/** A store's modes are Store, Library and Installed; Amazon has no catalog of its own, so no Store. */ class StoreTabsTest { @Test fun storeLibraryInstalledAmazonWithoutStore() { assertEquals(listOf("store", "library", "installed"), tabsFor(Store.GOG)) assertEquals(listOf("library", "installed"), tabsFor(Store.AMAZON)) assertEquals("library", tabFor(Store.AMAZON, "store")) - // An old saved All tab opens Library; Installed is a mode again. assertEquals("library", tabFor(Store.GOG, "all")) assertEquals("installed", tabFor(Store.EPIC, "installed")) assertEquals("store", tabFor(Store.GOG, "store")) diff --git a/artwork/boot-animation-source/droiddeck-boot.html b/artwork/boot-animation-source/droiddeck-boot.html index 353ebc0a9..55c1d9582 100644 --- a/artwork/boot-animation-source/droiddeck-boot.html +++ b/artwork/boot-animation-source/droiddeck-boot.html @@ -151,13 +151,6 @@ body.render main{display:none} </main> <script> -/* ===================================================================== - DroidDeck boot animation engine - Every frame is a pure function of time t, so the live preview and the - offline video render draw exactly the same thing. - Units: logo units from droiddeck-mark.svg. World origin = frame centre, - y points down. The frame is always 690 units tall. - ===================================================================== */ const DD = (() => { 'use strict'; const deg = Math.PI / 180; @@ -167,7 +160,6 @@ const DD = (() => { const eInQ = t => { t = clamp(t); return t * t; }; const eOutC = t => { t = clamp(t); return 1 - Math.pow(1 - t, 3); }; - // ---- 2D affine helpers. m = [a,b,c,d,e,f] (canvas setTransform order) const mul = (m, n) => [ m[0]*n[0] + m[2]*n[1], m[1]*n[0] + m[3]*n[1], m[0]*n[2] + m[2]*n[3], m[1]*n[2] + m[3]*n[3], @@ -179,20 +171,18 @@ const DD = (() => { const ap = (m, p) => ({ x: m[0]*p.x + m[2]*p.y + m[4], y: m[1]*p.x + m[3]*p.y + m[5] }); const rv = (a, x, y) => { const c = Math.cos(a), s = Math.sin(a); return { x: c*x - s*y, y: s*x + c*y }; }; - // ---- Geometry straight from the SVG const G = { L: -31.98, B: 223.72, slotL: 56.055, slotR: 71.885, cy: 111.86, dcx: 63.97, dR: 111.86, hcx: 63.98, hR: 71.81, ocx: 63.98, oR: 55.98 }; const ORB = '#1A9FFF'; const THEMES = { light: { ink: '#1b1b1b', bg: ['#EDF3F8', '#DCE6EE', '#B6C7D6'], shadow: '22,40,58', shadowK: 1, ringA: 1, ringGlow: 0, glow: 0, noise: 3 }, - // Keep the shipped Steam render black between and around the animated pieces. dark: { ink: '#FFFFFF', bg: ['#000000', '#000000', '#000000'], shadow: '0,0,0', shadowK: 2.3, ringA: 1.5, ringGlow: 14, glow: .34, noise: 0 }, }; let TH = THEMES.light, THN = 'light'; const setTheme = n => { THN = THEMES[n] ? n : 'light'; TH = THEMES[THN]; }; const ORB_C = { x: G.ocx, y: G.cy }; - function archPath() { // left piece: the "bridge" + function archPath() { const p = new Path2D(); const dx = G.slotL - G.hcx, hy = Math.sqrt(G.hR*G.hR - dx*dx); p.moveTo(G.L, 0); p.lineTo(G.slotL, 0); p.lineTo(G.slotL, G.cy - hy); @@ -200,7 +190,7 @@ const DD = (() => { p.lineTo(G.slotL, G.B); p.lineTo(G.L, G.B); p.closePath(); return p; } - function domePath() { // right piece: the curve of the D + function domePath() { const dxo = G.slotR - G.dcx, hyo = Math.sqrt(G.dR*G.dR - dxo*dxo); const dxi = G.slotR - G.hcx, hyi = Math.sqrt(G.hR*G.hR - dxi*dxi); const p = new Path2D(); @@ -215,7 +205,6 @@ const DD = (() => { const PATH = { arch: archPath(), dome: domePath(), orb: orbPath() }; - // centroids (numerical, symmetric in y) function centroid(path, x0, x1) { const c = document.createElement('canvas').getContext('2d'); let n = 0, sx = 0; @@ -225,23 +214,21 @@ const DD = (() => { } const CEN = { arch: centroid(PATH.arch, G.L, G.slotL), dome: centroid(PATH.dome, G.slotR, 176) }; - // anchors: the point of each piece that touches whatever it stands on const ANC = { arch: { x: G.slotL, y: G.cy }, ball: { x: G.ocx, y: G.cy + G.oR }, dome: { x: G.hcx + G.hR, y: G.cy } }; - const HA = G.slotL - G.L; // arch height when standing (88.035) - const WA = G.cy; // arch half width (111.86) - const FOOT_HALF = (G.cy - (G.cy - Math.sqrt(G.hR*G.hR - (G.slotL-G.hcx)**2))) ; // unused helper - const FEET = [{ x: G.slotL, y: 20.24 }, { x: G.slotL, y: G.B - 20.24 }]; // foot centres (piece coords) + const HA = G.slotL - G.L; + const WA = G.cy; + const FOOT_HALF = (G.cy - (G.cy - Math.sqrt(G.hR*G.hR - (G.slotL-G.hcx)**2))) ; + const FEET = [{ x: G.slotL, y: 20.24 }, { x: G.slotL, y: G.B - 20.24 }]; - // ---- Scene constants - const VIEW_H = 690; // world units visible vertically + const VIEW_H = 690; const FLOOR = 150; const GRAV = 6500; const DUR = 6.30; - const T_L = 4.00; // lift-off + const T_L = 4.00; + + + - // ------------------------------------------------------------------ - // Drops: exact ballistic bounces (height above whatever you land on) - // ------------------------------------------------------------------ function drop(o) { const g = GRAV, segs = [], impacts = []; let t = o.t0, v = o.v0; @@ -267,10 +254,6 @@ const DD = (() => { const ballDrop = drop({ t0: 0.50, h0: 440, v0: 250, e: 0.45, minV: 170 }); const domeDrop = drop({ t0: 1.15, h0: 330, v0: 250, e: 0.24, minV: 250 }); - // ------------------------------------------------------------------ - // The balancing act: one master sway curve, everything else follows it - // through springs so the parts lag, overshoot and settle on their own. - // ------------------------------------------------------------------ const SWAY = [[1.60, 0], [1.90, .42], [2.24, -.70], [2.62, .95], [2.82, 1.10], [3.08, -.44], [3.30, .16], [3.50, 0]]; function sway(t) { @@ -281,11 +264,9 @@ const DD = (() => { } return 0; } - // legs hop sideways to get back under the bowl on the big wobble const HOP = { t: 2.70, d: .19, dx: 26, h: 16 }; const hopX = t => HOP.dx*eio((t - HOP.t)/HOP.d); const hopH = t => (t > HOP.t && t < HOP.t + HOP.d) ? HOP.h*Math.sin(Math.PI*(t - HOP.t)/HOP.d) : 0; - // crouch before the jump (added straight onto squash) function crouch(t, amt, a, b, hold) { if (t < a || t >= T_L) return 0; if (t < b) return amt * eio((t - a)/(b - a)); @@ -297,12 +278,12 @@ const DD = (() => { const crouchD = t => crouch(t, .09, 3.64, 3.88, 3.91); const PHI_FALL = 5*deg, PSI_FALL = -6*deg, U0 = 12; - const ROLL = G.oR/G.dR; // bowl radius is 2x the ball: it turns 1.5x as it rolls + const ROLL = G.oR/G.dR; const DT = 1/1200, N = Math.ceil((DUR + .2)/DT); const SIG = { phi: new Float32Array(N), u: new Float32Array(N), psi: new Float32Array(N), qA: new Float32Array(N), qB: new Float32Array(N), qD: new Float32Array(N) }; - function peakFactor(w, z) { // peak of a unit-velocity kick + function peakFactor(w, z) { const wd = w*Math.sqrt(1 - z*z), ts = Math.atan(wd/(z*w))/wd; return Math.exp(-z*w*ts)*Math.sin(wd*ts)/wd; } @@ -333,7 +314,6 @@ const DD = (() => { if (k.k === 'phi') phiV += k.dv; else q[k.k][1] += k.dv; } const s = sway(t); - // arch rocking on its feet if (t < archDrop.land) { phi = PHI_FALL; phiV = 0; phiA = 0; } else { const w = 20, z = .38, tgt = 5.6*deg*sway(t - .10); @@ -342,13 +322,10 @@ const DD = (() => { if (sg !== lastSign && crossings < 2 && t < 1.0) { q.A[1] += kick('A', .035); crossings++; } lastSign = sg; } - // ball rolling along the arch top if (t < ballDrop.rest) { u = U0; uV = 0; } else { const w = 13, z = .5, tgt = 24*sway(t - .12); const a = -w*w*(u - tgt) - 2*z*w*uV; uV += a*DT; u += uV*DT; } - // dome: floppy hat with inertia (it lags the body) if (t < domeDrop.land) { psi = PSI_FALL; psiV = 0; } else { const w = 13, z = .22, tgt = 11*deg*s; const a = -w*w*(psi - tgt) - 2*z*w*psiV - .6*phiA; psiV += a*DT; psi += psiV*DT; } - // squash springs; falling pieces stretch along their velocity const tgA = archDrop.air(t) ? -Math.min(.12, Math.abs(archDrop.v(t))*4.6e-5) : 0; const tgB = ballDrop.air(t) ? -Math.min(.14, Math.abs(ballDrop.v(t))*6.5e-5) : 0; const tgD = domeDrop.air(t) ? -Math.min(.10, Math.abs(domeDrop.v(t))*4.0e-5) : 0; @@ -365,16 +342,13 @@ const DD = (() => { return a[i] + (a[i+1] - a[i])*f; }; - // ------------------------------------------------------------------ - // Standing pose: a kinematic chain floor → arch → ball → dome - // ------------------------------------------------------------------ function standPose(t) { const hA = archDrop.h(t) + hopH(t), hx = hopX(t), phi = sig('phi', t), u = sig('u', t), psi = sig('psi', t); const qA = sig('qA', t) + crouchA(t), qB = sig('qB', t) + crouchB(t), qD = sig('qD', t) + crouchD(t); const hB = ballDrop.h(t), hD = domeDrop.h(t); const sxA = 1 + .55*qA, syA = 1 - qA; - const side = phi >= 0 ? 1 : -1; // rock on the outer corner of one foot + const side = phi >= 0 ? 1 : -1; const piv = { x: hx + side*WA*sxA, y: FLOOR - hA }; const off = rv(phi, -side*WA*sxA, 0); const A = { x: piv.x + off.x, y: piv.y + off.y }; @@ -386,7 +360,6 @@ const DD = (() => { const mB = chain(T(cB.x, cB.y), R(phi), S(sxB, syB), T(-ANC.ball.x, -ANC.ball.y)); const bc = rv(phi, 0, -G.oR*syB), O = { x: cB.x + bc.x, y: cB.y + bc.y }; - // bowl: outer curve resting on the ball, rolling without slipping const ang = phi + psi, dd = rv(ang, 0, -G.oR*syB); const cD = { x: O.x + dd.x, y: O.y + dd.y - hD }; const sxD = 1 + .6*qD, syD = 1 - qD; @@ -394,9 +367,6 @@ const DD = (() => { return { arch: mA, ball: mB, dome: mD, O, hA }; } - // ------------------------------------------------------------------ - // Flight: lift-off, unfold around the orb, magnetic clack, hover settle - // ------------------------------------------------------------------ const FL = { TR: .34, APEX: -108, TC: .44, X: 48, ROT: .40 }; const H0 = (() => { const p = standPose(T_L), O = p.O, h = { O }; @@ -420,7 +390,7 @@ const DD = (() => { let Ox = H0.O.x*(1 - eio(tau/.4)); if (done) { Ox = 0; Oy = 0; } const O = { x: Ox, y: Oy }; - const st = -Math.sin(Math.PI*clamp(tau/.30))*.085; // stretch on the way up + const st = -Math.sin(Math.PI*clamp(tau/.30))*.085; const gs = sC > 0 && !done ? 1 + .05*Math.exp(-6*sC)*Math.sin(20*sC) : 1; const G_ = chain(T(O.x, O.y), S(gs, gs), T(-O.x, -O.y)); @@ -429,7 +399,7 @@ const DD = (() => { const a = h.a0 + (h.aF - h.a0)*eio(tau/FL.ROT); let r = h.rF + (h.r0 - h.rF)*(1 - smooth(0, .30, tau)); r += FL.X*smooth(0, .22, tau)*(1 - eInQ((tau - .30)/(FL.TC - .30))); - if (sC > 0 && !done) r += 3.6*Math.exp(-14*sC)*Math.abs(Math.sin(38*sC)); // hard stop, tiny rebound + if (sC > 0 && !done) r += 3.6*Math.exp(-14*sC)*Math.abs(Math.sin(38*sC)); let th = h.th0 + (0 - h.th0)*eio((tau - .02)/FL.ROT); if (done) { r = h.rF; th = 0; } const q = st*kq, c = CEN[k]; @@ -444,7 +414,6 @@ const DD = (() => { function pose(t) { t = clamp(t, 0, DUR); const p = t < T_L ? standPose(t) : flightPose(t); - // contact shadows const fade = t < T_L ? 1 : 1 - smooth(0, .2, t - T_L); const sh = []; if (fade > 0) { @@ -457,16 +426,12 @@ const DD = (() => { sh.unshift({ x: cx, rx: 150 + lowest*.35, ry: 15 + lowest*.03, a: .17*Math.exp(-lowest/210)*fade }); } p.shadows = sh; - // shock ring when the pieces lock const sC = t - (T_L + FL.TC); p.sC = sC; p.ring = sC > 0 && sC < .8 ? { O: p.O, r: 118 + 210*eOutC(sC/.8), a: .34*(1 - sC/.8)**2, w: 4*(1 - .7*sC/.8) } : null; return p; } - // ------------------------------------------------------------------ - // Drawing - // ------------------------------------------------------------------ function makeBG(W, H, noise) { const c = document.createElement('canvas'); c.width = W; c.height = H; const g = c.getContext('2d'); @@ -474,7 +439,7 @@ const DD = (() => { const gr = g.createRadialGradient(cx, cy, 0, cx, cy, R); gr.addColorStop(0, TH.bg[0]); gr.addColorStop(.42, TH.bg[1]); gr.addColorStop(1, TH.bg[2]); g.fillStyle = gr; g.fillRect(0, 0, W, H); - if (noise) { // static dither: kills banding in the video + if (noise) { const id = g.getImageData(0, 0, W, H), d = id.data; let s = 1337; for (let i = 0; i < d.length; i += 4) { s = (s*1664525 + 1013904223) >>> 0; const n = ((s >>> 24)/255 - .5)*noise; @@ -505,7 +470,7 @@ const DD = (() => { ctx.beginPath(); ctx.arc(p.ring.O.x, p.ring.O.y, p.ring.r, 0, Math.PI*2); ctx.stroke(); ctx.shadowBlur = 0; ctx.shadowColor = 'transparent'; } - if (TH.glow && p.sC > 0 && p.sC < 1.4) { // dark only: the orb flashes on at lock, light leaks through the halo gap + if (TH.glow && p.sC > 0 && p.sC < 1.4) { const ga = TH.glow*Math.min(1, p.sC/.03)*Math.exp(-p.sC/.32)*(1 - smooth(1.0, 1.4, p.sC)); ctx.setTransform(...base); const g = ctx.createRadialGradient(p.O.x, p.O.y, G.oR*.9, p.O.x, p.O.y, G.oR*2.6); @@ -520,7 +485,6 @@ const DD = (() => { ctx.setTransform(1, 0, 0, 1, 0, 0); } - // how far (px) things move during an exposure: drives motion-blur sample count const PROBE = { arch: [{x:G.L,y:0},{x:G.L,y:G.B},{x:G.slotL,y:0},{x:G.slotL,y:G.B}], dome: [{x:G.slotR,y:0},{x:G.slotR,y:G.B},{x:176,y:G.cy}], ball: [{x:G.ocx-G.oR,y:G.cy},{x:G.ocx+G.oR,y:G.cy},{x:G.ocx,y:G.cy-G.oR},{x:G.ocx,y:G.cy+G.oR}] }; @@ -533,7 +497,6 @@ const DD = (() => { return m*px; } - // Offline render with real motion blur (180° shutter, adaptive samples) let RC = null; function renderFrame(i, fps = 60, shutter = .5) { const cv = document.getElementById('cv'), W = cv.width, H = cv.height; @@ -564,9 +527,6 @@ const DD = (() => { frameCount: fps => Math.round(DUR*fps) + 1, CEN, H0 }; })(); -/* ===================================================================== - Preview player (or bare canvas when ?render=1) - ===================================================================== */ (() => { const qs = new URLSearchParams(location.search); const cv = document.getElementById('cv'); @@ -586,11 +546,10 @@ const DD = (() => { const sndBtn = document.getElementById('snd'), sndMark = document.getElementById('sndMark'), hint = document.getElementById('hint'); let W = 0, H = 0, bg = null, t = reduce ? DD.DUR : 0, playing = !reduce, speed = 1, last = 0, raf = 0; - // Soundtrack through Web Audio. When it's playing, the audio clock drives the picture. const Sound = (() => { let ctx = null, buf = null, src = null, on = false, pending = null, t0 = 0, c0 = 0, rate = 1; const bytes = () => { const b = atob(DD_SOUND), u = new Uint8Array(b.length); for (let i = 0; i < b.length; i++) u[i] = b.charCodeAt(i); return u.buffer; }; - async function ensure() { // call from a tap: browsers only allow sound after one + async function ensure() { if (!ctx) { const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return false; ctx = new AC(); } if (ctx.state !== 'running') { try { await ctx.resume(); } catch (e) {} } if (!buf) { pending = pending || new Promise((ok, no) => ctx.decodeAudioData(bytes(), ok, no)); @@ -607,7 +566,6 @@ const DD = (() => { return { ensure, start, stop, clock, get on() { return on; }, set on(v) { on = v; if (!v) stop(); } }; })(); - // version: remembered per viewer, otherwise follows the system theme let ver = null; try { ver = localStorage.getItem('dd-version'); } catch (e) {} if (ver !== 'light' && ver !== 'dark') ver = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; function setVersion(v) { @@ -662,7 +620,7 @@ const DD = (() => { if (Sound.on) { Sound.on = false; ui(); return; } Sound.on = true; ui(); if (!(await Sound.ensure())) { Sound.on = false; ui(); return; } - t = 0; play(); // replay from the top so it's heard in sync + t = 0; play(); }; playBtn.onclick = () => playing ? pause() : play(); cv.onclick = () => playing ? pause() : play(); @@ -693,8 +651,8 @@ const DD = (() => { if (playing) play(); })(); </script> -<script id="dd-sound">/* soundtrack: MP3, base64. Rebuild with embed_sound.py */ -const DD_SOUND = /*SOUND*/"SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYwLjE2LjEwMAAAAAAAAAAAAAAA//uUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAEJAAGPAAADBggLDhATFhgaHR8iJScqLS8yNDY5Oz5BQ0ZJS01QUlVYWl1gYmVnaWxucXR2eXx+gIOFiIuNkJOVmJqcn6Gkp6msr7Gztri7vsDDxsjLzc/S1Nfa3N/i5Obp6+7x8/b5+/4AAAAATGF2YzYwLjMxAAAAAAAAAAAAAAAAJAXgAAAAAAABjwC+eaeUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uUZAAP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAETEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVWu1SCQSiMSBsJhIEglv5aaJeRvmMLGAx41a1odhWJUK7SbMt+F3DJARa8xHUNPMPZF7ncrmCygIIQAMGCxgOZgWgKDgbqSgGCCmBhJpgcke/AzWKwUMgGPTCBo3RAbG1oHzjLwMriEDHQgACCgKCoCQNACD/wxgBgMAgDAML7gY6FgFBIBhIUg4CAYYH4ChD/AwQAgwoKURccYe6BgYGh84bA//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEFwYWXfygTiRMGhosMBCgxBYc0PKQMXD/plwuMThoyBfKRiRExMSZOf/l83dNN2qSYsF5aLo//+pqmfQ6RsDFTnVvZ6yUWiUWCQSBwNBIJNDlpprZQCY28bHHv1syR43YqA47FzM8hpwyiUZpML1CLzCjQ+xQSVmBygEJfgwZMCDMDQBbcDhiqAwYYQMOPEDpkb4GPQmCwwAw+NwMqw4DPUVA5YOeBmcTgZEFAGEAECIbANBUDCoD+KUC0cLnxH4GHAoCQOAwEEQ7AGBhCBIN/gYMAwW8CdDVMcYb6BgoIhlsL2A3eBEBfygThsRQqGiwtCEFw1cLlDDkDE6/5uXC4xOFxkEzJZqiiYnP/UxfN3TTdqkmLBPLRdH//1NUz6HSNkTJIzOcTQAAAAACUADchlzEz4U/4OTYNYzTJgb47Osu5N5uM3TGAgacwSADhMPqBizAjgM4wqQHkMBK//uURP+AAAAAaQUAAAgAAA0goAABHI1nLbn6gBuVrea3P1ADAzzATQCAHAC4OAJkSDAIQE4SANQSQeXMKBqUnZCc7AhYSlcHxMzkAVBJAjM+zxWaWln4rYQaiqtqxU0aagv9p6/N1n8bW5S2Ydq09vn5fnW/DPtWHYzagGrzuX/+/1///dVcq1bLn1P///X//6/+/8ajQGAx5llvRhISrDRZT19f+z//r//9QAEAAAAoixpdnoGUIUyb7YZJpNjqmMsaMbEZGhh1gUGAqAUYzoL5gDAJmXKAqYAYRCOMNKKKwGA4Ae1yNTIKGnWNwNbncjOFM69wgAzAtxHceYUmC3KSRVfrU1bljHOxM75OORl/9/8P73Lss6QSIioUqtFzBERWpm+obAro3V5d63DNjWEtyTMaUct5JABmWrRWJ0187ydA6haHbdy4pnviSKYQ2VpGJogXRhqgbIYOKB1GAoBEpgqgFMYB8AUGAuAFhgIABcYBcANGAQAEQkA2mACg//uUZMiABeM9RSZ/AACNxHiWz2AAFgCZCD38gAHTkuMLvPAAAQidDBQJWAoF8J4JAhQUeRCDAYEX2JV1frGcNh7T3tp+4ZZ36t+9G5DnIJnOkitHCoxA9JKL0m1hhrtW9r9dywtKDQFNFiQ8eCJLF3iIcpSEEWFHy5lk6mBlhcBoZSGkFWWxsJJOKL/Lie35kQuCmEmGQZRQM5qCDuGHuDUYRYQBgCAdGEACcYD4JAjAtMAEBswDQBS6sArzZml6jkXAhx5EBJCDlmKkZyoMFlEWNA4VmidtfD9/XWq/ebfG9Pk4qmFZ1ikKfWtb375zl7OKnlEu3bp//7rm1Y9uv/utAEKjHbnT40oemoNQXEwjk0EtQw4sJlM+6A5zCJgFIxEgAuPnzDax05MFMuIChlKw4FEiDhcwiElcGEDRUAkFDBgIwsPS2ZsnO7qAclAVMEIXciLfPeRF1xqcaag3GxGGWJGjgInkxYePDVSjHlhodc4sehhzlpruuz9Ptpfr//uUZD8M1PJgwxP7OfBlhMiReyZMDkR7EBX3gAE+EKPOuvABNXp9GmnnWdFu251as81V2qq3W+qrdDlzGnfPbslHaUoCAxYrIzP5P9MzwK04DxGTFQDRMPoWQxZxLDBfCbKAkjCuAQONYqFoJHnVjTeFARGIj4RLJcggQGhjgKfSAmgbZfdMmuv1GOzU2QWxSt/g6Dsx4aEgCNnPu/tn9T87G4KWo0///6DJMoD42ZeYYMsiF2DYNxO0yBYfWMNvFIzBmQN0wE8FXFgfYwJ8AbMBzAHBICKMAVABB4ASLPiQASjShgCzAKR6Q4AfgGojhjAE4uzUJKcyeJqxODJ4uYcS9davWes2Jt3s1637zAw1T2rdd+xvfT///9QjUN9LVOrd1MbTdNxESJpoMnhFCgyGIgGhwNGAgBu4im4Mojk20WsVS6NWAjTdUh0qLSHp8/XcJwxWtNbeW/vekuc/P9Jfr5x87g71vBH3igAFsWuagAAAADPi1s8x9oYOMcYN//uUZAoAA0Qcx8594ABDYpq/zDwAy1xzFj33gAFLD2MLvPAAwjMKAmowFUlEMoeFwTAYwK8wAYAhMJ6A7gMAxGCwAKyeYCAFk6GXlqVYGIiaAXgUTwnoPiOdRPwQRijeBrlwQ9GuKxWbNdTbg7tTD7WcyWrWA/fhUAARKHd4h3eIj/gAAAAAAALim21fdOmflFrPq652SgZ7TYTI31cDQVpYQE8egxyDQz3ai3xE6dR9buqb3i2kO1lo74RwGFfjjRpPDlCZVCAoGipg8xioAiSYbUFfGB5gRBgGIDUNAPo8ABGARADIYAFoKsKXvZTWWBGSjENUqQQgOIv6ScFUe5M2xsi6+vjNv/vO953jdr4vHEYqiy68mQAMGQyQ1DVRjJCBoMF8MkwJAAjHDBoCgSxhtgPGAqCUxQwDAA2aMIrv9C1DUJAhhNidnO1kWj1IXy6uevELlrCxTWNXz/JnWcf/FNzSVkxJHJoJFWMc3H8TYokfoz7sFwNHKJ2zBhQS//uUZAsNkwQbxRP7WXBHo0kKd2wWCyB9EjX3gAFTD6JKvPAAIxGIG3NU2jW342ISAggXTIARyn25QK/RoaWtyBJO0hTZebdIactxYkBEKjV0LOnfM+yXtNLtrHmwZAIwRH//ev0f/9vT/o6rdv6gAIAG0YAANDuCOM4nNrgdM7lSO0YTYkcw45K0RC1mg0GSEhuxnxVKSw4NmTAJWFjswrWt/p8tUY1decs+x3v7ADALgsk2HPWYxJNLZMLHjdDGVhX4zts2aMTaFsDCiwbswMkDrMCbBMwoAslQBLKAJMqgA7rN2bs1BA19gihrLxVlaRwsaNSqsJwnDPTCxB1Pq3zvf9NfdN4t853W3+Y6NQMGkyKYZuEGZjdg4GGyzuYAgm5jLAdGD2BiYcAGw4A0BgODAHAGTHZu7cpdlMVkEsqpNIXAXMhJ/TLs60NLkayXe6zuH8Rd/5vu0Hf1SkkN5e+KxNkAAAAIJEggxR1R9NCYCDjW+igoyW4DmNvVNajD//uUZA4AAwsURiZ/AABQoojozrwADJhRFLn3gAEgC+f3NPACrREcwSkCGMAPABzCHAOARACBgaQDgFAA+DFZ4w0h6lmGkAyKPxMHHe2HU6mdrmZ0w6CVwt5P3s5/nbw0HUjcGdilan79YAB0vrNAAB7pUR92rRz/XJpcDx79bZ1MVBjcN5EFxgCCgOJIGgaDgWXG19SQUAtpLD0seAjCuFjjltL42EKHEbb9D1Vq2HmqxFKYcOFoocfpAAoAAANV+R+TR1RYczCJpyMtrD7DT+aBUxcwYyAQTMYACAIGFTg8o8A2GBOgMwOAD3Cfp/GctXYEIGLmL5fD/Ow7iqPIiTCUpQHICv3SFbXcxzwE2uLvbWOrR///7Kf8vsiv3aQAi7bbdrbLKAAAAAAADd1AFQMvFAgE3HA0JVAWxZIjhFfSRVy8ieLIoekQpQyiVFykLopiVhgzoeTp2no8/3Pfd//nXi4oLE0JAAMZEB5TNHDmQ2HsmEMbQLrzD5QaIwGs//uUZAyN0vYcRBd94ABPAwii7zwASpxhEE/phUFEDiHF7aC4J3MEoAvTBAgF0wE8B8DgJwwAEAgAQB8Xqr00voU0AOgp65iD1qZCk+5IQ5K+WRmj7xavzfP/peud63NjwA64+5//6///oLgADEVEtMvUdY0nhxTBGI6MLUOkwcRUDCfBuMG0CoSCSMAcAIMAJUiuiX4v68svJGIAfRMVSXg4Vy3uTeyrt7Zs3jGtfH+6bhafw0uOHjykMGrQ7DLUb3QxHIYGMpHNpjAbALwwkcAhOXePPRMkWN0HboPGnRqxN3loRBzVlLpNxIEMGQdJAtbPDeBb/baLLuWpPxawfKngmTd9f////////99bDJUKdMku78ygiETFyINMMcDQynA/zOAw80lHhMWHgMbA4ZVxEJe8iwagjgr0jczDclhTXn+hyklILQA0juJ7S74ta/h1nMHtv///+r0I97dKKUlTDnC1A0XEFFNM8BoTFYDSM5FCTQFeNIBEFVJnZjoC//uUZBCAgq8VRJP8SOBSwohge48WCXhDJ69lgqE9DiKZ7aRwGDAGW0mJ2HaR737XU772MCUNhxQTIBUdYK01lMV0n4UDQLtwszpz5T0fmr/R/1Nb6P7v99ZhwMRG0Yz2a/IgpitrAGrjyeTrZnMgmyRQYgFYNHLgFATBzsRyFvciGC8Hwu2sZJ+ptYui2JnOQ8VBW8t9+V14vA4oo5WcPWJ+Mt9Hd/o/v7prQAAQ3IHdpG5RQdYw7rIzCSFXMVM7p9z8JICUimtpJhePe9i50lMnzaY/PL061OaXaIUvAMpANQMmTYcIO/b91+n6V9v9H1xf8Vv/qSg6pZMXMXg0fIzjHRDmMZBCMDNZ+poa2gmcgQGO06E10HWnchhyY9DbLKslXOBcjGE4rFzgmO3tMyz1/nr5u/+ePH1+yzs93+z+jvYj/699MYtENjepjRk27UGvMcmSmQH0zSYSOTl8y8cjD5MVAVUKC2t3YYdGIMQmlD2vuOytoDk1oW88SjXw//uUZByNws0RQwP8yJBLopiCe4wWCkhFDC/zQkE5CaIJ7jBYd9UF0iZljFKLFg6ZkB66k2atP+tfpt/r5LZ9lfa26K9RISqYUAE5j3v+GtcEQYYqtBqhBnJp4NAAIQ4YPEai71gwWNtrQkXDUFz5MaKi9TqBHC2+stldznXWmrp/3bO65/7/kU79/2f+l//6yGHvhSBmnHbAZEKQFGAaIShzOWGbiiZTdRnI+mGhAbKCkRhgqa2U4wiYWw7rOH7ljvMttXJVIqr7v/SVK5UffOCKhYodK/i/6n93f/2/+6z3/vWVVjBxPBMpu9E0TBlTFzRFM0HY6ApRIVmFwK47hFvYqgsstpMEszPRWIpdiaWsZGnpWLLQWWufHkVNeJV2t71/6FUVbv9t9f+rd2bX66k9AAMYpME9D8nD+aEqMsma8xSALgYBaA3hgk5niBkASa77Oddo4AmYJAxqwMwabQZ9jcX/JVsmg442DZhaxav///////awBgwqrmqxgea4//uUZCeN8ikUxRPaSTBKwohxe4wWCJRDEE/tIsESCGHB7TBYoTJljGMHb1ke9VRlMPGTQMVAY+yxkf0mFxsKwBwAw+8fTVBFSRGuOy0cpqLJy9JgeD5ITX0bMUTU//d/7P//b9BSGEmFjpkHte2ZHoKnmDvGHAUqTS7oy4jMCKCIeRuLYCQcSxLxOqA4mtZxVASNwWeTSe8oHEATwdIiayz///////f//oMM8Qs0SL2DXHFSMvEIU+gEe2GRTlaIxxkDIQEDjwj9roPDsdxhHYKTiiutXL8lbaDSm3sj1NW5Pup/7/X6vuZ0//7/6aAMGtGpzSEGOU1ZkNZMOzLwTnJBMBpg2Zc6Qs1JkxI0wwhl7l0+NR/n+erOYrvTTxeavdqWb17C8O7HMQu32e5ADD+BrNc3bA0yBsjNOIvPOGj8Ik3IkGi0v+gYLGawpRhXoaGNZmWjpMPRMNi7Rzj5HzEeqhG2j0b/93+zc7q/6v+z/14MIpFrDIDLewzLsZOM//uUZEkNwgARQ4v80IBEojhxe2wWCKRDDi/xgsETCCIJ7ZhYgYJyjtMSBKtM8mYABMFFoIAKQjNQZXw4WD2cF/bvL1xysY7bKnTbWw9s2nP9v0erV/+7////V6hRWExAheTYDhZNdgLUyuRQDCAcNrhwtQ+KgAQA7kLswuD1CEzxIwLJRjMXOgoZeQVoK7xf0/yX/3WR2+/Sp61f/u6v/oUoVAMKKFxjUMXVY0+oOMMlzJyzBigEoOA+jhPjBnzLLjDGkV3UUuAigBRIqVciQGUSBZORtcVfa2Fwqgm92judHO1tJfzlv/3fr//9/rGAADCWBYMkzIczCRmzIiEdEUM2c4zKsHCB0KoA3z2jkZqmyUbiY0wdnmedLIHQQ0bgY2cIf9v/p///RUyzGFrA0plRsA+ZaAIfmKbDWhj1KYRRGUF5MHxhrAkAr+Et2lxGU8SbduXrOjCARIGjjhTpvsp9a9P/Ff9sV/bV3/9X/SQxXTqjZEaeNuEOAxdxujeC//uUZG+M0mwQQ5P6SSA7IiiCe0kWCLg/Ek/tIsDwCGHF7SRYjeaxKWZIIm4YIOg85KHONTQnRWfIfAS1sDckCUG+g6Zo//3f//////lf/Wp/zBqQcIzt5zFM+PDtTGFxKs8IUCKjHszcDjNnTHjRIJIg2mlkiRiwMvgii7FBm7s2xzw5a2pmRoPW3/+n9Ov7fouV6PVm8AMFo2ow+epjHoGcMDwnM1EVMolhEAkxwTAIKEVYpnU4F8DIW860qQROb00w3rHMHoV4t2f9v/zPp9Pd/3fSu4WvMJ9DgDJOFkQyX0LyMW8B1jvVwxgzNDHDHgFV76v00kNMIDAvIjFDNyMILsWbF/f/s/7Prs7f1fFH//Qmf1b1EMOIpg1+UHzd6B9MF0rQ2iKNMjQFAiUQCARXKok+h5WYlZXEKjdjolmzy0kKDH+pDzptn/VytH/9/+7o/i/RoVhj39kAAK3HFGmzAkAIoxyFKWMgyCVjE5wdswLYAwaCauGspkEBAMvf//uUZJgI0jINxAv6MLA+ohhxe2kWCEg/Fs/sYsEPCGGF7aRYx++7sphEOClHJG3zBSec38d9ej1qT/Tv//03/7HIs0ABWBVTAfDFMkzKE2zhTV/hMGicMyYcETEgpSrmVyU7eTkx8DVyKTyENBk5OTeEmrLk32bW9ftx37GfZ1e+9Wz/Of6NQZhaYwl4UxMupZezNFA2cxUIH2PMZTHxIzgjMpDBIJegvS41ybKqRgWr0gsBJjy39nX7/+n5J398c977Pr/d14CrRUylUkQMT0XM21gqDZ7AkMbIgo0Ks1XYv8PHRZeXnhh24TY7KYWrSExNG91YNCgavczR9aKejPaKv1+ni+n76///+n2KACs0qmAril5lZ0K2ZfaIWmMPg0Bkc+CGQxdPMaIASIl2wcAMPCo1ybvLLoxGHKkPieWen3ADT6dhayGlrRYzVar5W//76Pv99CNVaEGDiAKYvH6hovBRqG6ZhIPhoELZg4DAqBqgSRIDjA/7WWA5yDJR//uUZMCIgiURRlP4MSBDwgiGe4YICIw9Es/sYsEGiCHJ7RhgVfeJiCRoAAsSapm79Vtz/6no0GBsh2fs/2Vfu+zahIzDYw/A0Zh+0NRpEPjKXg688+0DahRM9jAzEEzBoLYGj0tUTRkRoTSCoK9dKlIMCWPUJT5EO2IRTXsmjZ/nrdCdaqHti1nlrdUfdehjRn7ulx/YAJhvEznAylAcgQFxgmi0Gnigc9OJioOKqCADKmiMVZciAlAkEpjAsgeLQiAIS6tUhp4+qjTvcqHC7DTz+hDJdP1pqZHTElsMrYx41jdzF0NakmxetCteYjAch2I0GHosNB2HqjLvQ9AwYAF2AIICczEYu6a50ZAYHP1qBtGzioWbaLdVysmhcFAsZvtL0O7nNgUUoHmd8WLVvnEOWmlqA9pHn17bvU1XTZt0X2P6VGDQBg5hfqTyenSqYqquY2FiZ8jsYHB+YBAWpqqswVm6CieHMaciODpUehyUlCAERadv0g+vmEKU/6ar//uUZOcIwlsOxDP7MLBFIchhe6kGCmw7DC/xIsFfiKEF7hhY0uux5GhVTBgslPsTSvRb3NST/0btaDDoDRNc97M2ShSTG2ENOgTTrxs1RiACmRYZsqCY4jGJCYcdmCASibQnEuQU3qm60GYt8wQJysWjQSxwLpaUPzV9YnXIl7FYo1jcDr9L+zBGIwOQsKLb/X/yMgv//tGRWP+dkw9RCDlKm9OlEPMxWhzjO6LPkBNSowsCAUD0tUeG5HvzqpOHUYsSFj5b3122SHHCqjhgw1cKhilioRk3XlwkAUmVFbQM5xxrioGtQNL3meaFB8JUmDjzzT9cVGOdUhbWuvQ81QApLJbbbI0gYrEsZ/VWZDH0Y2DUY7iyZ7K4awKSaHqecI3MbE0sbfNkbPHSahvkZSFWZTBsRMxfhLowUwECiY4gHNkZ0CSa/HCsaaYiALqN9ZDUn0w1mNXMXBMVgAqnEMd7ZttmsgByxt03UApGDEha8NROpU2WgQipoBkjCLJD//uUZPcO8rcNwgP6SSBQgghAf6YIDGRrHi9tg9mMiGCB7iRYgCKJCF+UJSKpdIHCgIoACg0AkDGR09Fzs0YMwFnjrtqzl6n/kGU9DMNRGZ1Wu0UKfl9oJicCSKMyq7rtmlmpuUS+ipquF6/qrWu4LIqv///kyIksVAFlgAldNttgAAAwIERkRmynComJPAgDY+UPX/qQAEhwAekhD9GZ7RyOG+KgApJKyukAMowBOcEcPVUxA1WmRhPHXjLmJIrmAsHmgAImCIOGNY5mJwSDoCGFwGsnTvjFdtL8dACiAuKwALDAJgEURA/1fHzhmbX9c4jYbdO4z/e3uWaTP1vWMyRYti7gnQE///oIKxp20HTFEYwJhkhbGngaZrA5ggEIDQUBFqF7FutDgmer6gKEYHgMOKtTJ0EACow9an11MMXLyzTa2iYwxLxfOLzYoTFmKcQ2LltVM+WF/DEtAxcyg4XUMZ5B3DIWy1UxfgCWMDnAGZwwIcDEAQFYYHsAxmAF//uUZO4AFro5zeu7yzYpIgodaSYZzLB7M5XXgDjOh6TCuJABgAgVqew+x0BBkTmEWieqJbwYgDs1cJwBYKBXdf2+3jnzSS7+RB4nskVNbfWecie7R0lNS2rFH3tXkCUl27bvTWE3aGmCwKAEIhx7mOsS5Ao1zADAHV7HKsHtuTC4/PbUMaF7nOEX//mv/+8AJYAAgxqnUTE3SAMmVuoyFT4zOxAxM6oi0yshSjA5AnMHcIswvwZjJpBTMAQIcwJQEFXmBcAY4I0D66jwjxhGZEQzKNzCUKwK3BEJayYZAR3o219vnkT1jrJZTcvS+W36+VLK7eWcqj1XtTuOs8Ma2HPsYX8q5Fm59KW6r9NK2fJ/y2h136tX0gBIGRWq+RheKdYcRVfuGG7wOhzGt9YaHkS1mUijaRiPoMeYSYIsGB7BBxngxCmYOMBWGBAgFioDAcQDUwAgAAMAzALjAAwEtBMBGikg0RDPGbiGFIlvqImAQg/4RAHQpqMYp5lrrrP+//uUZM8ABWgkw4Z/IACI5Iilz2AAFsyVDjn8gAIGDiKjPYAAxeWz7hPvNSKmfJ+sO5xKWXbdim1Ws51It23S47uVL26qgswWNSr3AQCLEmWOlyocPOJbGJWZCIscba4dSSfMuIJhEJJTpeKjDv////9KQAAEIBQAJDdUFNMgJQo2zzVjL8P0BMTZkWIymTmRMYEQJBiVBAmFMBOY1wW5iVAoDQJqw5goADN+SgSoNEwi1awxrUXEMagtUCRGgt8qhFFvOxPWuUsTcN9qOjlz2X8L/YKllnfaWjxpXCAiIAkI1kksQt/obuup7eO1Xf1XXs/RrQU21YzC+P7NZPds8Ce5zWMNJM94V07ThyDEwBHMJgUIyiwjgACqYVAVLIhEAi/aqi+oYjByvPxzFg9mB2JixAVu0luKtM1f1bTlrX613Y87+6dq7QcYeEEdv/9/s+5pNBDWv//6QAAVSQnLQAAYbXnZfBkHeb6NmkS50x8WmCwSDmJBt3muvI0x570n//uUZEYMEyofxx95gABEgxlt7ZgBDkRrDg/7QsEMjuRZ3Yi7qEzgM9RIwFLIH7Sb7UpbGYfkcktHnKsNCEDBHExIwr4MwMemDTTSFCTsylceHMdbC4w78aTD2H4MMlQA0SRAjALBEMdsSowOAJjqvzRCQMRQ7M4UCS1MADBRijC4RtwxoqxdCXyq0NuDK6Whpaem+htTOqHkcoo1Tfj+FOaMjSCyQxE8nVqTX2/////9P+zeAgWjCJFzPmcjP7kDE8dgAOhz0LYXGwCqBNGOg5MFudF4FhcLiMdjEpj0btxupqNVv6MnsHUtTI4zdd1q4QiGPyT//TBOFBwxx3wwN7H/DjL4DEAxkMF1OVhF1DFsQHUwXIgwMLXBcgIBnmBFA+pgR4EuFHBzBo0YMIDAAFGVWIHMkBywylwONq/AoVDuoyLAkRkVJ6VzsYqk7w2JVm0bSZZ20q+cdhOFoUU/kmGzLSKXNxAQjiYRUmYhO0B2DIta5i67ry8WSU1PcWeh//uUZESP1IshQgP6StBjQziBe0lKD0SBCg/0x8EVC6MF7aCziniqZRLpGkDAOYK5w5natimdm9eYvQzJgZGQGkufKYUgDhhZlMGRqG8BgTDosAFGMERCgFpD5QBLvZylElakWhIZy5Cd8aTmfQQXcxFIu2uw9NOjlPjfc0PjSI00BUq2f0f0f7v/3p+Kf09pghCJMaf2SyGUhdCJiLRZ2YLEXTmV2G7hhZgHqYIuPHHyiRGQB/GUK+GVo3FY4GHgTlp0n2WtEZqWUYGIQDLxLcAQCrAMhLcpytJdGal1PUqhmkimjJFyBDSMYjOd/bZjumApIyHmquJbFbn6tzH6/p6fx3c9vvbf1bzGI4GWYGiQJmuEymO2BUYBIVpnTgnG/iZkHAZCNBBYoAgBRCae2kP247aj0xOypkznoFz2EQw1x7lWWtJPZAyIIq3eC6QpzB1BLIwvWd7OOSWWDMSSEoxKALyNZMH7TDMANgwh8LmMWWADzAaATw1vcD4DPvmg//uUZBiIk7kYQwv6MlBFovkad0knTMhfG1X0gAD2jCRquoAEFn33ZVDSwDNV8xyAUU1LWcQHASVimYoQcMGBR5AwNVpCyEqTEix0qYBy0wksnTf2LbZvdV3xGt6lntyQwEN+5+1/mn9I5YAFopOIAAGJ5pGbTdG5fMGJgKA1EzF5BiAQYKw0Qw48y4xdKVbJn8LLiiKBUQsE2jUmCdMzsYw/Y/vJf0x9AhfqnefrTqCABbTjbMDRBMDIuB+U0lUq8MVYBWjBYA4Axh0LxMGHBCzBAwREwYIACMAzAFTAcwCECgA4YARwE6deYfhAfLEgoSKgqyTGW03XD3Jr5qlWhmiJXnKzwb1q/0Xbe+6U/6naP/U5H+okC5AYih+ZgwYZd6GLP6KhuEYaCggAwVmCQYBAZKKqqT00/F/C+JAluhovRJK3Rt1FSShsK3qWo8yo5FrBEgKMgEARKEDAbCPkzRgUoMrbmkzDvSAYzRgbrNBHFiTE+QsswWAB+MMwCwjA//uUZBYAA64YRbZ94ABNIqm/zbwADdhrFrn3gAEDiOh3NPACMwMUxFAIMMD7AdjAKgAliRMAZO6YA+ADNoYiEC7GSZyAFKM98T0vaMQMGt8brZ/PnNc4v5loC5ifIUN+33WN6Cn9ci1+aV9rNSQAAAHeHiIiYaOMAAAAAAADK+U78fOEaDu30zP2NJMDFg9dhfZL0WYk1iZEqGAgdcwUMds/WUghMSxBpMrhMiTloW4nj7blilLVksYZ0gQEAAAGPxibhnPJIuaAEarGbVinhlbMiYYXWVDGCRhipgbgBEYSGD7GAggTJgDIBYYCWBWGAkAALjKTcUoAF5YLST8nx+F8Vp7qrZoIOhKSKdQLZgRJo0bHmxn51j7hBO613//////3//+d7v//WAGjLbprrQLAAAAAAAAZSQSrDD+iayZNGcxGYoCz0OmMOL7wlKGkL0w8vSH3zOBIhAZOoz9mUk+iMtjbGwMO+HKqYgwUUG7NBvYvTYYSHc01YVFMJmFO//uUZAqMEz4Uw4994AA74umK7hgBy2xREDX2AADzCuOmumAFDFqhw8wfQCzMAwDVTBcAE8wDYBgMBWAEBoBTMAtAD0s0JEurWTmFOWRcbFuN+xXrK09ccUrNeaNWaoOi8Ug4PU4dJLTX3N/6LF+3/q+27/4W1awAbvcjAAAC4xMrCA1vgyqMTFglMXDotEnOUAKKsYSBU1jOst/JRLBwIRSscyzju3glAOPtVaWsOpOd39wMYZ8FyGYdI8JlcUFqZl6UOmK9grZlyIDUYJ+ANmBMBKpgDwBOYCAABoFGAMACxgEQAAlE79NdzNR1EVcEJIJ1VIqJRk07SaxX/oxE469iPs3/X/T/8l/+n//pAAEJ8DRw2DFP4j5TQDeMczCYcDiwyzAAIB4GUJacpggEaV6Bz6U9nuNiCsE5ZqQSkSWp3eypITy/RkfKOgAIDIoKlA03sUaMq1UXjAtjAw09ySqM0eEdDCiQNkwB4DjMHVB+jBLwFgw64KRMCGAIUynF//uUZBqAA80axA594ABL4jjVzyQACpQ7Ft3mAAD7i2RruoAFd+BC3rjHMSNEIGMXQsCGOAk5O4rOUrnD14V97riuv/bHxeYGB4rNsRGXCy5Jx5FyOhHdt3D5h72FKpJvoG+tzE/s//9IAlwAABm5rVGLQOUZOyJRnHEUmX+CmaqwpJhcAxFAHxguhMmBIACYpYM4QBBGtJ00rM6cUAIDZIDuggIquMCQ3vcEQzu////////+kBi3F8YvbCxxA/VHPzwKbICzpmCA6mskIeYLAJJgzA0mF6BgYGwCJgPAJFyhIA5l03FO1VoWmRMjLNL+WZLwmI1Tcy4ee2vZ7v/3f8Xbf9f6eilPxgAF1O0AAAGEQbGJskmF/gmGhAmJxmmtIyGFoAAUIDAcBmApquq5MFRqm/iwHoTixgteo6+6Ypy6J0LQywiL/WQAAwjgD6MoXVGza/kU8xc8DNMGaCfjEmQ1szJkNDMj6T4MXQE1mFCZgIW6UvDrP4qJpACRIGEJ//uUZBwNkncUxBP7STBD4liBe0kmClQ9DA/7ggDai6Nl3BidGmTEv8arZNMTWFA07Rt/////7P///9dAGBmNcAmJTOGiGMwkQwwjiSTQmJaNpFNiyFnauguJVoTpVfHoYktHgiRkQgcPFDzeytaTUQVbcBV2f///9/f/////oMCJB1zLUm7k03uhQMFfFGjGROhN/U6w562TPS2OKgsGnUyUFhCGQ4OShWiX2d0L6Q+z9zcez0By6bpxUKiUFjA86526X7+n//+n7/69n07PH7WJJ2BjCUhnN8R/CtRsMI5guSRqAiJ+Ia0FiCAxBM1t04VDPzQQOFhexWdz2SOc63q/rXVlzCZnJQEDDSw8gzOhEsNbBgGzKIhEcwZ8H7MZcC3Dah4zSMPRLAIImOnymRAADwK8i0IxBlNMiOLGVqWRR/2WoVvviqETDXLmm3f/6f/b/9vs/1f/6+ABwSpk9jbGyBGqZAIIYWDUM78M885ACFT4JzCERg2XrQnPtgjm//uUZDwM0pAVw5P7STA7QeiBe0wmCixJEk/tJMDqh+IF7SSdzUh86Pq0wTLL8+sEQYCQEcM7Nnn7KmVUwJADsMWtVWDU4h+YyUcFeMHYBqDEKAik+QwNzKDHS4woYMJHQ4AMJBGaWJVSbKGAykZKXkPLL8mBOKwuET7hf32qxhCju+7v/0f/9ux3QwwfhszCnZ1M2aPQxGQKjBkF5MLYfI8VcCijTPx4qZIUAQaCj5O5HZKGxxwgFZLeNOgyMB1dwpNoXvyqJAADBzhIUymM1vMk1Y7jIfANEwUsDyMW5B7z9S0zDEOlOzDwkZCFH1pq0RW3WmIRQpqRtlNE7ZfzzzezQghgtb2Ut2f7f02mP9X1//pAAiZuAAAAxBIM2geM/NjMz4EIxCKj2BbQoBIEQ+eMuquQBnAub3JiFk83J8zjy8NlNW/MlPW9qghuaeQoFDmHJKB5hzK+mYOmCgGAugpJhP4PQeMeYbAElkYDMhFMUmamy8/RgQYXpI9y+pZM//uUZF0AkmoUQ5P7STA3ovjad4YXScg/FM/oxMDJBqKZ7ZhlWEYFIXktm706k+Y30f729qUlj7Xd/9P/9VCAMCsAUwxEtDMqNnMS8PcybOP32TIwYtEGCA8UFrkLI828sEZytBQOKB+WxmaShK1oVSyMFPAfTLpjaAyNpqsMRvDNzBSAh8xqAHsP2CTc1AykuBxIYcUl6goAW5QT1OYswCN11t1kl2sOSipJtGgglhPc1GmtM7Vu7ea/szx82xzOV+gan3LAGQAkFIZSqCRiEOrGCQJADAbzGFCIOk8ZoNIZFkDOsFgKFWFxzjzgZkSTQDJiTCXxfvq098wtKrJgXoWcYQSrFH13H8aVxgxhyjuGM0R6OMDESw4EjkBRDME+43aw7/7yw7Q9zqAALyAAj0yaD79ifWn9/+b+j/7sm7XT//vgSBgaBEmP6rSY1bSpiOB1mg55jVsYIEhcYCIFPkBAEMugtaZA35brLSMSw0QAX/MEOs1fSjCbAgAz1Env//uUZIgIkqoOwwv7STA1gdiVeyYnSRA3Es/7QEDQhuJZ7ZhhNqQlVzE1R8cwSIGEMffApTrzIx2ZQ6BgQYWUlgHFAOll4CzfQSSYYUamTBAKmz54qPcU+7b1c2Y2kNCshtTn2pVkna+lAvqyObSt25jfSBFgDAaAMMUJCM86LY2lLAx2BE3yB8MIBBGJCA4DBFnNLceWG4scGU4MSwpg/jnak8A2C13NUYHsEemAXImZjDSTgYayJHGBJA/4VCCDGYwwsZEbeBQxJJesCVJztYSSUSSaUQIFHvE0UGWV+wj0buv1/939v0el/9HdcYJIsRj6K6GqGzUYuQOps+EbD8rkMVjy4K3QUCOo1iD+h/qI4EBQJQYEzx7HhaPATET2v////t////+uQAwqUG+M2+DlTdnzUMyZoXTMGxA2jGFgWM3vjOtPNFNNJNlqScTr39c8DMK0mnBRqNf5n79exBn7+uhfu/31fq7Ebv7e1e//Z0jQQMwkhgQiSGMShIYl//uUZLIIsrUOQoP7MTAyQdiVe6ILSRg5FS/oxMDsh6IB7YxiTQYsIiBTKfnIwsJzACPByHi6ulyhVHcNv4hHHCQkLXCCtztvs2/7f//732en//7QAA7pG5G0QAWeKkANMJsxGBAIFk90H2Js7ZYjQtM0K82a81606sE1ZNBECQJgwKV9M7DuUsbl9WG4vADOJBK3bh+cwsTbK1Ttu6ADEfNnb7CxyaTe6+zCxyd9YcHnX/Dgw5e/PSJYzoYyHnwH/+fAGEoD2ZSqVJtFJkA5Ns0g+T4lGMmDEAmkOi4hBphQHp1ORPcmVHPEIFYRRQPqmXrQjevqt///dZ/Gf////9dlAAc2sttsiQCKBlxxDqUFC5gxw1sRcM0C4ywkOLO5MXEMRsVAMdI01jTeLWssQlP1Kpt/YdeJQF8mBFvm7oOsRdVpTXZa+rWWuxBOZK4HwHNntDonbVay7VlIIz0ZJj4yVZCTaXOnoTFS17LpytgXa7Xvlk96gKjS/Nl/9dBy//uUZNmAQlQWQwv5MTA8AViWe4kWDcyfP65pifj/B2HF7gxgbNhed/nBQEAkAAwZwnTGcSxNMMrAx1ggjKpwPnGc0YMkiTMoGRXSugiHaJYcD4lq7kgRElW9vL7xV7HEC4fD7si2kdAUMBHDwDBqVAUwrljnMF0CTDBpQdgzeUElMHXAMDA3hfc6wvTNadO0WExEEwcMjLoEFgGzVGVR1XzDUEiM6esoQgZURi6rpPjnnsuAqMNTtOW4xMxC0hW6j+73f/1///t/6rY3LQHgMAoL6tFpwF+8MXQut6AN4xQvSRE1ItnRzC81jRmsCCTtQPCfaJ6nTMk3Sh0KoyOlkDv0TjOC7sI4p0szHZXVMPx5IyjBODHWK1MX8GcIAjMFMYAwagcAwJgwEQGmXQMy5WJYGUuW6AyMFHatYlQpWS2FPiLq3g2hubTqNi9oMynxIHiFNX3ed6qO/96VP0aWRmjTenmBsXrFEAAApqNuNAAACEhi7wZZAIAiUlMrJAwg//uUZO0AlCMvUutZYn48IciWe4wYDHBpEk/xJwC+i6f1l5jmRwQrYq9TrNBg+JS9r7TyWjTIiurPEh6hw0Ew1HzGjrpl12766KmOCM9q3fMgH5ENVeTTTZB2XU6qAubM2cPITiVEpAxbALgMHNEKTOmwjkwbsRgMURE5TDiguMwGMAaFgAMwEkBVBQBGYCUAQmAigBYGWIKHA44C4MCVA+URiDgCZibQueFqDhiRC0jJFQsi2FEgY5xsTguYqLaREuqNkD5knWYOo+i6CKloJqXRroIIJMmksxTXTR0mQT16nSpUmToLTd1a216LLVSpKp1rM1tEzlgjCTIQLBkMEywhtEZECCgSef//7v/+xhrzpimqE0CZbxfZpcBiGha5eYaDyBicg3FYk5mfiSmCABcYfgR5g2AxGBkAgPAPGCWA+KgMEwDoAAZNAvQA8FELYeBWHhUhosRPwqCmKZDILnBet7BMwk5ZaatGxT4hTAYLtOh2OIhppdgjYEImUKFT//uUZPEAA3caRJV54ABDgwmdrbABl9kbCBn5gAJCDaGDPPAACHTChZA1rqCQwgXWdrcm1GWAdKC60XOxwhVGVJ//6X0ZAAMDDBozGVR8Yz7pcVMbzDpTAzg54xwkYHMFUApzBEAVAwwEFEMDQA9TATgLIwB8AmMA/ADlSiQAE366l+k4HKLGqqp1b6hepyMyPbPabp9z1kn1nVK+3k5swz/////////QAAA043JAAAE/TIazgDjChAqiMwPL4qCoxQ7lep2vVYjbwEEwlNjWNtNHVMDZuv9bospaLxnmFggLJg4gruYrec4mVXMjhgyoiUYBCGTmRdh5xnWMph1KBvaARg0OohK0umhq3BWNThtllRJsjPrdLGY4QQIhA2zqe2z9q5vI7Z7f9z///Xu//jb/uZ/9KRBxAwdB01QMI5bgwx0AYxbJY5bIkx0BMKSB+GBgSm45MEPK/b0O3TPHNTS4RqVchzTf73bedgdx6xVp5WgqIAwO8MFMekQgzaB0//uUZJ4Mkx8aRJd94AA3Avm97RgBiuRdEk/0xYD3i6Od3ZisQ8xngKNMETDlTKDA8YwQAB5MAwDQDo0gwwHNWjgiDDEBN9CUonDL0ZJmxVAoGRGTjTIjDQMpqbW37jeI5HhWzp2f/6/9Vn//q///6QAOuvwAFXCC4MM4QUElUzMnWAgow4g55yYZnom5bEorHXhi0xbjNSmnMVuiBtGdZcJh8LyleSWRAgOJtNMwOR9DIKj+OECbUyiQrTDGH0M8Yp8yQmDKOmN4FgECIBIMcAQsAkwmTw8zl0XtkdKHEjgdwJ7qXc2zGiBJ4PCjffdb/TrT/3fdkv/s+r9ZYGIRRgaRjpC2DMEIDGgazdQIAkYCbi0YyMCEuJxz4w40RmwIcCQhhN4SIORL81VCvRJVJAADBBA6Ax69M2MF+h8DCYwbIwMMGMMm/BXzQENTCVtDnATQQLxZowCBduoCA5Dm1hLJrqYMWh11BlcgIFw0RjhmLWdyuq2gUeetc1GJn/tl//uUZLQIksAWQ4v7ScA4AulpbyMtinRbGU9wRUDKCCNJ3QylvR+c06//Z20AAAAxqKAAABGgQdU8+XTQwJAJGNMJVdgAoeQKBEobBQKVYTIh0lAGrGr1LyV1g4HkYoIgjQTFFIDBFglwxlNFFMlTclzHfgfEwSkH5MEyExjB8lTDUJTmgGjAUbigWAUCYCAhfqCFdDDlgW1jsPEl0YXZBWkaSOW5WV8puhJh9m5SbdIx+dYxGv+rsxlE4QR2+r/T6b/vwuMaQTmkb48lioYaSBqkck0AVwuSUUrJWmQNAsDsN3D6QGLMExJRA5kZekJAkZfASWkFJRVTALQBsxn5RvNQqA0zLcwUYwlYA2MgaCpRbCDHUzzn4EzBolDAEPiqBANAZHWwrl0Wf217smEqa6JRYygVBFRXN/9UmSzbOMvShhWdWr3o72LJoz1l23GP6qqX/StS1tcCsSPlGChlckkgBACcYRixQ4/kYYQFxgrhFmViGQYYdmPQJ2B+FwAK//uUZNaMkssWw5P9SVA14gkNcyklC9hXDk/1JUDSC2VlvAzmEoQChQAaewWHm1p2fM0mAsilpV6tTmOYMN1hoNm7v1//9X///////262YRqBkGU1IThrpLuwZKEBjG6NkHXs5mXYWGNDmmzAigICgMLIKEEiAJkM5GYgu1NZiUGGFIwRwDlyY3cenv+Y0ukbdL31Iy9FC0XovpawT2OOVf6nV0SqnI5e+6QU1ayVgpUhTAGAADcY1Ynxl7rKgYugy2qzuTyGiyZAQJikHLeMHApHRWxMFpdG5cWrSZ7SoQCDiIpAxcLzclkMZu3////7//+3/0IwVUMYMZzcRDBvMpEwrcNDMEdBHjI1wYg0HBwwo1k1+HcFAkIRMMBQXVkQCggBFxp5MtUggnbUSoNEqYeAMSHk2U/imeWScSBJUmbQJH2qYHOEmDlULtNqi1J68NhdRwy9CZposmxgvGtYAb1HArOwkKiLNsf0tWAADAADtMQlO01ojkzIDBtOwyD0//uUZPMM01gWQxP9SVBHoiiBe2YqDJhZDC/1I4ENCyIJ7gxwMszgUIHcy8ULLmLnpMYL3YVHocnYFhdaWEosmxMzEv9KqaOZjhBD/////////pf/3fGIoQuaianp6qVBGbyBSb0qJ86SGWBWYhdYBDIkHjCAPRCTphFwWYAtG8zWvu7T6VGpAoGlLBQgNUv3fpuumX6d3tS5bLSxn/6e0WeO/d9PAoISY4A5pnUwZGJoIMYPoRRkBgLGOicy53xgoMyikc1AGvOKVBgyEjZA5bYrzWhSzaXFrwABOL///6f////+qkZGBbcxy59BOHPUKjJqBgowfUBTMVACvTEwTTGiAzVUDwAFpjYCAjA8dANezHAFJFpTFZRWHZ8yydNjlfIuZ2Z7ctAkWwtPODodBBAVaYTpfnkuDLORaczwDJuYsmR3K5ipYjDryzFKSQgQYkcgks2wCCxtBogDB7DUMT5QI1WZczJUFZMhsE7yijBBFMHDYHSpHIHBcMAqTKFC//uUZPOI07YVQYP9SVBCwtiCe2McCkRDFs9xIwD/iKIF7KSYesIfWHnShp9xiBjFccdsKIr5oXIvY06RtN5Hb/os/d7N1d6O/20U//91ffGDihCRkwAu0YBzG4mBFDBJjO/x8inJgyFhhcKpoeBYWC0wzCZsjVKRXK5Yuz1+mGQMNuKtEc5RwnFxIlRdotDC3xyYuhDmOJSJ1q76zazCZX9rZcwKnmIKKASJhDYkUt2+5tiBTMbIYMQFxk8moBnMBlXhOGEkDKZaAPxSGmLmgkPCwCFwNsDqLOjncH1gWh1FNq+dTtuYt4/fKU/1VN2J/2+n2fTVz27/sv/9dQEMCKCRjEcWnQzyaG0MdFFZDh2dOZ9QqBcxufANowQBTDQpFACkxIGVuUyeHRwJBRs5MVKRcpNQ1Ppf3drC5uv7cc1Gx/X2Gsdo/QRRY8XtV7px42nSuZWZMEYDExMzhzLJaQGh3DFpKM6VEoGgiBw8Z0IS7ioIk5saVNODKjo5aUfd//uUZPuMw6oWwYv9YTBQIjhhe4McDOg7Ci/0w4EbCyHF7ZigTTvpEBSWIMijvOe1f2a6mY/m9lPRZz1znPjXJBfU/UuxdAqyyYcwThwToxnufCGb7x1phwggmXsBGVmAJUPqhYiOmBISzlPWdBMU2iH1ZTrO1QDBIFFDohPklqTuOs2MFnOAGtZYQNPRXB8znTSu8XSg4bqaGFQvrUlE4zuh2iFJykgDBrB2MhJOEzRZlx4okwaQGjHJCUOWcMXAMQaQzLTpPwWp5wRPBAXDiYOvdf+3GYSe9i4fBhDBtmrVF60I96u2n+X0acZ2/3K/ivU+pKowWYIPMFxgxzX4IgEyccPqNwI2MdoHNGNjUag2U6DDQx0bKAUMB0gn8cOGnjmIXL87uf1d7yuEwVQoWDJIEu0mbaKixBicWCugw1tyLeNU4Wj7lmpwrYMZeWPDnNmh9Ax4uo/QxAzRiQYCBpJlCo6Gl3WHqHfG1okmvG54lUYS5gjBKJihImEwTNX///uUZO+MwrQQQov8MNBOoiiCe4kYDAQ5DE9pJME3h2GF7TCYzys2KWao3pYGQIXhISDDYHmWdF57Rx+23Szyv772/f/dq6a9bndZgiYAgZOkGAGWDwsRgggN2ZYi6cFicdleYj0cMWYc6NEFD16MHnM8IxTQ/n9YkGSbgeAxo2q13as1qemSbgYzoT+jTVp71PEdOj1KWqR5yLz/WupiqsqQg7GI6h4Z6KVRjqBIGNqh9lqYCWKHCMVULGi9U5CDYU5g0hQqbef1HfbB9o/kbB9Lmyenn9cts9zl2htWrR3bNq+r3M0vfXVRCwwicQqMP8Y1DUn2IsyEAJkPloYw6mjavjGPiOUj6FyiFrIHedWQRaF2qeVflrHdM00KzRuePPpVgGi5QgJC7ka7pyJp0uMGKeNcg88IrQTHMaaHMFH6ChFFbmsjL2qASZHzYEGBoBaZBqgJnzTmmH2JeVWUeBD4WQK8TMBTGxNUfOGYZr353UttEAZAcCBQ8KbScakn//uUZPOPQ0kPQYP92IBIgViGd3kQCoAvCg/3QgEtCCHJ7aRYOTfbmro3mj9LmBJVKOxCU7aEUp/Up6DalmPr202AUgBhfALmkGVYd6yB5n+jxmGUBSYRwDh3Fp22QfQP0YO7FNk4MOtGTRghIYORhS7SLUMYax9dDK2yOC5zJ2uQGNEJo8fMIEakZMtMnzZhuAtDoJiF6hVg4W1NMmpQWdfprYbAKZqqHAkTDMLzMchGQwuwmDJITG3DAAQCOLKvIFQbOHjsYiyOi4LvLwukyB9FiUdZYxz0mfqqRm57cnqt+1ErjNtNyUjsa/bu6/T3KgAFbbbbbY2gYsnG9Qppv8YIkG3TAZqkTWHQpkxl+mSwCaZl5EhkADgmR2B0ZuCfxm3B+mOKF0YjQaZjojxhB4ph1gbGGEB+NCnGC0BoVgOGACAwCPTTeHTiJcBmhOwHhN8IHSjwazw5cWQFjwwBVdA43ADjXNqIyHDHXACANLEQCc6Yim6dC1F/tRac01zo//uUZPkMgxsNwgv80IBQAVhRe5gQDFxjFk9pJcEviCJl7Rhgchl1l1KRVqX5A92guzEcbKypdK6lcus6sKi0ZlVDKI3GJfRTs9S0FDc3l/bFvC3zLG1ax1nh3nec1vv71vWq9QMPfUWv/1AAra7bbUAACaL+Ez75RCQz0gAI6JJxrGcWTJjUSAiyl0y0u+VjF/NqERYcaKa60qRcPpaUx3fcfcbeLBPEgkGZwngiDtq7AANId5h7dYYl6cIyaOq10QW2PK4MGNMQRCUymgFjHjE9BwtRiODWHDAoGYewMRiqEYyAJr0j58sWJg4RgXDUwNFU1IFMFAICQAdYoAVwYS06PyaV0Ft/ZuzdsU7+yKvIJRF4pRzPLQQZFQEVEYJbHVNSW/8qSAAMctskAPwIACZYlDKwMjNY2mepQW4DuN1Yu0uG1ApGYLiW//QPdlFlXw/d3OcHgAMV2TUyNF1jSKQiMYMP02V51jCJBbMGQG4ChPmZIL6b/wZBnWWRh4EZ//uUZPWBFq1Azut+ydZGYyotZ0xVzrhpS+17prC5CGm0/DDfkObB+qeJjaFBg4C7ODBVOjCwES6ZcILgICgJlV2GmvUmNCAaxo+5n7lnNIVRZCDbzJDv1WAAAAuRy6AABCeNNFET4N5jZnyrMq06SN+TNV5naZlAK9AlYzVCe5974/xPD9EOJhA1CfiMFWGI4qaZv5XBkpoeGIsHMcNzNJiVAXGBGAeYIYSRigLuGlAF+ZZBRi4UGCXuY+rBg0Plv3ZMZigMMyEDdWtJR82maGqByOM+U/fsilSaGlzkntK6rRpv/2gqAmC6brJaTMTM0toI0xZhlggrGRdKmXq/oYdebVBwmRaKEeGhth1n5leSiTBKTwu59Q8DAAMIuC5zHvR/8w8YtTMA9AgzR3xoAwaUBvMAIAKzAFQL4wNkuOMMoBkTOsbgUEphAGZ4sopmIEgQNQsBBgcQYkJQQFjREEaY1NZpXkvU8eKBGdbs237M+JqDTdf1SnLAAEMNuSTQ//uUZMONEv0YxxPdMlA0IvpNYeYdi0RlIG9wyQjIi+gplI2OAAFScCqAsvOpZRCVzFhq6o8vahTVxbRyY+uqkkZ6m4c37CDjPv6MQo4m1AzKmxzBGHAEZhi7o5uYjUX0GGSgIJt6gfMYSIAeGJyGGYYQuxlH6EGmSOiRBLGCwAuYK4RxoIjhgoGYxQmJQox23ASgQgZiQWOBIkKxiNy6xqVyePyuxTWu2t52tYzeMqiWdKK3jG45n6P7wAA2bmhhkGcPZGc+BlQicklxj4FoIEGBifiy1QANPTvAw+h5yqUkB6irN5CFy3LIbbCTDkv8t/3MqDmBQ84e5SGOA3gGARFjAID4gwSwvmM2/C7DFBgoA3es6nMH5BGDAQgH0wFwBmMO5FWAUmRGaQZmMoRGIKgmeF8GQQYmFQKFszF4FTIIEREABZZlSPsAUTMq1yUNIDF/V163+/WyH1rdHaDligLA6esNdp0DokzLCf4tv+3TZv9fTr3qUpLInvrAACZN//uUZOGNEwYYRhP9MlI1wuo9YMKDjNhhEi/7ZMESC+Vpvhh05AAAQk816ETALzJRQa/6JjMBBQNMoETDes8QHctJIWAAdOFYUp59QEKM4Zu+Bo99cH3GOWzf3r55NY5LlT5fyrrW2YP2WFGApEZBkWBiKYaWG9mqFJXRhGAJKYDGAHmBMA95hOI9CY4KB6GbAtmKYnGPQaHxDxmRAHAobwECRhgWgEBtrrX15l691AR1fyYtQKt/4u7putdts885w052f+j////+sAIElyy2gsoCQ4sUXmYuo50oZeqUaImEqRZgk7xj4nQq2Vwoa9rA5K2FCjdumk+cu0U0VytdqV2qMKTEWDFPhtUxCI8YMCkFRDIYWucwT8EIMAIGcwsQdzGakfMAIbIICdMDoC0wZQVDQfDrMIsBkaBDNgA1FCAcENAaMYWFQgGlkGPlR26rxPZKrVq//aL+fN1InfmcL1Tfu0e6v97lCn//////3+kwFgZTKQYlNwSngw4TKj5F//uUZO6OE+oYxBP9YlBGYtlKc2YrTFxjEg/1iQDgDCg1p5kn9NMmUO4wUgFjAqCPMRSY0zWwtDDIRKwaYEeRv3DmCxawswEAjE5GAwrKwOvNIRj0PXD6BcVS+MSRfpm/9T81KfOOBkybR///1/0///osYMSIkmOFDapkEI9yYOcFymPNqShgCIG+YBCBPGAugRZgoIvmYjYBHG8BGMlAwbRTBX2CpVLrJymDYmCj0EAZIhDFEiE1gPGK73SqelmGb/aeysDLq28QGDqj8Yl39JDEjM3NkqOM31TnTHeGtNij1Aw4AFTAKBMMCgGUxwGITAuCaMDlswWJTE5xOJqwxiOxYMBwhKwQYzAinn4nm/oerGAV4CET8/bd7u16UeS3R//////pBSoTBxx6UxUQUOMSqGKzBMQj4y7BPTMF6AgiYA2MA/AyzCBAy8SJ/DDAGiYIzCQPzjgbxIjQgAWHGBAkhwNPAgo0tZuEyKh8c7gtDbUvz+Mz45hNg31f/T/+//uURO0N03AYw4P+2TBdQviAe4xIC9xhEi/xiQFXjGJF7hkg392r///0AAAnJsAAAZh5y9GXRZGB4WHeRjiwINda8YoBKZSBWPBSsClKYaBeiM/7/pqtcmn9Fl/oX8o8vEM8JQ/azaYcStUvEdVu8IAweQn/MU7NYDA+Ug4wnsGvNslYsjCgwVkwMoBCEgdYwfIVOMYcB7THUOjE4NTAMrDkc7TB0EAgMWZGFAxGGIIBAFqAJpLhyoQSDEoIFE3Q2lWqzzVSQIFn0/U2ff9/+1/9P//+f7PSBSkAMCxlMCTvMvxOBQeH9pKmEIDBxCY+HGFGR4wWXMT4CwecSbJxyukIQYKwZH81C67TZlxaavdbXdRcHDRLW1RVoAwaQSzMhnH1zCUzzExJoBKODuRBTCmgKowNcBVMBaA0DA8CWAwGwFnNsnYwaBzIKFNz2hDRb4MCRhkgBAWL+FrraEfL4CnCvPlzm4173wqrVmzAkPFv/sQ29rH/9f/+2z/6gBJw//uUZNsMkwEVxRP9QkBFoyk6Z6YPTQRnDi/1KQEFC+Rp3aylAAACEJccCnMAwXcGgamiIRoYNQBtcAgIAQo1MGR4HiwdAATIkSNAl2oipguOWy4DFXVZTYNvlk79SiRSi7ZjCGAHwxPkzTMn8BDjGHwdI42sWCMMtARjAxAC0KAYBgWpHUYsqBchiCMTBIx0hj7BZJBUCQIouYnKoQWi6Sk+qmq4EhDEwHRNG8v1fUi5ACDkUrdGdnN+yz/v0f//9X73gW5UDFwmjDdDjDhdDCcTD5PATE0BhIBRGABh01RQF4GOV2CijUtIg2cwNJX+oqZMR6eJeRQ2c+w6E7UlclRAx9qqLmaTAHhIIwXAO4Mg3IvDDeAac1zobMMImAczAoQA4wHwBIMGbCkjELQHo4gZMHNwtBHavokVJGtzBwDIoMeCZU47kWHY2trX/zN3OYNKhZ9RGmRPL7PV//Z/+j9T+2ugAARjYAAADBVMjIsqDMsiTFwZzsT/DAoIwqBg//uUZOENkxoXw4v8SkBAotkHe4MrTChZDi/xCQEFDKQp3JkdUAYxBXYSdcaCAAKpoclICjcMONB6uJXEkD40D/xtqm7vsXWSahiSaspWGBsjlxhHJkQAT38wbMM+N3BSvzC+wLgwJoAiMAuApTBTxQ0xYUA8M9BMyOMDGp/Ortox6GzCQEZgYKFQ0H0R1LGcM1o86CF6kk244vjrKERd5EVJ/Z5QfR+qrZ3drvYzov6/215pcDA+IAMNcj0ykCuzDbC4MouZEwVgSjAkBTAgBDNFWgkzkvy5QIAYykGmGnphwwQDRfMMMxbixe3iwvu4dByPSnYggg+QIu/lAAALtYRMXaY+uezWESMMT8gs59rJDDABjAIHGAwgmK/PmoI4AYMUE4ECsMNSHVfMQGQORLd2o+z95br1b/0klqfUepAASDIHc17/6o1CaF0Uav9XXu+///6mgAAwUROjGcFjMQM0IwKAmTJjgEMHUJMhAQXIYPZDJjSgeByonyFQM/MY//uUZOuMkv4YxRP7MkBEIwj6d0ZHTNxXDi/xCQEfi+JJ7oyxBIM6i0DCQ4eE3nfpfLbft5KQfOrhVUsGyhQx7goLSlSYCeAWGFMEvJjoIdMYTQIvGPvqlZgBYHkQAKZeMwGIXvMEiAiBs4YMCYSIArAGer3Z+ISIYKZLIm5rWz9BZuiVi7coQIBIusAU8xf02dHo6jvUm+5Gz92NZ3O/r1BmAwnQOzHWHIMmsrkwwAyjZ2hHMEgHowKwHzBABBMI020wlwNjVAxZuZJSey8IjK2EGzI0U+3/gdw62Oic6cRQFUvLbf97PkrNJXf/96oAbULSYLSCnmLvBSpgtZfiYH+KWmPEsBxgdYLGYAgBKGAfgSpgYgmyYdAAgBh0giCB440ThpHp4wQEs8kctUNahzgOhk4n2z7JX8cuGCIOs01/X36fvciRWjt87dG6XWp17WM2X2MTNgAASsAABY3zAZwDmAFTNsiTfbWyMGcAcxGCIwdEMy8lkzaFwBA6heIR//uUZPAIkswRRsue6BhHgtiie2NJS/RZEs/oaQE2DCJZ7RkgCMjA9Wop9lxgECqLLxvwtFWDv1Mvx1Y3c1Mat+UrMqrS00YI5GZjGTvGZ5TEYeaABnrCWOYKoB5GNYdmAggGgBen0gvmHYECAAzBEDTKAbnSXc5QyBi7o9KYdW5Yys2z4GAJ4qbaLhI6wkhn/usVeoftmcchpX62u2bHJ69iaEQA+gwKBjzAfGPMLUtAwpAZjoSPrBw+JjsRjoKNxJ46wNQgriILmBQwPYNSpnLT00kkX+f2nZrrwTeQUIFIsrE4cFgG4s9VaVMCMDwzFKw5IxasRBMIuEjTe4VQwwocD1MDKAZjARQDAwYAVKMQUAvTeU4ywNRkOumVnNcVwYSIsifZ5IKhnDIUO3ACX4ZmedN4Ofc1E6ogpUG5QqE2tPNW6Klmh+cqm2XLxiGVUaqVWP0/a5stJwAYC45Zh1GQmLaiiYFAIx6ogtCRURhQAYoEAN0Qx2A4z0iMcAAY//uUZPaMsz8WRDP7MkBJAiipd90RTEw3Ek9/ogEKi2JB7gy1gmvTha+WxIKCC144/jhPyHo4UAIZZTYdU33/0atf/6u3///6P+MMF0EMzEP0ZszFg8xMHxDgjtgjuMIIaDEBCBMGIRgwUZwDB9GtNtSjOikhPj22IibkBreqACwhOtwVkc/uNLP37lSrvdZ6klBKLlj7Ul3KDRDQLlrskrs4udbl0KNWMPWvcuWT2Mi4tqZtLu8qly0eMALUzBjDpH2NIqc6vD/CYeUODswsCgxXiQ2JGNKwrA4wGEIy2DEICBnkSLnsTlkPzbn/r8v7vVgPtY5RZtCb2B6j+hVADASga0xEQVtMaBRADCkwHQ6BkXFMQFAiDDCBrMAYOoxr2kjTlCPMaFQHPMeRsmYNC3ZAoMSIQqagOsARgRQPDougLKDYUe4rite3xSpG00OdQf7WdEUWpfq97/sTVtdV9MoAAYRGIay00bVvcZEHMckLWpg/gMGDQRhAaGVIwHFo//uUZPmM04cXwxP7GkBMYXhxe7siDeRFCA/7ZEEPiCIFz3RAYJjuYYBiIZZiOkEzauKAmrRC5BjLt7/ndbrVA+ccJ27pn9P///0AEYdaHRlyduHkHmKbPxUZ0fCj8YgYB3mCmgOBgXwDIYRCA7mMHgSRvhpiRgMOm9liAM5DbGARKhcp8OflZygsbZhtsKCcDqQLn9JyittAHmbacW9LO1jlTDss+9d2bU9qVy2TsRckdSZWAwsLY3pVM2g00xPUM2iIejB8BtMOhgxAGTE/PPmmB8oGBIRMinYFAt4YUg6zSS4U87/Ms/3/26VOXOBFd2z/T/3/+v/3//tVCgw7DVjXliCPjpGM2IymjcOWaswnMEcMDMAYwgCjMDdF3zDcQOo4RHATMYwig8OQ9LdKlApQhHJKKJdri4osGwrYtRKbHob1D0kXte7S73KGX6A7VXK//pa9CUiE1/6VxaUAAMPEKMfZPNOKSMUWCMmWXEwUwiAAAiYBYCpgglCmEyDE//uUZO8M0xELwov+0RBGQgiCd90QDRw1CC9/QoEgCCIJ33BAXJDGxghYmlHiKKz5DIJYtFRTeWtZayz+eCzDrKrK3ev//pMCIAVTCyTVsw69AyMQQDOTTEIiwwRIHNMEkHIhC9MFh90wkAkjMCUEiQUPTgA4aGHcXiYCEpBM8dOCJZr6t7eGOWrQVWxhBQum96xVjllmn7Upsm2tHvozD4sOTNvFRzmxhTF4rdQnRlrBWHgFSj7zDN6jYAzj6cJTMBoTT3weBAPY4BUicYMBXZh6AGAYkyQASjCK09GXuCWigp1YTXr3v/PfN9ytjDyWpX7cRR3/92n8x/V9qP1/b/YxSnvMLcH00Zn/TZRwBMJAoYweZfvEYJwYIjKDBLM2PROnAyLUFv2WCwKu9P8EAHubKMoclvSgEA7rlxARTVMPQxjXf0a78gmzFu0Xfob/RUjXl/aIlaK2AAAxDPIxsno2KmswLMY3KIRTCYA0MJgUcCBkBvjaFGg+om9Qs63f//uUZPAM0woMQovf2KBFQfiCd9oUDgBDCA/7ZEEviGGB32hQchw072o9lfL6g4scHS5kuoHL//t////0f//0xcQQgIOMWGOdDIbibQwysTpNfLlYDB/wdUwtA3jB7D9MboGo1VAOTXQoyQtMbHDpXFnSdKA8CAiSNPKbMP0eMRYDMNUbmdhJrvXfnUuXLCtKd3W/jvX7SR7W5R7mtc+35d4m/Bax+f/jaX7ilFm7vYW/hykzHVWafvkgDAUkwvxi/HLGGqFWdWLnBiIgamKAUYDABliBC5hD3SgGtNHoJfOmAGoZiF2hpAo465jig5V3eIln7v9z/Zf///+n7zvrAABbv77GAXTGJT2MpFEgw+hdjy8qxMT4GQ/YwcoZY9B0QlioEUFTvEIcduHadLaNZ17lGgds0OclzSb2Sly99jqP+m+u+f0faO2r9Nn/79PAGE5CmOlIm2MGmigfHU4cGYegFxi8EhgMMS1A0oGUqk/UqTEAHepocCFqkoozhHoR//uUZOyM0qwMQ4vf6IBDwXiCd9wQDxwzBi/7ZEkXheHF7mCIYsSitbrp1zf/s////u6f+/9v+wTJVMYZg0ZoJx7mtxnWYNZHpurpqEYYuBOmFsBgYJIFhgeKtGBKEOEA5EwBA4AETDQKqPfDLAlRyuvSy4Im4nDzja0JF/tWmU+7r+VvVV/F9a83H+7eVdb3Z/V8PjgH0bGuf3nOffdRr/5q01slPZYk5Q7A9gL+giBPWZz+vk988OYMQswuQBAhUC4IYj5GXjL2viyAMU3tprZCAwrCtSWVkTptnq16L/Uz1ej/7v1//9X9z//MAuCMTDZjAUxF4wcMMjBjDn5A0Ew8QB+MDxAPTAkAIwwUsTuMN9AXTFSAsqOHR1rmmg7jwDoTIp6mCJmlsWIoWKIyIRVbrfrKZt1eq7u5lN1kPffIzZ2q3s7TOXtIh9DSuxO5HM7UVHdWRO12q6LMiotc0zK0+UaRUEDx6sVJAsMSssNh+8M+jzPMpiwxGATzPQYD//uUZO6Ign8LxcvZ4QBEYXiBd9wQDqAvDM9/wgj7BeJVv2xAGBkNwH+44GF/BCHiok/MASkkB4zFqeJSN62Cc5KyVD6vV6dvTZkuv//V0U4p/6U5MPUUY1Uo3D2JIkNJwbw5X043MLzAuDAsgD0EgHBgi4IYYgWAMGoUmHAA1mfXgpmxKAyAStWG+z8mKhgRAFjQWMiEc0VWI6DGMjrQsJbVjEM2mXFRomhZ76iVFLJ1z1qWSUBXNveaXkQisqaYaeLvMi1MuwACgZ2lQYWUyYUoMb47fJhYglGHCJKDGYmIfRKEsOcsDBq4ItMM5ytWs777naWUJXX7P///+///3U3UzArImM4HFI4msAzMvPbN68XojCfgXcyjJYxCCczx3k8LIwwwAdxgwGTOIDVCWhvImg8jfyuUXvx7+9dq1BS8sAhOxRdIAMrNLSRvePvWpR5MVULsCupWfWn1ThVOkqRkd5wato0hPSgDUrSfYq4AIMJKSSNopIumK2pzBkYq//uUZPiM0/pZwgv7KjBEgXhgd9sQDnwvCC9/QoDoBeIF32xAumyVsGBwEoclUyGSHOluxWCXWtz2F+qDbZlTWDSCVCqE9Of76fX/Yj/Ur/7Lupsr1vQYpwl5rYq9m/pvUYfqLBqDrnqYQ+DYGA2AQ5gGoBoYCWKmmBOgWRrhqY+CAQgOBQ10M5iTAm2j3ZReEI8VhRQaLgwBAyXeD4aFgKKcYaKMMhMw1+9sV0AAypww02nOJQhCC7NaK7FJRJrpoZcEFk1FZFL2AJRDM4kHOS3WMmGMME6dEwEAZgaE40YKmKJiXquqEOKmeN6WSj7+6LcQp4nCgdc1cU6uv/uV//o/9v+hFGFjA1CRMozAc4n3CzIcMnMRUa8jAtgUEkAgjAAQGEwJwQtMI0AaB0K3IQlTvuFA+YKowrK1cFgieoEFQmGkQabLHzzpd97g2ggRSITR1Axr+owgNj6KkTMlgQgXMxk+eZWhkfPNNlS4q8sxh1tq0AMAxsMhIQMc6xMB//uUZO2MA3gQQgvf6IBA4TkNb7oRDnQvCC9/YoDxBiHF33BA2bNYGTgwiwaTCAUAoRC0bAzUQBsoZEYBAcKjdI8VitYzDUjZziz/b/10/6v/1df/9nySAGGuK8bBiaBo7RMmLOXkaFtCjmDmAsRgJoD2CQAIwSAJdCCC0wcAGg8iAQmwij/PoqpFr1ucteGQeDQ1ATApmetKIOumX9o69L5hKBOanKZYkNnd6nMewyQFSLo0kJK59uiBXUhp14u1UGxyiiUgEADRhVi7GN0OIYGIhhvSNSmE4CEYGBpyGQPY2Gq6oHoEIFyzjMAVkHvqqyp6/4rjQw13u////q///9X6PQpAjAWwXAwQU26MJVOaDBdwW02SNi8MLHBMzIgeDFMOzSa8DygYTFcCgwLQCGxhWKKYzizSx2bzvA48irPqtLnZ8E9LB50tWGm3ZM4ivYLOe4+y62ucYzMmYycYVFLqSyAiNWadYqjHpDZ9l9MAAASzGGAzSsiDAwsTqPT4//uUZOuM03oLQpPf0KA+YWhxd9wQDcQxBi9/YoD6i6IJ7ZSgMN8CkywGAxOaFFDY6TBbqLrZPepsZVnrLl/1MTOCLMzX1//+uv/d1f9f9H41qzBkCpNOMu0wELxzNoDFN9iRVTC+ANwwMUA/MBOAWjAXhNwwOgCjNK0ABUu2eFMjW1CNoeyClDjECBySUHzqxAcUPIse9kaZaMS0lDyihpZpRUu06RfPGBVd9bR6I+TlWqIkGMVSEn34EusENsVYE3KEIJmcTvmDU+gYoDsiEZFh5R5iMMChHzEgeWSdGhBxY5NTKNSa9zd9gZ9E1Kf///1U6d//6+3/91UhzA1gmswK05HMokAaDFPwoQ5nEe0AQ3eYiABpgrgTGCCssZL4LoYAWzBJcwoAHEmKSy7jqy7gCNseUq2OnV7XgyjTdUvBC7NW/87dwy/7x+/utPu51GTnkdKHyafxXIwv9zTjeLZtK2wSBevgcf/qfFPDv8kAG6mqAoLM0UAK6wzSUDcX//uUZOwM818Vwgv9EVA84Yhxd9sQDjApCC9/QoDfheHB32xAJ1MJsAoHEKVxDLBDu/VPBCUu7v2QqeYtsu0YlAQfc5rer1U6f9P/vo/vY5qOv/9dFXRmGGBWaiwKRz20PGeUVmcEQR9GGNAQ4Q6ZkEDhofOB7SG5gUAi8ywDgqHbkVaRRqvuzje/zRnAuVTV5hnN95K5ezt33L9Cy2tO58n/ff75+g3/L5fV9G99g21FdhjuYqtu+LJqm3O6nl35X5AfJ+6mlpAOY7kRsFAmikaa9CIBg+ANhhZACZdsNxopenkDpHnaLnjCwtWgc3RmGvd6KLuv0r7P5D/xXX/3/+4AAAvq/swDwtDCZSeMTFlEwORoD6Vk1MV0GQxsC1LzNr7PQiQyBVNk5TvAcvDla3kTruaQiNAaA0IbKRkBqffFrqSAomITyxdcVYHxG2kWDOM3s2/TH1IRULPsStlNLAGEAsmcafnCDWmZKHGv2/qYN4KBhga6RiM2Bw4ta7S4//uUZPCMw6kXQYv+EVJBASimc9sQDfQxCC9/ogjxBGKJz2hATDgLGjsRuyQFEJUt5+5AtuSv///1Zv/T///2/9/SYGIAMmJlgnhlcRTEYbOKhm00PEBhIwLaYCmBDGAJASZgYYuCYQiBoGaD4FAgQDnJFSfLTrDP5PKtNZa5nCM59iQkJX8nv3//uVwh7OzelkLTxtTmZXmK+PuayAw36mH1b9K+Z10lPqNxuf7WkhtESlnZedH/NVRL7r2CgNMSINMqbKMMk/MjqdAdBpC4sQh474mKhy7ZCVQAwIFilavEKK9kB1HjC0tAlvfr/mPVRbpt3f//R9+5PdT9SklBoFmYRIK6mQMiVRhpApcZUK6qmALgtxgAQEKQABZgCQsSYMkBFG2HhDIwBwGTXEjVyYn7F1RKKbSQ8iPRCucj6Z5yKRU/55G8JyNKf3+flSCWptfD9flyrdR17QOc11D9qsBCotd0K3J/9LSLv/IZ5P/x+7oW2UFECjwzoWjnt9Mu//uUZO2I0vMKRMvcyRA+gThxd9sQD0irBA/saMkBBWGB32xAMwyxGXTA2A6AoCrgklSZEe2D26EwVJ+fBVdBYoyY0mp69Gn/3f1/+zo/t0fo/+tUjMqiPNToww4qTMGVEeTIgWXIwS0FpMOiWMQR3M4L5PFAuFgfQGFvzIEGGT4X5J9v9Y1OEZsx3HXnnHpnjhxc1cLki4uLoZn2NF4kONduYLu7TIsNnHXkutirMFR60HU0NMIDAFagZJhhm4Heg0avgJtguOGEaCKZwcCi5orYTsfOMQSnpLbHKe0J6ECj3VRjTVX7blM6vt7TCHf2f61f//nvk8vVAAAKBDATAG8waURoMUsLijA9RDo1XRmJMHYBYDHEeTBIQDO5UD34azMliqBCwU9BxtYXYlNN3us7nHPaiW52Zp0AnnI07CcPqMs3f/s+0KU3f2at37O7OGGe5mSqVL8L7q/+mP6WN0b+t6Nv3/M09f+P59CABUGmMzIaH6xoZdHFUoyYYADw//uUZO+Ow7oswYv6GjI6wTiSc9sQDLAzCE5/oED+hOHFz2hAChgQAIlwa5Z5BbVAgrS75ESdw2lwudEhpaF/6v///37v70/2W/+796QDWaVRwAHMCpCRzFSQqUwXAITNYhOwjB1wJ0x6DABDsZJb4bHDCYagEGAiFQMMJgpgqvTTNGFkO0pSeRWlzvL0iGx2DSbC8PpOxZ4lQQYLhlBMMELFGYHd2i0INUbfrNOPv3GJmXYQDNh5IdtW2RNMgKIWG5heXivpMUj04TR9gcL+PGCBMAQAcctvRQ0X7pv7KOAFaEwQGqMidwobV7+j/s/3d33L9O3///S66hVjhgKwmqY3YUNmCcgWRyXRb6YbgASGGyBSYMgThgxryGMGFmZWWGCBIcBEZkRBTyz0timdJQhUGPmIFSfDCv0PzjUTbU8uK109DryDN676N/zzv99tJt8h9BqPu4/AylrDGXKzUkJb2wX+22/DTcv02sw1ungBUYmTGia8jBg8cnJ4QUHD//uUZPGIw58MQcP90RI74Shxc9oQDfRnCy/0ZQD3hOIJz2xAMkyMBgQCyAKR25Sluaq1n9x696kMBgTPhiNgO1P/b+v/////u2EkYBcAumCAA5xhDJsQYO6EEHJOi3Zh0oEMYYwHpgVg8GG6hgZy4FZf1CcMAKmCgB0oLW2zCezoQ40t1zK/SKZtxUralDUiyB96vG8j2fz9mWtIXgz8jf0VSO7/9mdMyv7Avk7Q9rfUlhvtq75eln3B/X02X17P0GPp8c08y9PDuMAMYK44D0rzCTAmMoBC4IE3BtvLZPGUabPfrdMTWbUaQGBVr3lmIR//f3f9Kvd//u/7P/qVMnT8NTLyMeHJHzD2Ayk4JRDcMMoAyzNsQDHwGDTUuz+sOhoUVLxAChiMGa4ZPi4T34ZZXpLaUTlbWR+YfhmdF9mnqYHd9HsGth23//5XJa6rVY1w+S/t9mv9NtvfrLVVv6ephHP7gZxX/ezp3Jum+2SASGEaH0YaBDYWElNtRzgw//uUZPCM86ILQQP+2RI2YShxc9sQD1j3BC/4ZUjnhOHBz2hAeAUzHjkhTMlw3NLZ2VIVz47tHPGQtzFTaLdOS7Jyz/9//+z6KO13+tUZVMgBc6DZjGGgpEwqMJUND3QHzBYwL4xkDkoC8xxv82xGYwsBUBAczkeLN18KWDQqMUIhq2RpI4fmGOTW91raEvNJWVZgCHb1LU1wTPODLFxVtUclSiQCFrVlE2WPIWOGrMnJRyb2ghuquwQCmUspzG4ZPNGNuhuYCIEiMsGDP4s86silMe7apGxyqKU3dud/q/cj0dnsPdSvd9X9/T31qhABdmA/hYZi4J48YVUJTmlTR7BgpQPCYCeBYCAC5MA+HOTBFgNo2iEs8FlZwMSKLlRmCd3Luc9rB+KGBAQrT+k4Lq/KYRsGYD1oHP6cih27jAjY5/lmzhg++l+VDvWiHStuWZLmTIWaQUU6eef/yMxHRWv54/iB8+Kcla4RR80io8nAQQBjOTdPEg0zdIzIZgoM//uUZO6MA4MLwQO/6BI44Xhge0MmDPwlCk5/oEDkA+LZv2RIAsFMKCAhCQpNgIkhucia9e/9TYMkUVJRQlTL1Mp6Uabf7KNn3N6N35j30IGavbUERgGoAQYKwE2mH5mBZg0gcsYe04gmAhAsRgDYC+FwDswNQQ9MMSAPCIqTCwAQO0rX5L7NUSSA2ZXiwjjf3/iXPNvKPyZf59tpodQ/OJ6T7E/+HUJqU0petPvnmUuXOe/PxAu0+Yr84QuoxurDqL77m/9M3xmjWu3LLKruMDGYz5IzAKOM8BOcwSAHi47yCCCTVJZL+PeSEV73Um3CZb+lNVr3dFm1W3tUj9sn7Hf/9f32dFUhjAHAB4wHEJaMZ2EyDC6wxUzyla6MFpBBzHwbDFccTQoLTzwBxYYhIEQcARjSAaZ0Zo6gxWiGLsL6+Un51zG9uLQYU07guj6UlXudT2mT2tX53/u38Py+6bCPSoenO7T+6I39Oee6zV/+iLBR3c3/a78ehIBKExum//uUZPcKxBFMwAP6GkI/AThhc9sQDwETBK/oaIjsA6JJz2hIDLHkMon821U7jChAhASgDAxI4RnHXlldrkBjARuoJtmdyAfCYx9f//7f+vVV//aFUVYGQAEwG0DWMRVEtDCIgg40Uw4JMHAAjjGkNzB4DDGibjbQYTA8EkTUywYDkjZwqpjDsm7jSpv4bN0YMqX3Z3fIfvGYHv3n6a3e31CvKPaR35Na8s32Vfy0/e5DIPz6tP/fE7vy/3/U3/vPfEExATTasfNHCY30C0TCuAKDnZEAHKAKktKjscZLlMFySB0wBZskNMuvMt0t/+ir//Sz2f//QhUAAAmRBULAEBgFYVaYMgRamCjgzJrqJQgYS2A5mQQPBwvGEH3CEWwwShoAVpGJwHvwBggNhijzQEdZyeHKyTL2O8S6ecI/Oz2nCK9n3+WTnIf60uEUyn0ued+5eUzzyzyWbF6E3/nWyfpnnkQyoLUXdNp79MRZPnIACAEw+jTpzLNViA5CwtQ4//uUZOqI054awYv9GUI0YOhxc9oQjag/Cs/0pMjbg+HFz2hAXoeSR4KEUYiO/c51KO048oOIa4f0VjyofaBfZ7s/Zb66uc2vU36lf//jdv0lUARMEGBKjFHihAwPcBCOPOBpzDTAD4wNcArMB+AaDA/xLwwwMB/LBYtSDRx7TqfUtypccrf0eWJlIUR22bEdOwnLsIiOHT7FKf6dThcOxVf8k8j8fzWZkVOzB78DLMLN+4coOEKrafo1/LviDueMWqv2fRjoF3mulGUQFAzDA5Na54yoYTc0NKMJABcOXI0GJrB3OapY+7HO/d4VYi0G6UPOFCoehwn7en7/9ZKnQe/uq//b7fm/QkBMAPABDAVAgwxgkDaMGCBzDZpSjEwgsCAMdgtMAw0M20EO9QQNgks8KImDU8NTCSfnhv633VrAM4wdDFq3gRyBQZjQM14qlsVTGrCji21rgC8g9FbDYw2FFOirOlhhpYz2GClw6XQSDBEJHnpqqXG7RjSgIWF4//uUZPIIw7xTwkv9GTI84Phhc9sQDyy/Ag/oaQj7hOHJz2hA/V00xDBRFF0k6iITj16IW9OJT3M2CNZtb811+zoX/9qNP+z6r/p6evpBKrUwKIwxsZZjUGBmB2JCcLDQxhQAomLhgkAme/57oojjD0YFh2Q5ctDmhCIEoS0X62vbdiRdRRuLJPKDbxtr2NdEGpbVzi7tCTzHmgw1iakIHYpWilD2JOvBcBAK65QWAguyPIwMfNNhqeMCAHVWgEENgpVlF+p652xTFM6Grrb069lemq/R2/1/5FzOp7P6v9MwIwEPMCpAWzDjjnIwh0XsNbphqzCTQeowHICpMAfAJDAbRvEwNsDSNmLzKwYwIGODMWz3qJsPQ0EgS9J5AoSoQJEg54ykvABgVePXaFBdB5UVQZm5kP2ghUNFrmCnI1BoJECxHCRJZRmT5oWFT3MjhoPS65s4MqHqJKwQGyKyxkrFihm1S6fNYAEnoYlVUDiCBY05yjWscMkPgwAm9TAG//uUZOmIA2ANQYv9yRA0QPiiY6wGC2glFY17YGDdg6NlrshMBAFQC0jAaECnMx2D6Vaz1vJDTLCaCCAXNqYJ68f0++j4xstTR6HqFpFTLU+h0rS535n/W3aQ5kcdG0qOYreItmEFBqphdSpAYCwCBkg2GBoomP3Nm8goJlsoHQHMGwgidz71j7NzO0BhjnJOW9f1ZHT335W08I/3a269WvubyzfJuOt/3rx1YIpq+un8sTQdWM7W15dqcG4NiqLL7bJ5fc1PymHGKYS1phdzmeyz4YLIH5gBhdcwGsM4Q/SV2gWcPu2HRtEgODsMttQ/r1L/o56MLP936N+ijTQvt///aigAsMBBBCDGehhUwhUOSM18Z+zBYAR4wrFkDAEaCQefAAKYAgAkWnQPHasyX5XP5b5P6xurGT6/WViuu9LUyMj0MHn/gpU+kUPf6eh0gTQ6RIxFaUVvi+5X/E5KJ9HdHS/U2koX++0X2ayjTwOktGZ9YcjJLdDaxCEKOhhA//uUZP4M5H9UP4P7GiJFQOhhZ54ADawvBi5/oEjwhKGBz2hAcR6AmWNUpKUwagJgMTWIDDo+BlErmIDYhynnGlbbhhcgjb3/f/svs5nX/+pP/1VmAJADZgLIOIYPOTFGA9hH5p7KZYYQqBumOgkGJIMmeU1HWAmnyAGKgUw4aWJRG1hfxqV+wRzK49wxAthoSB4a/Ka0UM14Oz1z+uD2Wktn1CfCmTf153kns9nYNtfKWfafHmd2PTUw/djb+D/f73bx7dhpAVfYLJh4akBDs1mypjBpAFDlFJmJYPqwuxXga+5iUoZHLC0ChObW4YB7fu2WWei3+j7//nv7/9tdMZjgxVbTD1SBswnAFINigNVTCVgJgwJ8AvMBCAQDAOhBQwOYCLN+ACAorny01e9bu5fj+Fy2lpajucUs9aK9eD/VCdMRnXXP28Vf/LfqPce3+2hMo7bmJO905rxv/5F7vC0lhPsNO3yDbWXHSTN7nl0wCqxgcAGfp6YSBZtfBaiw//uUZOyPw8E7wIP9GWI0QOhxb9oQDjQvBA/3JEjmg+IJv2RAiJQnT4LGRCS+0pyeRhZY2qWk3tDwzybNCfcyjT6O7/X/6LtfT/1BaGUTANgCEwGsCzMU6A9DCtQCc1zcRaAQhONJCYWgsYQ8Qa1h2UAcvRooQTj2386EiYikIfKkLs/COOI62qo1I3aR8zPP2xfBi96eZ0Y4+Tqx8BkZykR/vsujHE+tlncyOlnxPncs/JsV8zIvtpzRTW33qnf9+g+mJdXAABPdTCEFsMGUG43PydTChASCEqVwzuGqd2thH9fqvzWf3vdQTucapordYlWz9/2d9Or8d+3t+n5C1f0KFgIMwOQE4MYnJJDDOATM4QwlyMMeAhSZvzIALjSOsj58ITBIAlgyEGDAsN6YLAASwCBOCLKr7W0Hp1T+diMmww2dX6528d9BWW/NVPQ1Puj06b86Wfab5xPOsU6IpYzGZNqan0BMnnSLmRFn7hDT0OA39tvUaugOqe6ArSAA//uUZO6I04kMQQOfyDI2QOhxc9oQD21/Bs/0ZQjuhiGBj2gIA1S3KqYwohONQTPlM+In4w+AdCptQagp/cxznk3XLipcRY5Yu7vd17/7rbUlGDn+j8Z+z/7fVAqgDxgDYCuYEePVmBHgyRplJiqYOiBJGKgTlADGUK6nH4hGEAGlpkGTD8DH/lG487ni0qj6p/s62PKlU2MhkRz0OzKxr7EejE5NXsrWZUe7obuiPvRfZvTsbtV3bTe3SuRmtu793cpAmG2SN5IWFkAeQRHB5Hqa3ZGgurGYH4FgABd4wdQ/ppsPUsCcy5chJ7qUshxwBBqzdt/0f/0e6n/9/7/t43+2ACX4kBOLaM5JEAxDR+zwU4QMQAJcwHwMAABqYJZYJhxgnGcCqZOU7gm1prNt7gogqSFpdKJUQKWl7Gn4xhtAsqyWRqDgo0VDMeFxcZeWCSSyLAXePNvXFjQWALlGVxbetrporMhBdjqgAACGWZJoAi4wHJgshzEER4HAI0nl//uUZO0Og+NaQAP9GTI4QOipb7gSDe1lBi/0RQDfBKGFv2RAGx2amFV5qZTe94wYabxrVhch+l7B/q3T9VXf/fVHt2Wfi7N937NghALzAcgHgxpUPSMJWEGDI/WkMwCkE+MAEAawuAOmAYCgZgywDkdg400ADApm/nbEF8yzwnKn92rkI86gOwVBBEkqGV9CieRHkq7ZcHZXf2hzZINY2szDK37fC26abg6+Znw5SkzMVeonIX5GaWv2GTQnPNaf6qPLGfQ+Rzw7pBywrLBZKVm08ZlkAY6CKJgMAQIJ4cJJo8ckF/kKrQEWaL1PGukSA+eV7E/0botv/Vbrue2hq9K3//zX2TBpGMCzUwowdPMEBDOTG6U/IwOcERMLhsMLxdMso/OdgaFgjTMTrARPNXvdsTvcvs4ZNkv/VQGIhs5Zehy7ayq0+WM/irTwKnqYa1faNT+Dr73J35NKv54s3fP/Lzpdpf5+1+i8vt03Xn8w93Pnh0kFR9BNJbT3fsjD//uUZO0Ig0QJQiteyDA8IOiJc9oQEAl/AA/kaQjsA6IJv2hA0FQcAmOAER9iL2M5YSKue50VkopWmhrX+n9HT/b+3/2e7VR6vvsrVQGTAUA9MA8NgyIF9jB9JiPRr18xHAxjA/A9AACxhDBtGQ4BuY4i2V8ngVJr+E0RiYcLL0ZaSFlu/plnXkOZoTytabZXMi9pxdJT41T872/kdkTJXnCiUS8N7lzLIjhdUEcOmT0ck+qcJ+b3yB7Xz7glgI6bj34LeDD4MKDhfA0pWNR4wcwaACAxNTQQMBqsSu3IzpwRUhFppwueIizXDI7QRDitO1mjX///9//Tu9P01J1KMANAEjAMgE0xIUKLMDUBxTT5zRgwZUB7McAqAwxGPmYGswrgYWhYCRCAxgwEMETda+KI3HBSlZTWdf9yKkNbnyS3U4/w/zmEk1lWmq8fZkw/+8/RBznb/viWaXadT6v0SyS8n+bZ//DmSfHpd8/MbnUIA3QAg8wzQTD4fNm8YgME//uUZOsMw2YMQQOf6BI0QOiBb7kQDwVTBk9kaIjtA+HFv2RAXGjirgZiEisLsW3vBlUWeE8w1YHDygaUKvqp//9213t0/2jde+v3K/9H2lQFwAgwBEBPMTED+waBCGywD1JEJomEeAqYIQLBgioGGIGDuYCIChZ1HIaC2XFO24iCVnhO1Y3Bfb5NxGS6eea516XbULc9mTvvy1CJ0K8KnGyI8janTKL3KO1P5rvE7sjXnzMyMprqnctS42SZzSZdpkaNicBad70/GWmemFEE0IwLTZKFaCBASI0lYFbwYriVVQA53vHoYMcARyhy2DbBoS7Kn+/1b/obs/9dNKX+zaUSdo/xJm4xYpMNyjCMRYgwLUEINYrDqTCbwCoxkBUwLDoyBnI4jA1YVfRAAphADD+5fjz8e3sNCNy3t9NGCJSR3p79bqgDhlDHyPufrJM4Bsy61unfkxb5qvzCu46srR93mt85hcLJ+e9/uZfN9t+WEa6AgEXf3VKU3DktCowO//uUZO6J04sWwQP9GUI6gOhhc9oQD01vAq/4ZQj2BeGF7RSQz2bBQpI4uEKoK345cs1ud1gila6bGXUUp/bf9vz7Pb/Ff/RT+q4ITAMwEwwDgDjMGLGXTBSAik1nAu6MIoAMzIEGjEQCDNEizq0IBYKW3EQAGFwNt5anKAPGCCZzLIhV3po28TjeyGZ3g7TdZBB5nZz0e+hUN8UMJ6CUhm1B+qpecJe/QlhLTj1Al19PtE4tsFjbY7hTfAOncLBDDeUoACEosqtxUATKqkwh1PFNKMKATQefkKij43OZSOq12lm8Ex5axb9kLv8d93++zdMGbPr09dX7nHf2JjAPAEMwHMBaMP0D5DBxwrQ0SlCVMDDAtjGoQQMExjvjJtuNRhcCoCA5posWcDgChtyFC0Nt8s96yFKezFz+gttHOEU8rfcwfAhi14XMa2Mk6vjP9WMtoEETm9wO4/oY9OO08mpT/8hzvGLxa9ax4oj+zKPCokAiGJdYyysMZrDH5TbM//uUZOqKA2EMQQN/6BIy4Oi5a7gQDyh1Aq/0ZQjmA6IZvuRIAMCxBC9QhphyZ1Z6lmCYGNDnOWSIii7u7urYto9lrZrztGmv5ZuiWYlTDlKv2f0If1X6YFGZMQcYWZaI6BiTDvmzZnwYBgZJgKgbDgFoAKFMFYDpALARUOZNP7Wxr8/er8ot77TyogHVNmDSR5Uc02hpza8CnWgKtazptZVAy1zC7z78cbMhClVjSQZAGTHJ5+ouBFHTu8QGdAwFNnNwRSmJym4YAIFoiAJ7iCuGEJXYyipgvc90gkVujsymv0eu72kdGp9/t3bL+L//6LOlltdAQYAKArmEfDmJgS4XsYdEshmBNgpRguJ4JBEyg005gCxElFssoYkAuySfsUqMgU00IMxhTTpmsZrLSmbf0kmz+YwYs1Uzg5lH7PomVdcxDzvLoTKbEXiD+sUZM5wklMylyTOrG7e917+xHmeTrYVU/Onvr3U8oE+jUwJSVQY1w1AkMZqiKpgUAMFx//uUZO+Iw6skwIP9GTJA4Ohhb9oSDIAvCK17AMDmg6GFv2hA3UAWbgxUXC+gO+1bLz6L3vdSkqt4WStp6+ptX1IS7/p376E1v1fee/6kovs5CBDAKAJMEEJQyDFJjE3H3Oq34gwXwgTFIOjDMNzLIEzjAARoMkgUxgEO7bY5TFqqIZ0WUqU8/rt1pW5e5lPfvS/Cpl96rdlamR/L9Lf6pkZe6fGNipEWTK2WYWh59fBd+A//+fP/UP7vAtcWSM/TNpNbnRUSYxmeRk5JoJlyGCmACHAKbloCuqR2K9BKOFWKwWJKNvA6gg8UCUtRkFd9r7v9iV/+3/b6f3WKBFZRICYwTAIzNzF5McwZY/nsVTHCAoMVQhMFgKMYIyNghTMDwMUBT3GAUl6nGg8Eplxq59T2OPzK3kbq/KnPEkVCG+bwszPytenk6G7Tj2HkXFXhZuSzEidsj/maQPCI1YjcijuDB9Phw51LecM34TNYVyhudYPDGCdLCKEPDgcw+U0Q//uUZPOMw8xiwAP9GUA/AXhhb9QSDj0HBC90ZQjgA6HFr2RAo0FhaQgLRC9tByYSRlV3UhYq11aEtWYskEsOa+v+z/9aP/v9f/+tBQUAKGAjgTphoAiCYOqCZmp1kZRhUIDWY0AkGCkYQcECRPCA6QNXkYiAO/FfU2Kqo2bGWx8akiVGxYvrV0JZTciNL3pwjOMp1oo2sqv5xLnySH1unf/5NzlJCLzjmXrLf5KxQ5Wn25QzSJmTauVHSG14TtmTJCMWj0IRvWif2jOBgBbHlYZkwxlALnEEjr28Pt/aogipa8kwUIkAuE4qyY0/7/rpZ//c9Cfo/7P8XgAQlelAEywACcYD5fRg3ABHZ0CeYfQFwGNZhQFGO7UbhCasjosyBwfnhJqW+PlLnFEU9EIOGFOJfBhnvAL0aJ6F5ZHSPppkxVYelfdsln1CqMx5cMiGqfTLgIE/6teSwhf6kZzpHzwNqHueYPrEWwyFuy/1KwBVSVT/JeHaYa0Z20pQOEFS//uUZPAMA9NewRPdGTIwYOiCa9kSDzV/Ai/0ZQjVg+KZnuRAT2CORFO4TPSSCPecU80KxdfZ36Pt/5RN/2dHXt29/eiv7gCpEkrpAAC0yJzEzA4DkP2dkEwxwMDJgLAoIM6pA92BAUBVhkPQCDqEQ4QoCrTaGlgmu35zPIq8onuxd0zekiMR1Ye8d9dqxGVPedh5GRH5yXMihkR8TRGB+4k/OpkOexehzet6l3Ur3PqF3l3/Nv98OD3QIVIzNDqlxnKJkYR0jIJhOBCxYaamRBUE1IYyi1+aDQoKNHUbGf7/9Eo5306s181or//V/0UbhCAHGAPgKJhsALKYF6ELmghGgxgjQHaYmhsYPAiZJwmbjiEBglYk8gsPrUb9mtnhS50VzIL9OJoxLumvlUN5wxEOmTLayBKLN85BoZtauRkuP7m2wXaPL4F7+REZ0SCAX6UbwZnCivKEWZq9KAEmE5CeqamHUZvwV2UHFx4pnneSMADaWpbSjZgswWnmwVVG//uUZPEAg6g9Q0vcGTozALimZ7gQDn15Bs9wZMDTA6JlrphQCwDsiiIrcaVj+VPVFeskxrQsgihqW9vsqfSLZW0ds+v9uSpJez79P3fSwZgEAVjgTRjOoOGBsSAcmu6ZidhoGAMBqy4wISxTBFBEPARYSwh9TLiJzzxLUuTBel7wM3RRb4TVz7mvaWlree6lZXbMjz3Q5yEd1r+pxD6VLN0bPkhwlOdZuw8sz+ka07zjFea25GVxItIWXp5tgagtp3WMm1YTo0THcDAznQsBiy4gOh/AA61Ems4lZRGbGaTxG4WnvR7/9X2UKrR5b1f0+rWqARSuuu7MAEBAwBATzDDJjMEQVEw+oljBGBDBAqAIhMRWM1sEG5vowswSBKdurkEsyaTnO8Pmcmh7BnhMXxbMyXanv9Lx6/Mq2mtQtfuRS/EQjhG0F6nJB+oakWfllT9EvHtspl/89C7Dm+ZOvCWUz8FHoUBAALQNNJIWbRWZeWbD0gYPgOWoS/EVx6FP//uUZPaMhBdSwIv9GWI4AOiWa7gSDn17BC9gaEjBg+HFrqRQh85Xe7Sja8Z3kH9P9Pv693/yv/q0flK7Lu2tyzBQHTDsXDx5mDO+RDha+jMSgHYwCQK0vjCIElByDqYzO3sKAfHsn8MN/dyo8unqxscVKskiETA8MKCPpzbFW0jeXEZ/ttDpcY3MyNXPwbHdzbkqmaiPPHS8nTzXJ4v6W3tEYsipwkpyIY6FWMIqxDUrL8XhGRGkdBPCAWAENXBJVBgDPGLKR5RXph0CCKbkJyj+bmdyv4q5BFFYzLNIYFa1HsJTCv3d+cqbou/H26EIr7BbwC+lFX1V795zAGAWMgcpIwThPzqnoeMVkH8xIHgUKzNlAOtB0OESXKVRgEEv9WnWVpuX6PxZEaF5rL7pEK83p/yama9O60vkbLM7kJuFaiJ6+tHRvPnFLjRXmzRQkxFaSdKs5Dd3WPVYyyMcXDn2Pu77cZEWbiUps7oqfPWWGBQro0N+FrEWZ/DOe7Yl//uUZPSAA5teQ0vcGTA0YOiZa7gSD6GDAi74Y8jyg+HVvuBIOpQ9ooPEIuRFd23+3+ux7I72dtH9eb+vorALBgDgGGAgDqYGiI5hVBdH30/uYngQRgpgLmA0BIYCRNxg6AkmcymLlnsT3jCq2qiIZ7FFYzmzFXX7C39DpfGJbSb2Z/l34suCRyvFwZXnEVUizhOewMk1PnpCuRQzzRATyxrlEfhklMU5Un3WHqzcd0U2pm5JoGOvrq4xgJU3U7jNkJpMAvhwTJQOwQl+Z7DVLlIPNGygmRvC7hohABxA0kOI7G62v77epP6n0f+ub/2+rb+OtHoAIMgoYPA+YjjUcycYbEg4aSGGSCwScHAcJgDYA0YA6FcGCQgE6bLTHpDAGiBXw92igvVSfMvISzwiWqgOGOb5HmIMyQYnc7x26blaVy5SLk0yzXJHQ7I9jHsbrDqHSdS8ypoZpJxnNv7xEJZSGIp1bZVZPhlTD9Q3VbJXF6AQCJqqmhlRYwNgAlDo//uUZPGJw2YvQYvcGTIzwPiCa7gSD4l3BQ9gaEjrg+GFr2RAU1wgWlYXeJZItiy62XbU73jEOK2q0v1N9GgLZjQWv7f7vR/+r7uoSFFMBcAQeCIMqMMQxbgwz9aScMV4G0MWRioEGd5MC6OpZAadwhBtwTpoW7LIZ18i3b/hF45zNZmhg7a6TY4xH/zSTIIlOn6nstc9tP2fQi3y/PdP4qa/+cmrEX6kWYuj/FdCoG9XHkcJjgK9hlpRRsgAX/a0kaaXCZp+dcPoBhlWtAqGRWCcTo6gylzn08fdKzlV6F2e+iz7ae/v+dZ/oZq6rO/7UR4AZMA5ATzCNBIcwUEGSNEBLMTCDwJYyAJS3xoeun+x0YaA6AZHUxyBofll25+q+p/DOR87vykrffI8z3aZIXxCRY0IiFMjrzPKS1H6pWMQ6ZXqMW2lkNYT5ytShoeRMrc7IxIczjqynXM4DNQwGygNKw02JCgr6OfVEBC901P64w26ZeR1/vlrnJphkZEa//uUZPOIA+xaQLO/GMI0QKipa7gQDiUzBE9wZMjUA6IVrphQW2sbFVXSneRCb5Rj9n+3V9akfd1JXtqd/W6zYBf63P6AtzC4KhiCJuDI/Z4OctGJ4DKYQhYYBBmYi2uZuigAghUGSpMLgEZqKARbeDDBnYwBLSNqrn0ttLSg2Jp7kdofOIUM3I7G8zTfQsdCnrvJDlMiddjvlFUluS3KGjGvSN+Ldi2NVUkNvs7b7nBvlp7sWuUlMeDz8yJQQUkjT5Gz4GAtmTWGA0D19NlJa2dVeaqtVe9zJ08Lio0YlSK+vZ9TUturf/0MVijUJUwfQli0fTT/1fIqVOYFACZllk2mH4SIaj/65g6BIGBQWg0DzCK9zPgOgcDKkkJgWBWMU+F3erO6K5auquQnhq0yyzhkjkxkhJahsVVieZYMj8oIVPB6lf0cToZZNQkwxqe6E0tJLKagn+MR41BNwzyfzSfYzSIZELIkhPD9xdIiiLejmgXtJLKDcMgWdMEYDXTX//uUZPWLA9ZPwAP8GWI0wOimZ5gSDwl3AK90ZNjug6HJruRITAwGFM5YQZkRlFf5IjS1rY6JGsMmRqrw4JGC93UoUQoj+b/75FVn2/qqR7271/QtVWPlUAgwEQRTIGHLMNcck0T8CjDJCvMGDQwiMTMlFO7AJEt7GnhxGg8qS04Jhk0zMoZtvqv/OPS+E5Ull0/qEsKSqMmYQvKuZnsELvMFh6ixncUTLkYJhqTVlq0Kv/+umExHFK8kySUi5GO0i45fSuSh8IxKbYPMAgQKyLLZeY8OjX1OFYsMDwBQUXegJJggH5aHc5aK4ucgMVbQOQPT0ra3/3EPR+3fsb0dWiHLf+/zNQoA+BApjDhQ6MK4hI7ztszDEDUMhjUwIBjOTUPvDEwCAl8sxMRAaiHwxax0RLE4IkyvLdr9TsN7akQ4pFZt10SUxRRH2dRZd/ndQtBmv64PdCEHESnOn5tahdfS3pkMmuZUoRdZbMzBmhSEz1aUJFmhgmpQ/TwkANUs//uUZPGMA+RdQAPdGWA7oOhhb7kSDtlvBC9wZMjmA+IlnphQzbgQ7SjUUOWFVFg8XvDiSg8BIZUE9N7fuowumL9zTE/V7/875zbmP+n+R28+2/alzwuAMFgVzFbKfLAip4HvkGFqByZABxQKjH3oNjCkODyvU9jAgHmHBZm9HP0irFMs0LGJjhvZAzFfFmy7ISJqpWkvxJtb5N70i5u/ELhtnxQUOd+vyqRdBrI8+FemZxZmtpWP6+fGzmjJf2h7PZ9DKZDVDtzjAzMYwMzPXy4IiPVY+YpMs2j52fqjHuqMNx73gCjNXadW+aX939CPRv69n8Xv0u/2qgAQipqqtrXMXwAkwHQMjpeL5MPQAMIKZgoIGD8sZfD5cJYV+isJycmoR5TblS4JzXUnX2hQ+x5tdTyZc5Lirn6+sz+Q0b57n5mjo5Ekh0eZkefvqbRT+MbmRFxJ545zr2Z1FYjZCSMaUG61DONtHw9hPMAtLIMd0MQNPwjOJiNSyaYKhYNA//uUZOsMg7ZcwAPcGTIzAgiWZ6IUDlV7BC9wZMDLA6HFruRAHNYqAJr49qrZBle9obDi0que1rqR33f/94C//bqWn93/ci7UIBgBAA2FQHgwgUOkMBtA7DTzw3Ewj0A4MUgPMBQuMdYeN6QHWFZkQACYKAk+uH75lzss/dUlMMbgkHlNTUvGS1E4zC0IyNjM1Ncp1BVOQ6lAE3VCqUw5McZNadCtsavhqRqZJh+z4RXN1zNYCTw9rQiUKKJvMIzHvTDZyZCpkJJj5zx3Y0SEZpjkpxWIfcxaAhiQpa0IZFfaXLCWpFzAqeijAZbXqvQlGvk+LWlmttiZMWlWzd71OivfmdzJKY+Rp91Z1i9LqjARAYMBoFEwNysTDuE/PXpyoxOAIzIYEBw8M6ls8eCB4AyNeBhAByb+YZ/+E7r9EsM+kXSpVCYyLnYp+RrDzsMy3uGXKnq+wv1kbIrn+fnCnd5ezyfLm6nULWlUL/vlO67SkRUyMv/kR6V7mmfO8MOO//uUZPEAw5BWQkvcGTI34hhha6IWELGA/i/0ZYkDg6FFruBANIiQAjXPsn6aRySxzhKQjAoCV3RVG8iBqfQeq51qWY0aPRYSUxCvzHzn9RVOrY3fdX9Ox9//7t7BhFEzJPejDyCyMMEVI5AY1zCFBSMCQBNJIwMRnTDVAqAwCqYU6PAfz6WPkCPh3Y9vkMvZDdbJRXCNOuRF+mOjHNqN+0yyt19zPy27grGMr33ufOParHPZ3bDw9LTbpemXJsvyZdh7lbeZvebFgtwvSjmynk7KTxJcEdAz3oswFAxnskGQfTOGpSY8OvRtGDuWpQPXuytGv2odGfR/fzH9fs6qv6OjXQAgAYSGqJh7hzGBsPSbOGnRgsBVmAKBsOgYmAAUaYMgHhd14kpQoAdDokxUS4bU+IjGMyqE2KiKhGPbdXV2MkpNs4dM1RTnHqRioc77EYhcUC1xVhNtO8jR1jh8dp7yATqjaV0BtXeciQIefcmwuAfnuaNHkDdanAKADhKq//uUZOaMg55gwIPcGWI1wOh1a6YUDkF3BEx4YUjQg+HVrphQmzbmE0mbunFgDVVGligZoGV8IZ6o9D+ciMhdImljb9tFdky11X0tumE/RyimITpdtl6dqNbdr3wKb21F5SQBQwdAYTBWHsMmDWkwEgngKJU9zKXtO0ApqLQ1hAEQYEHw/mFDRKC+7/T3xTNYh67FCSzCf0SURFTpeQKT4c28/eO2pjldTI0PrRCU9XqepqVyYvKZ7GVL03C58vk3YnCXM0OOzKDU6dKKGSsHhbdWXGFgclgUDky7zCwAljw4KbK6WL+ovpZWdY75WbHjY59sM9HqnrFu2Zfdsd01PTfci0p1fdd0U+x5LtVBoQgHmC+AWYJY2pxF4XGGeEOYsFxhsQkUEA9hGg8s1UwKI8KvX/w53c5zCAwdtr7WMcueReEnU6VeseM3g+8SFVI9S473PhJQRaPtced+5A6G9PSHHpPnFc7ltfKm8XPpmHJKvzJck0MkI2SEX9ORx6vA//uUZOyLs74vwCseKFJAoghVa6cWDjFlAA9wZMjuiCFB3AiYGlwglrvEtQzLM8zj4xLAdKxl5dwoDEHHHCXTZTdGao+MK32EmLxzI+tvLOf6a0S/p0RV9mLue+Z5tPnit3VEcx7kBf9UhgYA1mCUHccwjtphyAjGJwaAAIYsqBtoTltnBeNF6fI4ZLBPYnwGpfJyZOUM81KsxdfNGLmTc5MvB9v9a0jPyPNMj9oXfIz0fIpn59O39iPLq0jz+v/b6J5XWVOEL270cG9DnlNV1MlzemiKcAh5giYKFxM91CQGkDQGVAVsz0Ud33V7czDDzDEGVIrUt9qU1b71/37f+j6u7/o96gACkqaqmWpnGC2HbnnEabkYXICIkB4XSBArwNAPTOhM0LAK0ipWTYX+bW1SJbCII+4xam8HjmbsmuZ1sdZpFKq5wG0NmP5+Zw1YguRak7n54xLiNif0JF2Q+HpJeqhFnl0MxN1t1Qub7F6Z0w9Jo2W6H/2KveZlYC6P//uUZOeM06ZewAPcGWI/wghRa6UUDU1ZBC9wZMjWCiGFnpRQsRREhaRZeYkDBk4uQwPevIKpENb7IipKXOchttW9bbPH8km9H7bl67Kq7idLv1E++wW2BCyqqlvgMA6YMoHZhVAYnoUAkEEchyLMOBIyPpDh4ZW06K8gMI4tz9/bvdnv1AejEM5lNnhM7C4pL5kiOLa0oKXa6k1c8M4WB1SZWnldzPtGprrUDR41IoDfpqosnbkIhBF+0J0co5ECQTOv6liA31FydCeItJQWDfanZTxxKWl0gFKDEhO6FaMMABV9CBGCREC8Vw3OlyOfz8GWtWwGyT2RTqFvi3n7vs81F3rX6Y8wJzL9KbvYRIL6VQCBKKiRIKBjRD82mWOT89swyQJgEB6gKMDkEAHEBJ0u0z0v9XmhGwX7ZmqFPdP0+FkppnMzlyqyZgmmiZRYnXPI1KUvLzfp01pOdvIz5q+8IqxXbeqT5n/0vVAQUboGoeRUq5yEWDF0sAJAAgBR//uUZOsIg5haQkteGMI6IXhQZ6UUD7FrAy9wZYj1CCGVnoxQmAjHDC/RyLAZhWADOYk25MD1LaYMZ6K2P2Ld2bV0fY36tqv716kfp+MevRm0ysL3/T0+1IWlkUuUWhMCEIwwVBBTiCbiML4EUw2CAUCTHsVNwhQvy/UJIiDAt/HLHmHzn6M4isuZp9CVMOMPSMjDgjWeepgzMw7VSQ6f5RqUorDUjuWDw0g0Iy8vQX602NYZqcL/Mo7SOwIqSSLaUvoh+lSJ9UrseZRCOnsYMy8RoRoCgxFasqekuaVBObaySKL7TJCNZy0F9Fupu3UZureRT1+23/dRoZW2XSf/7P/kHf/SAQJJlFaS9QFGACDiYCYhRpfulmCiCEYICaxwTbTBoWL8tOhgt/0MJD2WCfZfZC8nI4ZLBUkKKn91BTkIi0uTYbP982M17u5J68B54I0xJsKyOk8JTpzpkedTMjylh0yRfe1+pluOZDWErvS1OkWJTvwTjxHlbgAAQLHD//uUZOUIg1ZKwbN+GMA2wXhlZ6UWD5GBBS9wZYjDiCJxzAiILPhAgQ+GBkTggAHxqEoNxgS5QLvsrd/b9AVbrMV76/p/q/dr/b10aNNFSE0XpfPbVgEFwACQBcwSAWjARGlMR62owGQgRALACKTHXZOAB5pcML7MFgKXp2wIGA6kDd93TDEhFEJiMilFJjRZrIZVTZLU3M/JkaSiqdaAlNwimXBZi4uIJmLvxCYpdCIhTnM469azquqxHi0a0aC1PjCySEcLM7k6B3t4OxKkgsAFqajoYMVMzmv8MEgNrD7gU0FkTaWq3XLyFabr7Kb996j/TQt/m//6lM9CUO5CypnYh1KaAANAVIZWSMAGGBWAIYGAkRo6QkmDMCwYDBtIY4QoeFXalc6TCCiu7z1hYznv+HK+/766KZo8pKDpw+7EayP96rAzmXhACXEBAbqzHHMuzSDn625oZXmWeud2oJlU9tqal+VgdEj0juELNtmmbgiUVpA5q7sH4gEITOdE//uUZOuAo7FewcvcGTI1woh4Z6IUD3ltAC9wZMjPg6HhnkhIGJgYHPUNmDgBKXw4w0jDczuV/cNiNmLLJZCUqAL9uMxtBJfdhVn1oe9u5yBZNVR/FGzOro5YwmqzHZshAAaIFwCzAGA3MFcQs4ZX3jCzBTMSBIBB8ydADhAPFgkv1W4LgGmEzixZWOqjbSLCqRUClNusfB+mgpKwuxwUgwJ6pT+MNPPDUAO3mS4/bny4Bzju6HiFvQE+y4RrZ6j/EmIC8dr07kvnEemle1cC3mEEtgYzePLzv1UhIW1IPOA6QOaVzRMntSVTOf32S9ru2iK2atCkK0dEUSubkJRvaP6+b2a1Maj+h/RV7N50VAQAQYKgQhyJJXmGAA4YlAJgQEAy4GNBChKazFw4FUdn8cu4cov85TPZqsm7kRAiJFpqXMtp5sTYHo6PTNZZkmbtXVoU9ilrnlS7m5laqGRX05YZHqX6Nz3ZfKsdfQmLypseZlAiFPoMjipwhHe1UufI//uUZOuIg69VwUvcGWJAIPhVZ7gSDiyRAk9wZMjnBiFFjsxI2mH4nkQYkQ4q1uoShFBtNVQO57Oiv16F326Eet/ZtT4ZZWWz22wxWK2Vi9vn6WPmKEogHqd29VztoxDUOEjBSgHlZ8wknDRUQ8ECYA4BhgOkMGHKAyuJ+HxDAQKL+OQIT0gbOIM9aIUs0RoyV0ev0jgfKIU4Il6PZizhEJpMJSna8QUL6ii2X2Urax8JVQjGMl9KePm0ilGz2bMtTVsgm9ByG/kdjHTnEI1JVlCA1FnBjliMigtLRilQCg49L8WEJtZcSgYnmtA/VNm6dB23vMMsQ0H3b36BRdvV6B3RV036v9j9r/fsbV6KoooBAAF4jAgEwU3NuhzqQMuMPQBUaBaMBAAAwSwug4jNtKFqiT9bndoBm9QuUyQjIiq/wn43WPO3bpZ1jSm6dTbvC31szpSa3JyVflDwA8Yb8T8OP7/6YOe/iTiG1e7SWfrHP/uLJLSk3PREwAAgQCNR//uUZOiMg5NWQQvcGWI84phQY6cWDxGC/g34YwjiimHZjohQEvIFgnOGmGCApadJlhitKYN2vQVvfaP/nFXa+yj3z1/qqv10ym/HNf6/2293f9VnsWADKSzVKoCW6AQEhgbhJGwKgCYO4DQGEWVGPuJ0YEXxa83EMLpelyc6PorG2/O97S9j30Qivg9KaF5kJapm5KeZHqa6rYjHqcpP0NbiI7usC4tjh4L0dMLoiR5pTsHc59sT3z4X1ytZM5h1tW3iRIRGjMBMqEYsOa60tk7M0MgFEpGpGdKbp69R1prdOPPH0ftvf0/KpV3L6Uf/3Ee/RQPWwCPZrqPrEmSqqoSgYAKYE4DxgZB8msE10YLQIZhEMAwHmD6OZTCydMPNJBwYh0UurwOz50mIKX7mEJ0NxkzwamzqZmelR1cgxFTeQ2lwZoHhfKZkWUIJ+h6FrWhzyTV7SLKOHPG8yp74JXQj7Z5MFOUmd9yhktIGDQWqTRVGGq9rcFNAAACMJqq6//uUZOYAg1I2wKt+GMI3IqhodwUiDbDVCS9sZMjkiCHZwwkIrR2XkFzFEwWSvNYJN9m2RpqQt/JRXt2N/r77DHO/9wn8UQny/sS+xSu71gABAwAMAIVBxiQ8mDcoZpdepgOg/BQBtL8wFCATCoAgEgAWLLkCgAd6/qvvLHsgwxwAZTUnLQWeCJHCAKPvVNj/D4QLi3dVNxxLJ10IgZjMTCmelnTNCpEVHjuHcebZ2IcSTKMRk8FlGzBtKcmdJCZwaezIcL4RF2u2jIdbxl5O+zn0BEMKq1ZIoaMCg19u0Ejt0hI1bg5yg3V9TdU1/xriRRiIt7nbdn2fT+Ae3xapCPo1K/+c1QAQDGU/dXRJsZACxgCPJq32xgOC4MAFuzGURuJI71tKK/NaeJ57Ss+7nbdDjrsqIryavV0ux5DuiPY1T27y9zaL6Kkq1u8tyUdkonclUdTorjmnIFe8JMASjSySmj21lcWhoAAkRCQTCnR0BvTtd0DAu4j/q2lAEHZR//uUZPAAg+tdQTPcGTIxgLisY48QEGlw/w54Y8jPCiHlzBSIHZk29dW/gxhHbQ/u+AOi1b7037P/7fXqpu3+tiYAACMC8EkwihRDk2qPMNYEwxaDyyhlhaHZhGWadFsYCDl7dAzSFASdIKlvNZP5rJLt0X0Zu4ZrutGNTZ81++p63yTkbQPa9tffWbmVrzORmEtyr01zfV/hea8dBLu2KNb9p+9fYu5T6Kc6dPffeNPznGCZj/WRLvaaLLK6OvT6e5mxiKoXMoQIwivZxY4AQfSgYsLG3liiiTIZUC9LbdOo0BV/aXq07yHSMOjX7Vf8khPi7O3a67N3IrRov6VqAAKFACQZankGAUjQU5sypADQm5MWIDjAvo2YFWdFYkkTomOm+KvQhE8K10y8c9mrlDn2f/wQZkv9Jnid3M93/TT1JxDWpCOAhYTpSuOcpplVt+7rNYWXlO6bWcnw3OU78aGXPzGXc6Zm2zuS2RJWlANHepRDwxrgg8YB5fr8mPfM//uUZOsAgxRHQ+O6ETAzAoh5Y4IUEPWG/A9wxMjdCCHljghQbQ761oMxdd+Gah5yza1K1PuYqh1aJycSwT9swtFVvCtKmVV2RJpX+9aAkIkUW1MBECgwVAxTduNNMLQBsMA6BQBhgTiJGASAC1p/XaJgDrUtaGba3+nNphQzP3Tbyvv5mExHu50TXhbG/MM7maFTM0tO5kTJa7nws7iEddlIn45ZHubGHyiIe5Qpe+mfXhmZ+5dKXO+e9ITDnErJ45HJq9hyj2prq0UvwAtObDAiAL3TiTzzmxHd+K219dDOnaUb7t138bZ15O5/u+nrVPN/43uHVQAAi992WVEAUweo8RIUwpApMV4gCEBMH0VtVZzEjLV1b2eGDeABc2ZckBahR46nvYwkLIGsOzTIpuRE0BWlXSy0MoVlt+5DnPbYSvVacGhAdWAIQKKsrSx5O8hHhtmoA4BQDUMMlJTtqZfztrPgrnq1DEdaLxHorlZNjv8nb6p3ysV+k/l7MW7V//uUZO6IA1tZQUvbGTI/IghAdSJCDwVdAK8YbYjCBiKY/ghQdn/SUgIe6mApQ/2fFOua17rj+M9td3+teFuos+zny/vrLGyhpuqZBVF84eUgwIA1kUAiE43IFSB3SRkaCtRa0mujIivalmGqshHOTUMK2gVt0DnRgq5iH3JWxgoKHWH0JRLmYUUu88adFxZwkvLTSmtW2w8vUrZIAQ2Cfire+jpt4hYdtgGL4l89EFzVTbpMK6ZbCQqFz4sVS2n6XY/NF6T5H0pmZPhZkZSefMuskhvIdJ2x8prOUsz/o/gzmIy7GISs0YAvmRgCslwp1Vy89rH2tpUBjzqGFglADgYbQTRHJguAxwWam0LgMj314+bU/TX7wque01BDZxxfxaZdEdj5+cPcvnf/OWU9TL3LOw+IKPPoO/V47+LZLXmO3rqfi2YhDlehI/xH4gCASUwUvsYDY0pmERgA8coCsMWJRW1VdjZ8tN9Yyl9FFOeVFMJPz1RTThrDjoRB3Pwg//uURPKIAosPwsNdEMBZAqhJcwImSmxlCK7gpEF6IiBBxY0xO/3fPVl3IpQNdw7hLjl6LtkAmHT8t5etUTdjENv7ajTLn3Ux9PhpO5MIC+ASHYE0JD15rxVBTQWosG0RT3U2BKKi0SqOnmpeIgF3FTK/tE9DZxc6S3cqfpbf2/efN9DoGPYKnzZjwx7d/po/2nRKkpVrvz4afqZt+4XGaPHhq1KrYhxAJsxJUGdyiVlUVp8FoAPTah3pkUmTWIiqhcVWcJnyLoLrBxrLUoMWKsFRaVbNtM7DDNR5dtrc8tfQ8aQAqXzyUkCB8UF0qgKf/VlTUBGCDTfXU2lNOKB0iGBXiRHZnZIYljOuGUIMRcMagQsoTbEVRMvJJdbZPKNci7ucQqx5MzYWNS3KO8yIlJsqXJTlJqXMuqfrJenI9dsvdU+ZfnLqqKPXpXUX5P3cXQCkwOOh2YiH7ZokFysPGPvOLgs484/LKetM8xo6mw69hm1HyKGewfMp+QCZkhEd//uUZPIIgq8lwakcGDJZwpgld0MmSrhDAgfxAokvCmGk/hRQ4ucK7ak7xZHs9VkI1iZrFzRUwqGSmvTznDKUyTrQFEgaRpWrS4UBqaoVqZXIs+SmlKCaBBoghrrSAIjdpDqMWETOJjyFS9snXSlWmyuWauxT4srPDq0+P2pQK4V1p2+lrE0WWudSu5ruoULLQRT6PBWZG07aNKwKFywt0UIzpUF5UBEdYlnmdulAZOVG6+6zMjlFl1x/4u2aby+F9unaFKsyf+mfB5oP4atnbHvP6mEfvtN+76vnpafGF+4liLMm/xqnoB0CiIcGRu2gkgOYRee/G1VdrboVfniP1AZaI7a6qGm9jaWIqG7sRZiyllKq9F93lKtn2VvmRfTk3MebTPzyt9FNGLYjBMhnzZjNSP+Ue60y9MmO+fDJJa5mGb1gkqXM42VfUAACTNVT+m4jgCNRR1JCN1BFgbSBLAvwGXNspEfcfhP0pPJSLTuyUuf1c+N6p8PPNKfdkth6//uURPcAgwhaQauFGnJl6OgAcMNoRvgXFSDuIGFTiWDVtIkJeefvgsuVqp8lJTLZk+nt+ly9PxMgpbywSDwrXXUZJLnBQuw/mE6lbBASETe9oDAe/EvTsc97aBmpr5pMwmcu3FnMCIkhGVF9oI0O9hTmsHxLiidrZ3G37vCJ5T/+5SmbXSj3vXRsT2v+jXd9taNl3WM1Go7+sr7Lk/AysfRGohJ9p0PAaFTutYDgu6kaJCoX5EzrIpUid1VaZwuA2mpT0FyGK/BCDueffkm5zbu1OANj69Lr0pbyOpfwR9PX7rJ0dLbif/lXfyuclsP3x7DJia6q9rk9dIfPYjUBBCdsN8j4c4gY8FHnkDclvmOUCceO2nBFe2aYLOu6JSFC/mtfr7ZZX9s7Xfy+5qa5/XaBrlmlp8hVCi/VeZcIrSNSZjTCmV5t0kMqrsGyofffkXJ9/dDt7p7Cf+84AAQgDanUTwAAD5TdFhQzdyAr4NJ1t5QnEtD5pnENnpcP1mO///uUZPsPgyVXwAOIG3JbCUhZcwMiCxxlAg4saQlrjOCVxI0JWM5kr4972/+9d88cm/kTkJhbek3rHJH9jW9f+9TMtY/zmMbT3ObGRp9eQz+Lut+euSuZ/AAIUD3C4FUCPnaEWpD2u1dWob8FoTW179suQKHgvbBy4doqzkg2cpuz5rsnkU6kPrmVl6fOlyHpI18/45aavhJqa0jOv3OtNnbu5S4yRBAOhWN4XUMbF8tYB7JpAMZ5cCER+EaFAqc2mCpCN1LrUD5vczsGpPnzP4+ZTLjaTmZia5Q8sfDbaaZv/y1//EWv1v/FIr0dmfvTTM/XqwTbbw5+fDS2V5FdO+dQdyT789bzuszqtUq7gsEn7DIsAxalY21JVYCvu6owtd0uqjkVlOSFPKeKRAzbwqmcSUm07g8s1X6hGrwlqGOf/XBZShXjP15i2phF+WpKvl2CsX9kQ77Ys6r47l7Rf+5Dynf0EkWaqyss6R5OSEBEmRWRVJEoSdBXYr4duc6+//uUZO8Aou1CQKuIGmJYopgocyMmS3E5BwDsYMlakGCVzAyZzH6GR/f9UKi3NOBP/s2V7Ccc5L+/kvP/MTo94n7elVmz2M9TTve936SFYj/ux9E50xgkGmspf9E3WWSOhj+m1dHLQOjeqI1wuj5Poj6WUOE/DsMecV0gst1FBBX2JX90Bb9yd59pvr+4Y6KorXgzebL1Cv0vhTHUKXeqi/v3937mq0L133SAAEgslmmS1EYMOerlCqW05IE5UUzAWjFaSDUnXUvIMJMoNzE7m1hR3lBilqii0yRcOzkzB0esKLD7MtXGAKEZdbRU6yaRYx0Rgg0Walp24jWgWte5K3jqADAAg9YsITbNbJ2bmMFwecQ7lo30dUyec/Sw3+ghQgLJKISLlI2KtUQ8isdpaeFX3SfQyLMefJtCjZ8jOMEUi94EPLFTAaIHxiEIjA6yVTKShAJTh0uT1gqPiIDowPqhndcM7MVNnXLJLK6kozLLmpYI9drIo75i7soxEhwi//uUZOiLAs8twINlGnJSwPhWcwMiSdgnBK0wRElcj+Dhwo0oIdCOxD5UDyXk+Oc1XQZ+Hl9Jf7f7bNbtNZ892qPfbztzpkPODqvafzeIBBgVn8GtqKG5rEK/s/izWDZ1zIJvke6Dbuiq8BxzsQMZwGMVeFlxO8znzXIm9snMmM4lTt4adItLPuRnoTyDOxxZHu7Q4hF5kLvMrvDJi7dnlzpWH/Ac6nwJhE0/kppmSLnFA8ZHlqVEToFVEmUQRATvRzgjSM+4kzIr28RiOn3I983bbP3tPcyLHE3X/O7w+chwGSm8EKDmt5z/fa1WOMsI7SC7zPRRToig+i6rtL/zchgGlTVLTF7CnaCmmcCgNPpKcJ0wpojYR71FkkF8i0BkmeZGFuXCeL4M3YqRKrKiucctPrn1NEosiEL5RXOpmdy/BisHtL1QoDmGIiW3Zywu857AxsERzJ53TffU7NKMz37VAZj+q8CioKabfK7llskBKW/v4rikTbhxMJAjI27F//uUZO0A8m4YwcMhGlBZ48gQcWNMS+VhBsyUacloGOBBwo0hG/mB/ODQ3qj7ui3W6XIVbXsVGYrnTQt9mWW/KpmTqjvL5WQlX63e7oO8DNW1pgkOFKllrQzLIBRx+8H2hwDxXyv8UdkzwMEFFhGq4CvfHFiUdcdoepw5dV+/c+vNuWF2c4RWTk3YvnZmZ8558Onvn588QtYvx3at8OXoMMoJsq8hr4Fzr+wOvRO0E/3d2s6R9kBB5V2LAD9xgZBFX5cuRf6dmKYvt+AxTSuWUO8axQlnOqW6XzZkUlSvVHdCOejp9Jxzu1zP4H/VuSc/o3X+Rqt5aCCT4UpY3b33blnYHW5jvYwI3PJVAACBEOdfF9j3i3zO4Khy5karptmoISWZ3gck9Ft4apyncsKxEMyXFb/nJInrSeWd7w5XZK/OHRVvLVUeRN9X/295Dxl+jfEj5NLC/q5oUlBPyXMyU/oD12/tdr52qQq+c7LlxLK3UPMuA4cyajzptQKR1O6y//uUZOqAgtgrQANxGdJUybg1bCJuCujxBq0gaYlgmOBBsZWpnblH+mtsufyYCgkY83lH25KI4MxI/1/f/qecSznO+IV/ZB59ZP3Y5+ru/fMfJrL3+5JL7fcZQo6eJdDtb2ytWoNDhWc+cKA64hsDG7TvocPSr/cqs2sgSfjpgV50E3mCHi1caU4mt6i79gesUFL0G87QiV8AW3xEp+/d/v7EXEOg2dPGy6bh9QBAtWarJQX2lADJ7KS7HmsHKlLoc7vOqkVes0KW8WliLc64xtXdWkZbtq60f2Kh1yC117J0d0eLFjFR/Kf6JPtxRrJjjOAOoi46zFqm/S9v5hbd+UcA1QQABu5KlZDdxZq7psqtHeBFbhZtLC2tv3QgjpvSJxJ3cfbTtpmin7Tp2KcL+/mbwypZL3f9W8LSNCK3Moae450zdjP51dSs1MEiuvnQlaO3o5tynGtze/k6uY53mdmwqAEKPeUNSKVHfn6op2+nbPDGGA8+lpWycRnon3jW//uUZOiIgq8awUNLGTJTQ4g1bGVMSoB/Ag0UaUlYnCBRs5UJl9SFYLzBHxL/c5IWrVKGXN1eHbeO5lmbNCUxvEGdKJnySwtjRvIvcHbDjKmS/djmb7bX/IzCv5htK819lP03ZMyATW6FsYUqles4YajZBd7pZlH7MkoWOW5Z47WilZAnZ2xnJDZQ6GkPffQ8dWma7Kbw4TOiIug69AnFjqEyQRaZ4xZqFDqg2UMAJCp8odsQAQAGC2kdTuUnFe0U9OXNkocLuRkGtfGd/TK7p2yWKqzlsksaSV5mQFEOdlY6UemkVkJFa38iul8l4DWAAAW8pYnYaiu0ynqDg5aslDoSNxvKfQfE0cg3UbD68rk9e7D7vKdQ1E80vN2OnktMdzQp9vveySq3P+/YQdkv7f3Xlv3uMLiLnI2M3SmZOKK36wmGimlqM9Rp+5F0zG5iRJaN1iDmxmxpE854XrLnMNQU9WBEH8IuYehugiJVCDJaF0lWC+vRHRUz84Z+hxxl//uUZOyDAv1cQCtIGnJeiogoD2MGSdh1BKyUSYDqg6EUPTAIU26WftW9UW50pfQavbc5Fqmp/Js+531XR2Wou5DPotq3Mz7NrZljv5ffTWPd3wAFSQm7yPyynae7Aax/w03c4p517FwlDPf3cAQiJm+iQgWUmuxKZDTX0SlZkyuJVvus8E57mUUuAkIWsdHJOGDWIzyEjtllt/Q/mkN5iuGy2tBqYs9bA6e9Puf3RnEFzQ8JTkQMShB8zIjUS/tIJEr1jKuGMWAEEUiKzO5AZj6U5FC2NithqdXosO/T1P7P8d51ORUrob09fHmdESIaVsM4RvLIu7VAEU1lZbeMlpB+mEX9Ps1KiYDu+ES2oPb6Il0JCtekyBp2scHBCTgKQvUYlM4Z03ZiuF3N4tUigyCxfvRvwJpxTCmyMgjSaFXVWfE5bueJfQjhoFimClzY94UuGbQilCNu6E/hAy643BJBHzQAmsq1Ld5uMHBjIHYQlqyV9vkv3Ps7baRjyJHE//uUZPaPA9liPqtJMnBRCPgQbWUmTQmK/g0ga8jBg6HkHJQIXE6j6ppMXaa1rAbA4lIdNLPgU+KBaOecXkp1aaupZ9BKG61UCjbq3oAihAABjnSPscsa+e/X230ZLgllZVSgcPzpAjqoNTJpEji8tG8FFB061yNva5ZzoTwVMqFCPvU4UOk2ZmIm2syIiGuRob7tPBO0p/ARoaFd8zUEsKPmgIa1lNGhPnYPR1AFeFm9Mk+24AABnnhFjw0Gz2OwTB3FK91NzUL8SkJhqf1nm3XXhiLkIRwcy3I8JmUlZ9GTsjkyJq9Ka1uZ2qaW7Hz2dmfdkl1ih7Tva8q/asbQfSUgPP+Be3V4xxh0yucpminlFLbVDXtq9DFg6AjTDs7JDSQZWMzyCjlIDZtk5Sk1NChRI1ydFLJC52mdm9q6P1R1dRTkcMjIohEpy3hO+pnuZPbtrzu4/CM6YgdtXDUX9x+lGNy8v5nltABNG0szOyZdA0AkqpMxaVTZ9aPPtR+H//uUZPIAA11dwUsoGmJGoqhpZOMoDMFdAK0kaQlcoyBVpAk5opEcpKq5dUjbL6vWg6yyCj90jKrgMkS4KZUQuOkQ48gtbmZJa1Dn3EjyxSIYUQuhoccpK0Qx57O+s9kNwUAcfatc1bmNQ2vxMmYqQiOG6CPBn+yk7C9scTIT5wkS1yPpm0PCA7S1FF87w8AcUutY6sLNZKjdP4OBLJTa3f2ljqsfv5qEDCL749bllh3Iak2CAAP1ecI44R5smDh74kZZqGZ4HSbONLxuHj3whnpR7Q5fqP8QjqqWS5uxHGKmTeDyz3JY680yIvy9VyxIpy3XLS0DnB0+GhacMkHIotBU5In4BaAE0JeiBICANfjWbGakUusohOzjw9Di96Ce9bZxmZkfDaso0gBcG6PRsK65uU4Y//ilyt4Idr9CnKG5WpPlYDn3OfAv9lvfU/KYftr46xaUVtjMxBZLBIs3EAgEQGudxpgWqRZQkMeHFfKuYK+SKR40ji1WxC2Wm/6h//uUZOgOAudOQAMvGTJHh1hpUCKeC9kNACygaYlenSBVpIywabBA/SgtgTWIIbLLAtyhWI1FQnTIxSiKj0vWuMFSTrEBNMuMvGgvYfMFzrEUtKhr2oABgAAVb5ViA4e5cAcfEF2tL1NiVmYytg1afwb4gJoXUINq/An+O+z+/kK2Sot93ZfqXVu+/WnXydQqlKl5JQI8Xeoayfg+wYvCm9n3zOTbX0PAGWV5VAI/SQW9gxNsqDHQpAlDmJbAo2mgtNtZLvRJHZMy6dGbcvKwpT/5veCu1XrfFiLIkhp8iJQ0QvL2AAzJzKWiZyFta9te4RLWlR++JHxd4Vas/SArBaq1ypTHOEj5nEoexyglq3zvjl2RvvpErNvMi88QhfzppGtAiVl5ubgqqOXSFKwTyqQUMK2I14yCEvSpQuAzU0NVWQ2hqeRlmeYJXMPx0FrdIVBX61mEsL8F9NO2d6/JT/c+8cqA87jE5o0jlzbwaRNOJCLxmrSTSOTN2B39sHb3//uUROiAQqckwTMHElJRA/g2YMMiCkx/BSwgRYlkEyAFhJiYmzy81ygvN5rVrsxfvdYrfNFvL6sP+7SvfBUtXT7fH53vLLHr6MrRVyzT7AsRbt3e7JX7o+L57vDVWutWz+v9cK1pmIBzSiIzf0oB79xRHtlSXdQEK8+lCMLL6XKJNnHCIJCgyDAqEZFx5N4FQPSl4HadalbI6crIoZ0UM0eS7UAFPH9p6sAEIFRWtFSUPzLwqrmA8kaYqTcpUoDqt0O42hIe2SNc5LHMUKOUfFTabo2lFjHl0KWiwv9SNkPxdttTfKUC1kw1CmGLCJEAbB1Sn6JKZV9G8PUXJmXJBTSVWzcqiSziWvMsSutkiRyu5JI0iAQCixKe1ORRw6YzNV2/+7MFXYMK+KFEwGiobEoK6eCx6DRUGisse4NeOeJQ0rgriKd8GagHxykUQEEbDuIjTmDoJfezFtddueWRNOQEgyN2EysqwQSoe62LK114zdOjh9cTw4pcLWg2Gfuf//uURO2OAxk8PwMJG2JbZEgBYYYmSLhdACgkRcEHDOFkF5QIRXtOrm4JkZesTn/WaZW5gToUtZgbfBq2Ri9mgKkDBSIN/d0g+tyW8rr+KBCQMCgwkCAxAOQQ2Wf9pNONKLMPMPLZyiyjzD6qqoldNNMr//+VVVVU0000A1VQZTTb///////ppppqqqqr/////////+000VVVVU0000VKOkBOgc8T7WkcDXDCjGt70ZDMGc916p6yfbFYdohnHWlQqUEtSz2UlLMjoEaap2P2ZEhZhJOaPt6qekjWraK/6q28duKb9LHCoiv39YspT+18qvcZDm8fk+j92s9/PxoAISlJ3Z6lIaPDhk1bLRk2PmG2J5rnmzCOF1GWZubkotDg4PmG81l4wwsQdPZ0+7////s7toUww44zs7OMOOIBzSNYITRl0yYNaDLAoQCABJTTkqVCcs+nOB41tFBetKN23ExxgVAT1irrbEveP2rEFRAXTrUhSPWBkNe7onxYLoXa//uURPiM4u4vPAHmHFBhZifxYSguSghIfEMYwAl8nB9BJiD5hahi9Q1sy5w+LFUOIkscGA+Qc6DiKQuD548XZO3KI1Ggt1jggxiAtILg84I8FrJlIpmFpEOekhwRDfcptzAgtO0HP0zh+iJuf8CJXcRQ/Cf//qSRJdG0jeknST5yZwPn/83tf8f4DxHSzv+cz+vwdx3dAQiAALPqvb9eAYrwr6Vd7wSFd2QJ6iBtC2Nc5Elpu4WqCSJ9TWlRLwLKcZcia+jNREMm03KRmaO7W9aCwe79q2PHkMlQWlLIiI5lkSHfjR1blM45lHcVZW1Nm5bDaKWTqeCBUK/wVpQAJPcXEeYBMxuRizWCuSdX2TZlygprNQ/s9J56f/kPlsj9fVlSa81x1VOnm6E+jFDyLFOUqCTxsIi5QFD5S/bTUyiJdXBkwo1jBXQIfWu+eaul5DpGe7mZ7JFRqZOimOe+UCUEfaBAAA11SBhUSoSEKtXiopM2qhNhQFF0oVRBSYK4//uURO8IAlc7rbDJE2BEAphdNGU0DKi48geYcUmbrV/k8w2hFDQp/wsnxSQ38RWR3gvwsm3fwK+/hRwLl8LFgKNOFBQTC64K4+K7/6C//xXf/+FcChvnDcn82A6vW4AX234c/toLCLenJkGo2N+wPHfKa8WMjqYYm/G31iBb3HTaRHVzcnQcoD3+YcZMjo04/r/QmQ62J8TbzfMhypw8TP3/sdz5RMnv33bm7NMU/oFYQVgoIGCBhAYOBQQMEDCAwcUEDBAwgOxZZUtlllQODBBQwMEDCBwcUMDBAwgexWoIGEeyywGED2KwMEDCF6StNN+VVX/6af/lVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVRAAALR6cKwIZO6L1jMNPL5p7pmVcFaa8UGLl50b//uURO+EQzRcv7HmG2JTIXe2DSIaSpSY/CC9AUkollAIMY0xP2qg737anoanYe9+J277bOUwmveR/usyQzbnvecO2XXOpCBEVSQp2sRsS4MzHLGK4ydegk77QqtyapK5QZDzl7Knv1a0aJp26+aXyMa8aikf4EslONh/TB0pAYb0y/k1iueqTqBoNcx1qlXS66hB0P+rUnEDCNVZiDM6ZHJfKM3IUm7vLEq0573E8Y10lsXkILeD91zTVQYHUmxW1aOF0lVKKKSfMNfGcYma6ZUYnhWypD24LRz4o9VMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQAgAAAVr2VinDlX+ZQXl958WlXfQqauhkcuHYqbBV8oUX+sjM9jx+GWr9bTxK6Q0vqWcGl3//uURLyG8ptHv7GnG3IAAA0gAAABED1+9swwzQgAADSAAAAEJGMXjF2QzDFFORihopIwY3tJTzNnOB/FfkN7As8Ei2x4W1o5aQPlTQsh/RpfhxA3NDiYN3a4UJ0bkhCIklJNw9Lx4KljdyVPMbHgusatH3BKOBBONm5oj1n052Iv8w/9TWgh6WwheKM6tvccylH19oPQVhKNqU3Rwx9M5ekOQujnngWz4Xs5tvCD6iaeybYAfz7K2tRbd1obtmmXzIDHZsfPg3ezmYXOn1CRDDZHTuAyLw6QzO0ltDBMQU1FMy4xMDCqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqgCBAQIKoVVzAG+7bW0KCfIVnGBi5JN1xWRhjoRQx8SJKNQy71TO5FaQ4xvNXMOoaykcr//uURMiA83BWPssJG3IAAA0gAAABD/2A/Uikz8AAADSAAAAEm/19vshU3jZ5euKPyvrMohdTUwxiIDZ82Op5z4F3IzPMsG9WVbPWVO55wDzyfxeapGdyjCPQdTw0vgFOvW9MqlUoia0nxw4hD2W2ioE4wXKnjUswvKC0uWnJmMa6gMMCjfPI6ikNCnyTi0QUQLJXkvzAsJsaW2USnQXwsrZ5hibYlSUkrR+YdZ16duHW2vukW2LxGTY0jSBNCjkrgvUb12NnDeReLR/OeTkjTFsbG3KNSWhXnSr7JVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uURMeA8upVQeFlGvoAAA0gAAABEcWK8Cww0oAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUAAHNCQsXMCYhYJiyMlFR1hJ0SIomNBi0EjbInWidaJIpZEqwWB0kBSLvCpJ6iQUArAESCQFIjCYSApEFiJ2kX/27AERCQFIjUiwFFaWmmPTH+siEg9CoZGIVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uURH6P8swUuJDJMFAAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUBgABAAAgD/RrBgAIZtqjFHDUPGX+HAAieJjXwIkY+4K2f6a7XDEYgaYwQYCO04Gih8Bg0X+8BofgZ5LYGHwp+BigGAYxCYAgs/8DGgODJgMDAQMKAYTB/934GDQKJOBkUYgKBoAIOAFA///E3BdQDY+NNQbeNb//xSgGAgEFjg54XABqwZAQQ///yISsISDIBYYI0IOLgIp////mg4CmbmxFG//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpBQAACAAADSCgAAEYoE4cJxv//////////zchhEKBQEBAEBAIAwCAQAAADuTUvIyMVeJMvgCcTO2Rcs0IQNTMY7AEjQZATkxT4VnMA4BGjAowDIwU0IfAoCGYJsAnBcDHAoAkGACSctUDG6pAz8oAQAcDCAXKZFQAQMBhIKgZdCQBwVLIfCWncAoqgYeIQGOxsKoXCPkyUsEBJAxWKgMDAkAYjgHDsZEqybIDSSWoCAWAeFgMKAQDFYQABBYyxoOUM8PROmKlVYesQ8fA3BlBlC8XS6yJDkV2+IzHcJ5DpHLhAUSitjI6a6/65ElEaXhyScWi5itS1//5mianzJZ48bHUk0P+j//6SR3/MAzACUVDVmy48xkA0G/1TiwAUZd0LKGG4g1v+CgBBd5gA4DMYEcAmZJmgGmSKAsKOm6gMYAwDFITACAftCxcDCYRAOB/38pAYPAoi4nQPj//DLYyYW8EXIoQP////uUZP+ABaSBQKZ+oAAAAA0gwAAAHamBG7n6gAJ7QFzDP1AAFzjjHWLjK5IkX//9QWjiCY4yCEAHALj///yfUbEAIgUBlBwDvM////8nyfTppoETKBEBZY55P///////////kENFAADUtMBbmYyZ2NzfgEWPjBHIzhh4jNLKGML0UgxEQtTCPBFBgMxg0AKAgA4rASZc4rX12QUrwEg5VIjIjE7f3PbczDmY2YYNBazfiwAjoYFIGOPjfnLf77hvlyGHTUxAQeDBoFGiOZhhQBkAs+5T5zlvn8/9///+8rztupE4vADiXZdOZ7//7////3+Z6sbugRrPTShtibbg84mTmTASnzZl827JNssrBACAEo4cPS4dBnkaXCIZDVEaRAMZik0YSAmYtBqYSgyFwCYKzJFZtWsNfcpqL+NdW02Y49A30dEuC94kB95VRSOQWs+8z1lq1rD6WNvA6ltpDqSs2A+swyJA+CCnKZt7VdiNzNKLj7i+rGo/aPe1VKIx//uUZI0MRUAyRp97YAByY1i37uAAFHjbHG9g9QF8k+NB7CE5gAIAEpmfwSG2ZaWYj49J1Px/mH0JycnAHJjVEyiwlhgbA1mEcB6IwO1kAYChrLT1p3nkeZsJdRFYwPgmQEE2PAWmAaB0YcYbRQCYhku9ci9TGMQQMAwe83zJIE3TescUPFLpvpOzylJqyiTVGUIDmnQzXltN3f//6/69t9IBe+HIej0HMLiTiALGxBP///uhpYHA2ETX/rUlLM+mzi2lVrbHGwsVVBDGzCCMO4KgxkCxTCaAUMewKQxBgVTBRAUJAHwSBeX0RmXO90qijyQI1iKPCh8A6A5DJFDDWxXDuYVIU/Dpu0zRhj1vcziA5FFKe3HACrhMUe////104yi93FFPRVUAAG0gJpuMAGGxCbME4CcZs5AGDCecVUZuHnKqZkRbg2dwc8aX5nPhoCYTCxIaDE8pfc381IuZurzmn+YWXpMJRjSCUxkQQoMdGDDx066oMvHTIAEvM0Ol//uUZCwIBPA6UOuZ2SxQpRpdYexPjgUlVayabpjuGiu085o/tzWesabWest95nreufnhKYMZcKAZgwcZqUiwOv1vjAhUxsqMYCUECBzgz348///f/////+//91aCRKYoAWVQtrLMEAbFMgAAXECkq4gAOWbIxAIMgKGiP8rA/9OShZKpKTpNEOMTe/JX5k1r+A5F1H8odsrAGrZGavmm/M98ss2ifpubb+mdrAZgcce4rCCTm3bf/wawy4ioxHcKBGuZWRKUfI5CiKsA0MYPQMXWmk6xEQZhgOETBd1a5AycZPHHenZSw6wCrh6TUgHaME7VFxDWZl8YQoMky4ymT2/SS6y6z//pl8csUGLeQAnC4QMQnBv4NgsPXHPJ83TSf/////WmXEHfAAwAALY22KHAApkai1lmQbGuN5f8gBbCSJKG3+bEeIooSvozmg9Zrhh2/+MiWTM6pb////6nrjPZ4DAaY9UAABOtolpmMFRUuVKSNhuAP9ToOElvplDh//uUZAyIA51MVmshVww9ZnstPMpnzbUNX0xB9bD0ISspACfOJE6hwgaZSpZKmBEB0NCGGojKpfLZ6ap7NNdwp6XX93S/j+5Sodn/5bxZgHRMpjNWdrVcL+NUhhEaYTuL9f5zw8ASHxxxpCFyFELR3/oe5qWdf//1NKkpucPhsAAG3XE5YIAOyQIsOBpytoJJSMvJrJhAHNo+eWc1vXOe8w/mhKPe9RIAiGJrGurOymOqaf///iGOepo9GQ2aigWBA03QaVBUbswo1hdIFjAw0mC+9pTRrzD9YtfXpbzlFmfr26tjWCkKJ5ulZBC7fMklLNyGEEW+oc0Mtg3oAeDkkuVE6CBggh///+3/8CC0EHDuAECGpeJDhLYcBLQM6FIyPQEwf///7TmscAAKSJQAgBFxWg+hNTYPqIzB8RQi4uc7MmM1GBupdeozTdborR70//zI/Uxmb2+tP/8rUB0E2s2vhG0KItoEgOGpOAw87MdVjvi0xcNOlQjBycJfzF28//uUZAyMg3FBUhtmbLQ85Do6ZYJmDmT1Om7tScD1GimllKmee1DMBiHnTNVFkL4zdKABUMuppIv6e0SBDW8VhyLKsjZ7bFCZ7zzSTKdEQo2pekYhJSik/+iy1stFkWRRpa0TaldFqnpe1fWYmv6iSNvwVBp///rdyQAAGlECZgDCbDyoWD1FjG2SQ4ke5CQPDEwtdy1mydu5LRu8eixqmOd3FeopruJe62N8N4J/0Dfr/8n/kesAAXwAAF0YWm8ZnnObUqIZLG2cPFQYLISbmj4YSkEaVASeaQlYcYMPnRNg0fumtAKuCD7UmrMPLtM3btBrvONFYGij9xSnmqGUyqMxait0lOYaahpzOPRx7gDSOvuGR7f/W9a7qyzR8zsxxATmL0/8hJfIgsAXQOAkehARTtVAUxDCLNIA0mkrpbHVnbj1dfKQ6nUXvZiy1b/yjftVf80Au7mTfNt/uj9DXa3JW/qfw3UAp2wAwZ+Mxih8oPI7zR4g6SXClmd6xGB///uUZAyMk1UzUJtvHGRBpnqnYKtvjjDNNG7pTUDykSrlhB3uRrxGakUq5JBIyAzL5PvYAwWLA0In1OThjjKxSun8jg/tEhz3gazjV8ViTbxnea13+J0zVvjWpZS6LF8Ztf/Ne6UnhPCeGlcLVyJww3RXkA1qcAIJOWMCmpVjK4biNAKqZQMlaGunWbNOGUYaxDv/7OzvPV40VXuUqYFH/RRDgnSll1cSp0+nV9f/FZjU3W1Rqf/lwAJuwDBtCAqcphe7RvzfxpcGR8iPJizDZo6vhh3YxiSGJmqbAKG0BA0aOgQRDMuFBGZjeRBWwv8mALLJ5rcWUWkUw0x3KeVxKSWdQ5SZ162uKK10SoqEi0EstflB6FcOH1a3ezJu7a6SQrZJhCDwW/++6775HFU4W2BXpAFTxUHKH25HFjvxLYpVEapsLMj1nnsQlPB4+DN9FZKn9G5T+UCA4+zYs00EEjmzLImAygAAA1G3G3GBDxgjoVXmgnmTKgE8SORUcSDi//uURA0Igskz1GtMPLxZpFn3c2geikiLT00lcHlWEOhZzJkmYOICvIigAhDxepSuC/1WaKPFNnI4qr4hIVWSapftAs1ms1B1AerO2FQcY6CKe79BUAGLa////nC5u6C5/871gMBKdgACAWZOXZmeCmpJGcjLmnWhBemQswW7hNKMST8hEDmhED9Pu3cvIRBFqEvBO5wWE4OmDjELLJdjDMhxvbPcz6IP7gm+q9iQCCccNxMsHjpsLyKxIdxQe2m5WAEtw4wDEBndBnhAapEDcABmSixoRGqJ4C7ryVfT0XLUnSICnKvWM00XkUxgJEdlwyojoP5+Xz58hNzkDs/7icASszUr6OWGxb/fiA/7JiCpyBCBTHQaMjEoy28jPREOFC8wEdTAgsLSAaVFjaBBgQ3I2cROaSOXLMQ9DzZ/uCiTB0o5IlSIU1p00lzdd5/KS/NHHfP++oBXyp2bQ2gu6dEP+jlaABliAMEcP0whC9jJDIcMW5VE3xEc7Xf8wrqc//uUZA2Mg3Yiy5vdYPBAxfqaYYdNjjC/LG9wqYD9kimdga4e4ADwz+sk2VGAydSUHFKYOgEblgEAgMAwAonGAYlBgdLHayqIaBpxU3jIIBWOgdOkwlFeTZYuuvjb/XG2py2TkV+85itMznuLmeN48ceWvWoQM/+gAAGik3WAGitlKhVgVBRKjGU+EjE8Q/pCSN+peN2vUljVvLsq7eGtv4qahplsq+2eWRWG7/oBIxdv3W31Xd5R+XAAeYAMCIScxWQKjISRZMJpe4wyBsjR3G4MEUrkzDhFzF0PnMD4Kw6sojBYEBCPNtQIyQBUuWcmKTWGCZQR01cpwrNeddqr3qaa4EhdCs+j8RSBhYQCY5mIFB9zq5zB/YJiv5zh8y12u7qImqy95cQdkBlJSAANhWUFpF9i2wCCqqTsIcRVVl2YdSL9sxOex1EoQ1x1OsFle1bTOzh50X2Yy6oWj/2bA4dY1JwRBQIejZUAIKTiAAUETG08ziANR5zIn82QyMIQ//uURAsNwrkv0Tt4QjxUBPoHc2geyeSTRE3lCLlkkSaN3iB4ToBsxRBDGQ/ueVLQ/Hc67fFEuZX0ho4tR0ieIIqycaPHsRF2PBwXearcNrTQJp/yxUApEyqzfCcz7NHv/cQLH80AQhOYAAGBUwumzRSsM+wkwq1P6UDFlU1ovM+rQFSBBU3peQzYNHhCitsRkEM2p2BbFWxNvC5rqt3ER/Ba3HGUVWNEGOL+hAA462v668e6ZxOxwKyF7cgQdm8kZxHAaS0GuFoKBTvDEOLQ6FNY2ZZUTxpRTs8kYBEWQ5xyiwNpgo6VVoXZuVrKFThm6Dg+VSkWvjQgHJVWGdC8WhyYnQ0mQ2/nQjL6AAMDZj8WZv6QRtFKhpX1HSJyYsMpxBkmaiuLkExwI2kmAQQajBKLK/IUMgCA36lkkemKUkgzYjEY1RgsyHMBM02rZYu9cQDNhwknr+DgAaDqbQ6p7fKKBKlgAFRYDEgZqXJynEmyhmbXDpVIR1wqkPwDtTS8//uURA8N0rojTxuaQdZfBDlje4Y8CjyLPG4ZMVlJESXJ3aUgtCoEzSJJuALQXDKcxWrCpfSyTB1xMQcSotZpZpQ9UaaNHNPx9Nl3/7kAuEA1AsqixG1mv/bukf/vAAHQAASDMYKRJxiSHbmXifeY5gvZonhHmDaDKZDQlpjz1HhCaZFaA8IDAZfMItIRgVbVHCgwBMdkDlCwLqwRMN5G72ki9Ah6KJnIo1uQ9N817QzWB1///kTQK1upH1qBTg4LDE6sMQTw51ZjfIjMrlYURBywamG10IwSIia3UvoY6D6fUkiCh7Fr8XPsinYqPCTtObprPG5RdwvGnnhnqQQPG/7ywRK3vVInIlKaSf/5BeYECMZqvUbReEdQYgcoiWYiukYBAIbWEqY2ygYJiaWHUBAJiQUfUQkw0q5HRMtr8Ny+CVYoJfERnVJPRLHBqNYxCMfkf/932qeqKR03//+XUfUEgAMBgJgxQT2zMVUDNvNjE2KgeDamCtMMwzg0VgxT//uUZA+Mkz8jSZPcWmY6RDq9YSY5jQiLJm9taYDHkOt09hz+HtQEMsUF80BsA5xmGx0flU5pEHBYBKBFU2mEAEuiFKUlYQTlYTB0NTMspnKiUutxW9atHGboNyJSLJcmgr2o93/qSBovX8bPLgAAFRxzWUACgehUqgbYh4yfytLLAONWDbDNgqmSjqB7eRKSVNPZKkH8/PlkiWctpZb+AZv//LJN//UAREAAYCYBJihJgmfYa+bwSXBrVgRmTWXWAD4jLqBcMg0QYzQQET99UxMwMnnzPegHP4VAiUHGkwHCqqpEAsILwrVUOS+bm16non2h6GZ21fNl6JPLd9psc9sFXGw0pv/DxiZ7xdpr/WXJLLrhuFOIIOUdBAFhiqueHVQt5gWGT5766/QO49ilrYXTTZf5UurI6oxlSL/VAWO6VQREAABoD8xeCIDKLUWNZMhU8Chw+fOoxhrAzkIg0PhE2VB4zECAxPAUwwA40wG8DBAydJEqCEsOgCZm1hQe//uUZB8Nkv4iSxvdYPo4pEp6YYg5ivCJNE7oyXjcjafdpI2UBHGBUECyToSkvUn0LHLvat8/Xf+s/0d7cdLqZNbWmU32yAACpuSAACXsJC7yy4JICmkDpWDjwtVXJ5pXEI/WnRYocsTasWOYkZLTCVFyMlpad7hORb/9oBelAtmBIEGBrpGd9fn2w3GncemsxnGDzAGLowGGrZmCoVHZHGLBCpA+7tmkmgQeBiwZprXoZc69TxiLz9i+QPgsy3pMimivN6W/Yc9HywXjfNs0cEAIgDWbghckqiYUCi2g1h42gE8YEK3CKeypqYOBSOnUAXAQlhc+9zQ69w6I6AYh0uh7DUjRBRHPxEl0Ai8AAEilGAAAoHTKIbOBJA/GFAb5HLF4WxjmxAwChDoAxAxTyWWBQpfs5LyQJaZXljiJMFwinLUOa1HK6XXi1x/yIs6sI8Jv0YCRrCrawEDeAACAJmEdg9AHpRoZyhxro3mEMRjowYVKh0oWRc9LcxEMeMQB//uURDmJkmIiz1ObQOxK5Gnmc2kniJyHQ05pIzEgESelzSyvgAKp8kLrMLyYhesXKE6r2l9y9bq9y9ZH08pmF/J3K1HumxzEAMnLCoBxRDmeTAc6JZztpwyoByAf0YxKRgACOdaTExiEoEYMCVKLRjJqQy9dWwhG32xdZkqdLZfgLKTqED2Kl6StGymQJJrxEAQY3TW8sPCH8ZXZvcSmLlGzJGWVk+EAhIGewa6qep3EeKNSCqfMmZBkdMzYc07OUc1U1pftW6n2KEebSOOvj2UChYTyAADAWgAMC8C4xJAnzFnS7NPktI6VfQ2bSYwvTg5VD4zERw1wBEwDKlBkQhIY6iu6SsEACMFi86T4GDdcanE3Zze63MrbxPp19MzrnUc27pbQaf5LepTJn6AC49lbuNEAAF9OS2gACAW2LIEVkS2ZsaK23tPThQvBI45gm8NpWyluYrrnn2pC6zS7gVVXUVKuh/rChZlBITxgIhHGAOucaBi7huYMomuobOad//uUZFWJEvUjS8vdYO42xFqNYGWFi8yJJu9tCUjhESj1l5T2gWRgtCNAKE4xEDpDEABjMCuAEZGLmp9NsCi8wYALmKTKCJt5fFXOjEMRCDJXT04uLnnH3buXMy483/8Q+4gHLSI+WAdZQQnXrAAACdjjctszNkYHumQYThOMU1JJHpxzkOFLNh+G8E+0xiUk+M43IixPvKqqmudR93zP639/2C1pAAACbid1AAEFGIghEpHCyQO5gYFsZGr8YMhoHLdwidSwo4OlqT7EGgN46sUpboDB4dO7fPia6aI/PIAkVS1lgUdOzE5qoJVmOJTI9/rAAAFtt11AAEwYkYa1wcfcdlYOmXwFkYwqTPa3exIgk7GmSssWY4khiK/4jHS6ctfdevhZG/Kz4nFItJIJlBoyblwyo7EItZvNHVM6U1HLrsLoJkiiahAnIKEzIVFh3NBoiPKety5lcid14WkhEG7cOq2gUZMqzRFpP3TSI55xchBQlVTc7rA0jyrNPrFU//uURG2Jkmoh0mtmTL5LpEpdaYeVyQyJTa3pI3kgESh1zLCntfBknyZ3A3JJbGYmB1OZGg5oJ3j1QA0aLRg8cL0E8asFdoxe/TWYy0Yn3qDWhzDA0tUrYY2ucjtGmFC7d6IyZd2rmpv2eZNZma5iaz3alQAAbbtoAAkRhQSYC5Gr1RoqKQAwzOJ1iMkmia/AnWbX0OAcSpNNErKyJVq0NCqZCYWZk1OMJaKSYUslmlWRg11pxhLY5cVHgoVGSogAAQ7LLdQAA+Jhw2YVIGn1BsZ2YgOIeiQ2XnIi5ndJNoDp+E0rMhmIA7GYQxJjo/OpParnoSzhryYZHMUHyfK7WqHLBwGora8uC891gucwfDQxmXI17UM2Y083NnjdQnAMTNwjcwq8SIxmNBLXLWhw8hSKLPkfkum6uo70ljEuBSlJGwlLXMxstuM7y15T4u2Zr/KysTEhuMIQ8MrH6OkypNsunPycw/IjzCBdNDjEw3FzMoAMRBtW4KAEzEDPuzrW//uURIaJEl4h0tN5ST5MZDpNbYh5iUxtNG7ww8klkSYJ3hh23moZBRplG96lGnxKbVWZT5Yq4p25hWbyUP/nY8baFgFeAACABDJE5DcYzzSvWCDAmgSeAZGZzDJgFymSgQasQk4kEIjV3I+sSIA3JaU2F9H/jkflFPnhXsBgYPVVVVWWW8aicqqlpS6a2HJAAAAAHDDlGYMqYskxJUFDru7PYsswygzM4vANZMnAgAh5mo6EgoNy+TcaJnr8wFG5BPzVCIAobiRS5YEFQHrKRcpXhIaBS4CkQU4CAzMW1PMIX+MOtTM610MrQ8KqXspCo5g5BDG7StYUNJqoJqsXZK0hhrTharTHpsopWNy9u/rRPiUh4YWRiekaLo6qYPxfqb8wSDcyBTEycpAxbq8QJ8aAkQYW2hguKQ4l4Dgqx15xJ2U2gmYoJqIxIDOSIkjX+jRytflHlH+jTjdfc2L1oLqlf/9tBhrPsboG3gAAwGwPTFCNLM29E8yVmujKoNrM//uURJ6N0lUPzRu8yKJKo7lze4MfCRBPNG7liNklEWaJ3ZivmIKwxl0TXAmBKIOUCcw4diIAGCAGZMAxhgFIJUmkJiE1CpAxY8ojbKG7xqXV6yludmtK83LaQMg/syQ8sgFsanFP5ZmQACrnLqAAM26CQQqwSYRHKpVuKaGkJpBxqBYV6IlneAItAivnph2EPkBhC6whQnnFHEqYQUxp0nwPLcAm4VA8MQotYykAXTNtabM40uAwyhIzBGF7MTwFQCjJGFcCqdtKJVAUOMcvNAOUVDg4wAQTFAFHtZy/HjgJyILnbdIo6B2RK6ZpmHBE6UyNXwroXIgMCJu988AAW1bmtCsoL8FMm9KVRUWRMMoQFuNgePBIMy0Mh8PU9ztn/yoNTKqD/H+eWi/l8IML1GWzWpB6T3T9kgfAAwEgcjELRAMc1Xg0inGjSmFBMm0AkwJRcTHyCIMEEgAwXQEzfMhkmNEj3mDHCIsYcGWTQzDgafSYCkHraXEngkcxLHAw//uUZLkNEtYaSxvcMXI6xEp6YeM5y0iJLG9oaUjsEKlphhjnwQS6O+hU99Nz0cJXNhFfvic/ugAAlKcgAAuCokCxwQ8N6AnVFExti1DSwlmqvyWSjUTsSt5xcGrtfzOISI4fmZ04m5kkRQ5p06LiIPowzwALpg4ACGRaf6YpkEJsdw9myuP2YPY2hgfCzmV0GQYL45RjGgVHd2GoJGqLn+yCAMYMQgeQGwEAFghMYEhC7JU8anbxvxINDwcHJrXqZM96bFa/b5n2GYrL7sVrpu28YyDoGHIXGrez0fJV4nUEwplIeC4tUwKFKhyhL+xg3ccRDr08mDubIIEAnkSJ9QhmTl28y9UBgAMCyBQTCYDC0yJQkgM3sQjj0ub8N6AS8wfhLTYyEoMJEFMDPXn8UZpp+ZmnHZSJqacZ2nGHqAoqDJSYWCkQmWWAoMOhBVBCwAKKNbhbrOtFtSGDoFg+Rz9erS2b1inu3RepYXNvpPr1gAjOU5AABBQICG9gGysA//uUZNGNktAiSpPaGlY8hCoaayMdy1h3Jm9oyUDfkOlphgzvFwEcn7FyiJT4LVMwR/KpELg3FC4pV00TUFAw1UMzRmVRxxUZil28zmIELLenama01pLX9cPrEYbITxr7M8Hv/HEfmezp0sxHmeiYKYK4rxsQAxAxBEyZwgDMNaMHDQxiLD3YINHjwxWSAaJASHCTgXcexAPxnMndNNbZm1pkzbOU4T7Q9J79uejUSlVq1ay33WtZdyw/nN301H8AAJtJyPQQigZtNjTNFHM0KHABGEWjKEjpU8HhaLwi4ZHyo6NT05Lpiqjs0199dFAQaH+TMzMzQS0YWbMGhIVNg0WcLQdAAwE0GYMGbMTDFNkB80hYoQMVMLIjFxwWQwFMLiMV6AsTA6BTswKYD6GLs0s7NikzorI0UiMrHjBikwQzARGYYEiw6WaAQE3EvazVsDBYFjTtS6OxmpGgk4BHmo5RK0qecZrOqyCPOjeoE7wAAWRAxfN1Tg2NLDdruNcF//uUZOqNE1EYRpP+2KBE5En6aeY7zKh3Gi9zCJkOEefppg1vcwkmgjunB4CYOzVTUEC5S6UlWFgl63tmnetwzZBARbSqUNgJlUoezaltqxqJVOjJQY7+uGg8YNoDqmNvhOJmrCFga+ePimHAHZZizQZgYL4IBGKCApJgzwlSYZgBNGgKsZpIxgUdHYGeIyAAi2YuDIyAQUJDCYCBALQcLpFuWGJ1NFe93aQNFTAqKmBKWeyo1eIpvjWxqs8aNx+389rq3pEYpxqdhZiecEdXOfYqcseOxg0azlqIwCdZTlMol9oZzE0QYeROTIvjbLaiyiVzWyyzk5q5qgmVHF4UQk35JpuVGAwBhDzFKmBN3lKU/0DyDdBfyMgQWEwekRDLCEbMP4BkycgQTtW0x8zMHNDkVUUHhGIggIjxVAS8CC6aica3WbOq70qu3C0FqHBJE4uLx82vTtf1WDHe5tsqDAAGAMAv5ggRuWZieD3GoJAsxigpioYqYH5mUFHG9yZm//uUZOyNk1ceRpP7MmJGw6ljcwM6TXyHGE/xKUEBDmcpp5lPlhGnqQfmMoMkIPmDYqGwY0mC4EIAy/CcSQSIqVzTWXMpeIDxgo25pWpsNtsvjKMrpDNi8RcWg/QOGCmDUZwAfhmCOFn8eHCctipZkjhcmCmjYYoAjZg9FDGNUBufzseEAYZCOqjRBCoDQeCxhYBNNlTHWDs4gsNBEwmiQrRmwjnWN2GLkSOogxN99BIYICB5GN9CPZh8SpSbByHiGXKFwplEoQKYKmG4GDuAspgXQiEYZgBUmQPg8dGBz5jv8mmkOn9DKtKl60IJRKeR3woVMm0F/0/yYXUT3J1U7/yKl3+dyugHAAMAbBHjACS3gwVszyNDGDiTMMQlIwgcFuMApE6jBSQMswGUPAMBBAaT6RgBLZao4AvM4BGjoZgoDHgtylcSFd8QfSGYH+N1hjsrG4RaadBnwPDbSAAYCeC0GCwIZRh8yrIaXgOEmbUi6RhbojYYAqFUGAOASJgN//uUROuN0s8bxwvbMlZZAviyf6ksSmBRHE9pKQlgjuJJ/aUgYo8IgJ00qFDDYBMNBw8QbzDwAQBxlVF7ohBztvfMWRaR6yGH00/5eFZ766mwm5HqCwwHwCQDCt8wqcxNNP9IbDP4gyQBE8BgEYv4YeyBmmClh9BhRoDyet5hIEIRQ6cZBRwvBd4IKRIETVdx813vDL3ilk/G6RA4Iupw+PSvRY1PLf1z9zlLtxGhhgPwHsZDuNhGX0nspsRp68a2MLBmcrAaZgogh8YPiBLmDnjNBh5YFyaKPZEjzCqMPgvMwmEFqwe3inojGn3hGMSFID0UDMy6feyONH5j2uPy5G+fX/zf+hvv1+vfWhoAAwOAHHMH9N/zK6hCs0iAvPM4FHGTBUwc0wBUT2MVjAGzY02DykBjJsnjEcEBAHJgKPRdlMphwUA5dLTn6aE39WAQNFXURVLaJfdDVuN0HhRYDP2I0ygABgswUGYkKoimkcIKJpryLmabSAyGJqis5UDk//uUROqN0qUXxpP7GlBUYviif4ZICzh5Fk/syUF3i6HF/hkgDCxQBIweUHWARFUaueGRjhjcOeT/DgKm5EywBVpdWrNBs7jWKyDBJAQ/GilT8xW/Sxv//XQQwTkC3MjIFUzQSVOY2bVYQP9b6w1GQNDGTV6OUcIYyGUiTOnBOMJgnEBCHBg4f8FzqZzaAkmVfKp552hQVBUSlUVh2K0szM8ws5d7W0YA9oTBU/Z0aeGByg4pkiBMSZxO9GGsfr0hprxNsZZ+GSGQSVkZzoexkOotmhODQYRQhJWC6DghTIOAXKwSkImosqYvMYvhT457KylVCKnTg+eEY+K6n9/9fX///76//6P1fpoIBUMJZEXDHCmv40ZWFZOLyjozRT2Tkx4IXCBoZQZV6C6GCykk5gmYI4a6dxn0aGIDcdpa4QQqGKgkIjQClOdl5sNNOqmKo/J15A3r6aXuefBjFtTqf8n7ONX1puyWr/9yfu/SsAAGBkAAYx5OhsrAAmysgOaP//uUROgN0rcXxZP9QdBSQoiCf2VICrxTEC/7YtFqCqHF/wywCXZnAjXmFmI4aloCRn9STRplzgAhohKzCFYSN5dHXGXFuqMnsqnfMpJ/jb7tDkpeZZ2rb0sQYQYCfGZKosBvnBrwcM7LeGNuNeRkvArCYKABpGWRgoRgqQ/6Yi0BsGgsvmGQZGE5Bmk6HGFoHrQaWIAbV5uklNNZ0gtlFRmZYKpiIuRVZ5YbIJqT/1a9/v+63/Yz/7vdsBSkbQMCg9MaVtNn+kPQbMPQWeNrz4MOyDOkwyApMDVTQ7VPMsNgLXtL7Ky0RmRkIx7l3xliKKwl+WPfN1tJ/QACBvVQjB2FVMhWZ81tVljsM8zNfyNUxwQiTChILNLcKwyWSE4UAcIQhJh7QEeMnafGGgi1+jvOQ2cYvaj8HR5nNuSFIBaTFneg0/v839S/93T/Yr/zH6dkTGDVBupjdqkGZb+0NGnLPqphRicaZdyGFmDqgrZijYJEYNkGimKIADZj8GQF//uUZOkMkx8aw5P8MkBGwviSe2k4DERrDi/0aQD9CKO13STdAUwQCk4GDQOEFPJ1CACgRWw6QMS3Pu5Wd+e9bxUetJCT1vR9f+2xpdZjos/69u//Shft7CQAAwQsBxMeAOXTE91qczwd7uNH1NYDEnQcQwIcWbMfjBTjY7Yj4YgTV0lQUnpgeGxkGPIOBpWVnQoCzfuBFA2BsUCQ6J1UdKdfcg4NB8JOYbYvZ/9P///////+gSqrIYHKBJmL9HZ5kPK7KY2msVGSylqhiPodwZEt6eWkwarUUeEDCcuPmUBZAsGqz4OGmkx5lTzUX15ZYxJ1ViVfS9FxZOz16+v+7f/u21//VWAMEPB9DGKlaQxkBZUMkwkyzYZCmMxyYIeMDGBs2nRRDEmZCMSAM05xHyaWCofNKmBXajqD6UTNnxcda7L4/EHggSF09FSXDB4DiYYCAEBAfCVc/9lf//7n81T3/rWBUDAuwWgxehA6MqMQYjBt2hk/ssDTpkGqMP4t//uURPEIgs8UxsvdYbhgIohgf6lEC5BLEE/1JwFEhmJZ/uyIczEwfDEaU+MUQJMx8QwwsGCxGcdPAkX2rO+VADKLve0QIAiYchOH6RRj0VdP+rTtu19H/XsHZ7///sGAgBvjGRTuoybcHQMGqhJDWVQcYwDkKVMBeHazDTQKIwD4VAMJpAZzu/4nVAEXjdqTEKz3fJA91mzs3ctrbd3jgOnkFidG9uO4M3pmxc5mE74wE8IwMWbTsjOZzswyEZ4+NSdKfDBoxZkEGJmSWBuYGi/Bk2g8mfzETLgweMTsBOQhaHKUtWOVMbl2z/aZwwsKBtYRW08zyGjDA30Udl9X6LN+mz91+76FK4wwqoNFM4PeETbvK/s0E/o/OW+KJjNDQbgyMa5TBkAjMY9Qw3JwRTgXXNxCMLpIzHLxUEIjBcGAwMF4ULUmVNWiunqGJRdnaWlxqcr5BM+AiKOxFlwuilX6BVH7QFu/OJXQ/FP6EtcZ6GvEoAAARkbiAAAAAZmG//uUROsNwuwQQ4v+4RBXAXiCf9wUCghrEE/saUFtBuHF/3CISxmnGfGXnZmvlSG3QvmEDTmIwKAg0iKMDcagZ2Y4lUBQLKCyRTUJRPP0xUTOmq1mJ4aXCKE19fR/7CssSmA5Bcpi2CU+YQEzVGVYRMhprJwiYSMI1GBaDNZhYABqaN2uf0k8ZTIoYiAEYHBmZ6iwgPC4BMSZkkUx+OrVhmGn2EcJQ+OVd9ENVih7h16XSSev3t/e3b/2NX1d7un//02AK/bQGBIDGNA1G0YlmJEcBVXTJUUQCa4QhA4tpQW0r2MlDJyNZYyqfpWSyl2oaC4KhLSV0tizJmK1PTcWJ2m9yjAMFtB2jGFigozcUpJMjHYuThO0uNEQUkwXkmzRYB7MF89ExFQXjK5s1gIAwWcsCDQ4Bg4LALipGq2J9Mpg5nUGxWGqSexw7zLtmqimlRf6UoTFvf8rIgIYOqDtGRBE9xqy4zyZNKxMnudjwcVxkphKFUmPkGqYZ6WpjNhE//uUZOkIk1MSQoP+4RBFYfj9dyw1TJRREs/1B0EACuT13CTsGEO5kwQOFZ9TADjlY0pRfk8tuX4zepiQkWECqiqBIzhyzp/8K//R/2f1zf/6DAJgbwxE03WMz4GLTL5lowxQdDzKFFpMGY6M1UAhTAmPNMXAEw7TSMiGREYGJpIYEEwkGBKZyApO1VRY7JGpNq70Mymgu3Ltyvhbt3rWITTRVEHJY1V77KBZTAIQpQxBtI4M9wRaTN9FaI5D8WzczMtMGcisyqAwzALTxMaYHY2piBwyOnBn0eNBkDQywlzL8tlUpuZ2AeDbdx4UEhsNqJVbd1ftutvbR2foNS1jHf9tX+hcWRUwUoHXMbHIkTDaEdw0o9crN83ig0FxcjCULDNq4A0wmCbTLqA0PZfjOAgQEJzpw+oOFzBAILg4oCoCkfU8k+FjsofGGoGkFPcxt0lPWtXrdIHC2P7BW2s1UmC7A1JjmIjKZBUsIGiYoZpg0RbwYo6GUGFrmnXgCGLV//uUROyPwq0SxQv+2KZUoZiCf9sUCvBRFA/7Ypl2hqIJ/2xQYgtjDSRwWHR0DPjIBYDfy66EtpK8xdrWb5g+0I0svHV2+j8hTzva3rcidrTqTvf3POaXp7PKgwYESDFmH7I/Zk8KQGa0ophHoRl6aSA0RgxBkm3cBAYQwxhlcAhnXJAVLTCQM4w8YaYiFjgMBggGACsJcdhSSCmcPNYhl+JLJ7FS7bqcqXqe5Y3nhyp+/1zP7Eh6P+MAYCqEOmHCLwJodaq2bVwnIGQfIQpk1wUUZ1KyfwEUZVM4fVDWYbBU3UwCCszQH4t68zkKLCyawgHWEXrrQPh6BQKOeHzgeW/1ey32f/q/b92z9rfyKg4AAwLcCcMW4GljGEhHo1ek+dPjt/gwch1TDQHYNbIDowYjVDFIBjMK8RpNwKRHRuNJGOeusgGL5riiSCi6GJsrzl0NwupRaqVa9/D6fPWsOX7Hg+Y4yhl+hgAAwNsC8MdiDADKQh4c2PE6PO/vTQxV//uUROqNwrMURIP+2KJYwaiSf7siC9BtEk/7YolXB6HF/pigERTDYDeM0gHwwjEQjHkB1OTngUemFnBwDqvBgbdKd3rOUUlcvlHO26k5T5cz1ypsLAugHyBb/3f+3+swHEFEMD3MRDGHyig0ZAmIO8oXkx2QaTB3JSNAwEIwKSqjCzBJGZpkzK3wlUUGi2YXBoc38bVfi6IMe+H7strSyKWN5XBEofWPSABkxDvH1jIYGKEDGBlKghks6/sbFEV7H3U/OboINRimiQGbwHgYMKVBiWhNoBwSA2CgChYd4mAvZi47KRXIMGBWAeRAwgJOTtqMRbYmjKODx86OPHzdM//9P7P9v//21RAMFAAXTIbxmUyWhYRN0TLjTTaRzkwnEIwMDuFFDFSgEoxpxA7JEAwYWgxvBwwgc6i5SYcfLNkoNrEWd5TZFJOpp0DOs3rmQ5Sbm5unz1fv9oAHMUKefSpWWGAADA4AJQx+ME3MLKXlTWwRqs0J0ChMB2DojLpx//uUROaP0soXxRP+yLJSwmiCf9sUCfxFGA/7QollCeIJ/yRoDqgsDHHRh9lyQlgMGxggGpjQNSc7J19xFtJ2qEw2AcByM0y8fJyNduXbe3NTBpJAPPjft/////////9I0GBhBC5gA54qZC4VlGp6CMxmjRIAYhEBLmAiCQwsK8GAtgTJhIoBIbLAmmgYWADIjWGA4CCoAKhCs7/sWS6R+UydKpFXub6BgKEghHzM7VEvQYNsVSmLnFUph/EHmRH1qeODRR/cAgnmwjobEwpZhNDtGL8BsYLoNJkrgaGFciTYgLHogxxm+oaHxcJh0Tk7cWT5s223cWWTZtsDiXYfL0/6utck/V///u/6P9QtqpAQVGZE8FummZlrxro4xgZpaXdmHxhL5gKokoYKAA3mGPHm846GUAlAIyn6MtQBVrLvF7l3RKDn5kSul3QEkTDiQNCZYsifs56tVSafOcaSIp3cr/9rqL13dn6/d/6P9gAA1X4AAXBhhBeEPNPNp80Z//uUROmNws4URAv90ZJY4oiCf6kqCvBjFE/saUFXCeKJ7SUQUTPgwCygVXOQmogAvnpNHRzJ6MipILMaSe0R2CQUSVN2iHeK/Wku3eKW/h1mygyrCYMiHmGGNL4BiRDaacCEewmRRJYBi/IMsYg7BRjDiSmFAtGYhgWwHhxbWMQRzUmoIETAQZvxEFsSdtUq1Vhl1QC7MVkri1H+yuym9Ws45U2eN7G0EhEJTjBQu5O7r+x1n4yftqX20//5On9PQcBgeADmMCQoaLSqRq/CnGfWkoYxwmpgXFJGKCBeYBoCkAumKF7MDBCWaKmja0R8hDJkARoBWVEJkiNIY+tk/JWQZ0s3KhkAAwTQCnMaXFKjKaj3I1f0viNfzS8yUBojBIQEMzcLswrCbzHaAsN0owdVCAKM8Ix4IR+VVbxXLMnXYi8UDuLAUjf6Ozkuq8rb5ZhalBEQaNN//YIAwb4BXMldL2DT5Fgo2v9E0MIZQqTGqAyA08rg3LOU0k9U7cGQ//uUZOeMkxEZRRP9SdA/AzlJceY7zdBbEE/7ZEESieKJ7STZ15dAUUYoPHeJo0QM2pXkazGItEZZLpTYl1a3La1+ludskjwSW/QM1q/X3SP//9n/1/v/ewwHgHvMG9RfzKQyIE2FZAdPDHQwyOhyyFAo0wQ5SIX0yfgHz94ha8j0G47rbNNUqZ0yluTYXofeGIfhyQSednqWmxrC4ZhI0GwWE3////////7BSFUwKoIrMNpRzzNKwZI1lhDnMZPObDD1g30w0ow4TOUzsDc70Bs2E4X+YmJHUlbLpDfaNTXbmdi3eBkFhYWCwGJiZqrk/fVd/2avH/4orqV27fqu/qogDA2QEEx3AkpMUaNXzZH1PQ0doo5MMBA7zA+wzAxi0DDMWMGN+CNMFUvGgrCoLmBwhPGH4III1jsRBLHAunxoTzuBajcXwBsHBZNweFnM/X//2//////6vW4Bgc4FCY9YdPmJcL6pqxKpUZkcZbmPIgi5pfER2eX5kJvhzIS5//uUROsNwqQSxRP+2KJZIkhxf7siClxHEi/7QoFYBmIJ/uyImf4JCgVaYHUymMW6eeQeCz7RVgo4yxW8fYvPWW/d//c5dXd/6v/0BIgpgcYPQYgYnrGMEqmBqvC5iagWIFGFRg/xgzicG0WB+YPJ/Rgqg+misBhouoeovDzlzEFw1IKe3GOTdaxlYzpwQJmQyOcOadk0JqYv/c7//T6v9qO6rf6nu9IacbjQJKMCQmMtLOAPfHIGVnIaDGF45EoQPXYXkkQ2xXycBVPF9Af7k3PcjeysKCpWAlfvV//Vt2Jtq3/6P1+uMBCEZDP7XHg0RWB/OEd56js3AmY0CoGvMCcRk8FwIjKDhuNXUQs0FhUDFgYSioaikGDheXTA6VLUaOeq0VzOv2z+deqDUxE6wmamwsxW8a1ahcaooKNLDDzxqyWlyGvS6mOYZrOl00PvTZadYKiszRSpHva8KgBa8wTAUzGxPfNfhQs32VQDUyUaMusHgwew+wM38ZWepSa///uURO2MgssRRAv9YaRMwThxf7oiC3g/EE/7RED/hOW13cCWuPDEPTECG/eQPHXc1GTbh81OLft7PT/f//f2f0M/o8Ytd2r0GHFCfZmz8emcrgsJmmibKpyApqSY2qJ+mCch+dBYehlEj7GwuBQdOUphQcGCzWZnWhgUAs+g1ECE37IBFqTZWHBz1hpKGGU6xorJa3jX8JMcVZPqqcid7Wsjr0LFE8gUWtiMzMKUF6S7XdD6AMAi1NhObMwGWg3WGiTMjeLMhwcgQkjmceCEZuuCkJ5F4cARUNAPj2fJZZQg8HFLKJZtrdHqr+SvFv/9H/3/91UwUcQJM5McxzZy7DQz0fkuN1qW5jJVQrgygWeTgXG0MoNds03QhDRa3BTMEiCc+D48FVJPkjc34dSocs2KHXGwCaEggKizSdQbVCMgsVUxhsWY/S5WRsc/eueJ9gbHN1au7Cnu6l+gqVVMXSENkosNFNesz/VrjEdW8Mh4RkwBh+zIxAYfofJPEcXE//uUZPqPg68QQYP+6RBHQbiFe0g0DWwrCA/7hEEChOHF32gQViCO6a9nJNl/YKgMCSZNnV/o2/7v/7vTd9X/+4w4UUyM5odRDa56rkz0TmJNRpenzHaBWwwZH+zZ7GmMQGHUw4w4Dq1bMpBQwsODa6JDgNnZSPh4AAsPJVE542BJDVP0NuodR2oCPCSYtiqzew0y4USnrpbAdVjC4Ef8XXl2lT8i0wIgSjFhQuFsJDMudpNBhT0yZwrzFGwTZ0RASiMYBNxPIoKaYgCObhu3aooqatOgA6Nvle5rf02/9XR/s/17Mv8YMHmEGjPSZ5Q6jZUCMtp/ZjHPJZwyVoZPMHCI8zE0wUgwaQhJMTEBGjv9Y+Y2MxVTw6gtmzCA0bWrZ4u57ORps7or7PNbROc2j/3vXWSqtd1fuuDefVqrACiakBe9LqX2TalHlpUQ4rZ0mWvIEVCrKABEiqph0NZsjAZlJuDGWHAoa+Z4xjoiiGAKXiYPYLANgIbgmIUUHIAz//uUZPOP0zYJwgP+4RBAgSiSd9kEDJQnCA/7hEEIBOIF7uiA1cHNSVyrH3Ck2BAat/0JTX/T21/3/0bC3+a/+j3mGdCmZn97jWZ4XDSGUX+iZoq0QOZVSDXmNLReYaRDxkSHVmy0CmZQp5rgWv6JsUaIDrw4oY9tzP+55XDxQwhCVBx1DQbedLkh6Zhz1DFhxzgITfUV5Y8AiI26mc1lwXSpN7jbkcyt49jTcdEzxCMZQOSA98oCDQDTBuJ7MQ1HkxXkTT5BWTZYNDApbzGwGh1Q0sYmTNgJCOw4FKYpi7S7z1LLe77Wfd/M/1bGf47Z/qZ9FTBOQ2sy5a0rMuh1KzaSOzA3yNvIMdrE2jAWlJNKAVIyhFQDWICtIeYbWD4FLhotYiEBKAu0k838pw5fyyzx1nhSicsk+kXHFRQ2L3LqUhzk9d1ZZrSZxjjCFsFlDFpGsFE3/XXSpfWprnLHzHIqKrGmAYEgYbdTsaGi4hlbzIHACGQYPov5gnFsmQsB//uUZPqPg4AuwYP7EjBGYSiGd9oEDjg1Bg/7hEEBA+Nl7uBMkCARtwBotQ9QQlio5+b12CK/XB2Jh1VN3AfFf+hCv+v+3t/RT//9f6zB6QhAzqBgIMrChTDhPcZE4qBK/MxCCbjFEeHNnAT4xeHoDGIDtPTNTsBMxMRPJbiIDVDC3ThrvbOVNdw1ZIhoRuCbDSFl23Quvuc22sWRbUhbyrVUfaVYduzxg539bk3dYsccMaXL3vQK8MDUIIwF1XDWHSMMUqXM3RjNjLLA+MFcmky8gOgvLKK4KlAqLAA9FnLtiNCoZQ7UKHcMFwCXc1Ejf260aEf19l3/9PZ//q/U/owJ0KAMQZTqDLWQhI1ZRvANXBBSjA6QikwKAOHDC5kwu7Q2pCkyGLQIHFWMDHA5lDUUfYSGCoOMzmrVDU1olDmniP2eQ6vr7+1Nav+yuL79jNlmj/qMErDPDCf2u40sVB6NqEazjWkyjYwlUShMA4lkzJgMQIpYZGYMpm0IEOJh//uUZPQP04MPwYP+4RBEYUhxd9oEDUw5CA/7ZEEfCGHF7QzYIOeQPEwrR4wDO6LFlx7n1rq1fT2t7GWlhN2/de99+24U9Vb0IVWjcOq7UdxUd1VcYcQX5qWtonP1X6fIPwJ4GKDAJBsGpyfkAqYXoACsCN/aCDKHIAj36q5Tk70BgmIwQaLBuQe/Zt9Ngr/4v7/1f/qZuX+77vkyGDZARplp5OKaVlAFm15siJrcwIgZAWC8GfsDn6gkmZtSB+bm9nGcFjioE512TNBBAaDBG9IzDNil2WsHSOtNqaLrZhT9b6vSyiT02KNfUxpj391o9CU1mZVjBchHkwzJy7Mcgfojf6npQ1/9EVMJqEbDBKAV0xzYCJNB7JPwSADnVBwvGBwLmWYfF+4xXVmHbNRfx33l2afIMOPFmgih5MKw43QbC8XSojUhnpQ9B+8lc00fph2kaU2CuKref3vfcRUpYuPkGmFEyiIj6whNZsUs1HmdDC3SnMY0MUwCQdDK0AJA//uURO6I4rITQ4v9GbBXgUhQf9siCVgpFs93REFbhKFF/uiIxpGiCoFT3B6GXZ/JDXeaHhGAXtlWOYmvubYP7f/Q30/7P/Z//1hAgBiAoYsahARFHX9q7J2/cnWbNM9sGX0CYZggweUZT0DzmCRj/JhNIJAZL3Ri8EBQTnBjADgqrE7q6xmouw5c+vzyhe7VIPYGtUI6msiLfd+yLG/Z1MnpEE0LHzP3/NHaGvtr/+p/tM3tuu/23zc9uyv/3+I5m/VP/IwYBgHhVGA2mMaTzYpuDv/mauugZLIdxggxB5oFpjyMENBjMALBbIDBwV1t5U1+6IAJbR1n54BJHtaFG9m7s/+5Hq/V//sV4cwPMSSMLfh8DWqFV85p5QsMl/X/THNwyMwGUgDcUFlMB5k0yYgpDCoc1EJKpcYU5tyh+wone/6wxyHCoeS6OcsVM4yjiiVGLLE9Ck2Go/M2o1MDhIDsXxCjfNOdD7nP22RcmcZuUwBioIpwvABlHSeG1Q5c//uUZPSMw3oQwxP9MbBDYSiSc9kED3BFBk/xaIkUhOHF7uyAa3yt5l2BwGCkQsZVgD5gsy7QCRGmbkg4dPYdgsQvov+pbIms/2r+l////+n/9bLmFRBBJnQYtsZmtMaHXamG5jp6+EY2aGiGC0rwa0QcphpHcmiEBwYWmw05wYCTaoeIg3Ak+7kJAg+dbShJcEB5dKmAZKJ3YKIxZw1blpFLnKnxqa6Y0XVuPC60BmiZixlW1bjTGvSopeBjAsZbagQA4mCuxGamropu1tim/6kYYYA1ZilihxoLoM+A6MYDQRVo2RQWgX+SmywxpV1uFlbl//3fS38X1/nbI7+r7P/tAC/5gPAZCYEC/NmefuW50iQYIZ94oImQsBKRgMwrQBhtEyodA+oIE7c1AVmYADnFHDe2sGjU7wq+YeyKoeecftFULxpBBdOsheNfQFkYodGj7A7j5vUqxFkw65acY6QxrEyz9zI0AAAm5aYIAPMSYXMy2iFTSaLoNQAGkxqw//uUZOiM0zULQov+2RA8gShxd9oEDdAnCi/7hEEFhOGF7uiAHzGvgNVBsCkH/gu41uCEe5Zv5jGjZXStkrVdb3fV9e3+9X/u/Rb+//rIAwXcCVM6wKYTQDg2M9cso2NvZU9jAJRVsxYIDDEIA5MNpo4yvAszHHQO/zETw2SDQEPjHH7hVhBeHSbDwswQn7Do4wcQTfLKrEhZasSXF2OuxdY3pUiOUbQZJPYTike56PaTZ5B7gWA2LUMAYDoJxh/KqGcLD4b0ZSpuXmQmCSLAYIV8Y7heYt4fENqjcymoKaiv7mrVC3Et8tFnikZM/p/3/+7p/W7/0/9mjoUwqUT8MfMtejaRawg+pBXdOPwLfTIlwCgysYajIlEsMQuAAyVxIzm50NCiseDQeLiYHMVg9O2UUowamXYYFIuhFojsESDBIQ6jzmirz55z9IvPrc5rCiGGHLrRIFmCoaHoz1VLDTq2tc18x0MSogDAEjDemSjGrlHOdwJg26lOjN6DRMA4//uUZO0Igy4Jwyv92YBBgTipe5ggDWwpCC/7ZEEHhOHF7uSAx4xxwSwrnTKJxYdGhkIaNB7jE7fH0tGqIZWwIBl0/66O+qyO9Tf//+7ZTi2ujZ/qhkwZIJwM83NODL6KQE7VtpCOCXHTTCCxEww9GxjU5CIMNdOw0VwFTP0RNLhIw8OTQRqDAS/1WKvbaSeePDTVBq5KZF0K3HTrFPGLG0IPWC6GPCeKOQhw/Yt3MAdi1WRcsssonFb1NGmHgNErRacQPMIIMExy07Tc0MiOp8bY0iV3zIJEzMEaeOpw1FM4tYMtpHpzckEc5v47Y5cjT2Mlxlz/N/v9PW7o/+Z9zhPT/Vt/xdWTjCbxQEwslxPOUIaLjlcpls1jAaeMUoCwTDPh1YxisBLMIFBxQETBGAogPxgHYBAYAGAFGBCAOKjrjQhRIYMiI8wuE0B9wdXIyMmhqVHXFVn16cXoUsoedSZF5WTuoStAcc+UwNCTbKnvCjgHFFsRXzSmAHglDH0L//uUZPCM82wKQYP+4RBHgUhhd9oEDfgpCC/7hEEKBSGB7uiA/Neteo42jhzEbVAMZsgwxGCNDKnBJMFUE0wKAHTAPAQQ2X2BgGr9jsoEubKRy920YA60++vWn+j+v/+yv//0J7EaDTZq/Q+vtQRNozN/DIc2WoyjLe/NluPZTJQQ1EweEFgMTaG0zAoQOcxUsnLMISADTAfQCAeACwcD2v8YGmAqyMJoDLIYCnluuMmMgfDiQ6A5UccEBQ0HGhMQMPnSIiUwPrVxQUHODJ8G1pUQXFhND1gpcsGkVMnzYTEbTdYjLONiYaGf/+oZ//wk4AGAwrEHsMXpNszFTUY8zyMsMMZUNwTGlQ/sxasKyFAJQw24DqMAAAwzBLwoEaBMDARgAyVGAFgDrxGANgCj5BaQJ0FiSUzH0GvcU9vVNMMfUcxNTF/cVv1L08f1DP3xB33ccL1P+rRe1/PV6yl9Cj7aaxPETxpd1H1Xcdddy1XVXVPWN3uCjwEEXHTMHgZf//uUZOqAA5QJwg18IABIwUhxrwgAEUgvChn1gAJLKeEHPoAA//oFVTAGwkMz8hwYMLljwTc5LBszXpiUMb1D4DCwwZ4x9EB1MFxFoTCXgJsHA9oWAIzANgEkwCwCUAwA8iM7KsoWJpxW7iEZyrRBfTTppPzDTS79GhW7Q+lVd8Ct9DGLaxz2dnqJlIlbHKdj0iDCbKANppkg1lD5zWDXMMQwPswKAdTL5AUMF8LIwQgNyID2aVWSnl+OMrFOgXjvrVYzr7fZZ////2fo/7um7qAAlVc0YfwShs55FGY7IlBnbqvoYY4ZrmGqA3xjAaJ/OIZktJZuADxhORalBbAxCBGBLfKWycfh69PIZxnZ6Wepyp/H/VX2bE3+xCv997LPQgwIEMz1rszdRNDQxSmNMQr0xpQOzBhDNMjUA4zeDVBM3NLGHyIqD8M5kYn7JTHKGMwWgL5Me/XyzOhR8c1TiXPM4JT8DDkg2EwHikTYHC5MQ4BEzgANTak0SZDFwg5U//uUZKwI00IMQgd9IABAQUhw7wgACgQnFY9/oEC7BKJF32QUdGgqBZ5ugDisUxR3e2iXGuvU2+pd/apP6bWURs50V8dK9mZ1C3SplRxaiAC4HZiVJFmT0+iaqjZ5uOiEGIOJkYDuOdLg8Y1AJhzZKRYQuUMEby+pZ307U+iYIder9v/8z/////Vs6TA7QnsyzR6HMr0gSTS8cZg2d5MlMezBRzGvUzNmMD0wok1DMsDdPVEzigUw8tEDUiPAkndKRHRMKhcJB5ItQQVcydUhZ4AalUbdlZeT7Bb0s3/LqvSyZTzWe2hDU/3RjDB0HDZrnDB2huNW1143LhdBJcEwAiLzIBAuNGcp6A0Yto3ItjLP+PG1M31QlHyliv+O/q+zr/6f2/0/s6YAABl6qkwNcEkMWMNfDFI0rgwuBaJM4BEcDBpQgogvk4nDAwzuAw5Fs2lMaipeHMEv5X1H8kptP1+hCBzHXM0f+62zq+tf/pTVp/oXwBg2OhlH1xn5mxml//uUZMYM0tIJQwv+2RA8wUhxe7ogC+wnCA/7ZED1hKHF32QQM1uazpVJhvh1mAGQ2Yh4DQBpkRozDEoWOeTE+OD5hjKlLmETWy//6v9n/////+sQq6u4wHkJEMShPjjIKTCMxpplENBmBQjFkgJsk2s0rEQy5rY3HDky9YnEAkMccuzaK34ZvVbHubpeWlw/mh6lJpYKa9kvFAohaS83N36I3XQpBAzWysc48jXvdQ6cSAzcaTTZg0BZoCsp4xNZx79hzpOBo6ExgaoYJAYq0p8FgooFkrOJ3Buv3Il21izXa/9mrd+/S++P/+j0f+z7kdMCDFoyEM0B1YfOyCO3zWy9s84b081MpVI5TE5Sb0wEsE4MH9BLDE2wDwLA0JgEYDCYCKAomAggRhgDgAMgMjrVBKbgCrJZpIIDBAHK2MIQ2E0GTtZLsky9y0XEI9bElDQHuStR9wY6Ghs6s4JmTFD5oaKXIQMYkLta0Pl0ABoSZh2INmRbBAZpjWJkJtMm//uUZNoIgl8JxUv90RA7IPiBd9oEC9QnEy/3REEBBKMp3IjQCSCuYY5WpiVBYGDkAeYHgEBgPALjwGhaQIANpafVgZaWHV10gH+rfs1f/3aev//3dns//QaSEGvmxvs6hkDLpoZh4DkGH3pO5+th2YYkIIvmFIgYJgfZAiYG+AyGCxAgJgJoKYYEYAsqOGBXgBjfGBlACTvkaoy8bj2rCc61DiQoMBXBK96sm0wtbr0nmi3FLGvehr7HMqElTFtUdMlyLEJuS/E74gUCMu9Fb//9Jf//pMEUBXzIiydIxs1JHMiSJUzGQEeUzCYvCMUOBCTAigOcxKcL/MDIAXjCVQ8IwBECuCADNoiAZvAgAFX+dQXHgy7vHyOJRjKs3hrp6R+rnml+9Rw3WG5aIeepe6tJ5ZKTmFIFY3rqJbu7iK441VGSahO+Y675euutBnY3eXX2TiKqVNmUkcTAgRJtAIkLg4wuf//yh///iJUy/CHgORsfqza91WMxyU+ANMIk//uUZPUAA9wKwY19IABG4ThxrwgAD6wvDBn2AAJaKmDDPoAAlT7LVtkxsIQHMDmBrTIpRgsw4AGOMPRBfDCigFIwGsBvT6MAkAalymAdAOokABhtUGBTIB0XDCy4bBEXMknABUAxw9QViRo5CQqBrbCbUn0PQKuQtxNZYFHhas6HySWZODblHgyfYIzQ44kPyp02otSYf//pE3//JAACAAAGM2FGdKURZlVDyHtCiYZgHXB6Y0lmvkCsYAoi5luh6mE0GsY3JdQ0OejpDhgSgCtdMCwBBXYboR4Y6riWs5rjm5Whk3cI7T3EVP3Uz1PXLRDLKLD3pS/cXMwjTTL1jJj55jmXa7425Sfa2nu7uI+blop4S+iFD7RIyLxMKE0gu0UhZhL//nj3/+gBj+YBECSGC1KmRt+AvgaqE+8GMfsuhiIYZYYTWEPmKKAWZg7QliYRMA6GBbAHRgJQAuYA4AfmAigIyIy6pt0RAOhqHsUbF++pbHq9YcWKGPRWtKTt//uUZLeABF8MwoZ9AACPSYhFzyAADHQnDD3xAADUBOJXvBAEplbCWxHT75fUzsPdWvdTqAgABgdhkGQOAkYaaFBndLMGieYAYxosRhpgVmOMAUYIwA5gfgLGAcAgX5Y6HAIxmz2uvuWtACABhDgTiZhSSrmOqrUBuDLrWZfIqpmMkgfpg4k/m0sGsYqQ8ok6icrpGRCN0PUEYJfqGJacKrIyvfY8lUbHIb1afT2i1rOLWWNIIb6em5q9qpLatzaq1bd5fACQD8wKkhTE/dNMwVqE1viHzJIBAMOhNBZyCmg2kafTawUNBwMWMX03KImRv7ltVcxgBgLqYOciLmDDpIZouRpEZTySAGEZgrgCEg9qAQy4ZA2WCEAMQQlQxv270prQVfxnU6dPXt06/oXOavcrX7ftWj/9en/kHliAUDnahsZdSEhmPMXGueDEYOIeZhwThy4A6oAcKwcADN3bX1R9+gbVX+y3u////+n///v1/+kwU0AOMwEHsDGdGIA5//uUZIuI0tQJwqv+2RAuQPiBe7kgCWApFM/3BEDZBKIFz3QIetR7NlMNGzIKAX4w9h+jZgAkMPZRIw6geTNcyBQLLrmcRoTAeET0B027UYwOCrvfRRNaO3fYmS+9VZZ2l/T3nfXv+7VV7ofNpN8AYGINBhGIaGjKbMZ1Ky5qjEmGN6AoYWpMccAWKvwxcF5gsDigsB083bm1zj1LQ9tvv1f/1//+7SDEAmCqhhRgsi/kaE+RFG+nF/RqkYmsYKSFxmCAYMaLwP5gFHgGNOCCas6AkaV57gEJt8ek4OaW2U0aGsnDecZ++s45VzH7v27+z6qq/19td70IMRC4/GZjOCeTNYBYY08UfTCqEEMgcY/8JTBRdAwIAIBIhG1wunTfyhQssk5cKwiuE//+jqUweYBsM0hBMzW7nd05+8sHNpmH0DGXQEYwoUxDSUD7MKcnQzcQFDQOgDWYgGDDU1nU3yUS3RimfivaE38ijdUtmUtvf++rUfsO1I+lDaxncux1//uUZLSM0rUKQoP+4RA4wPiBe7ogCkAlDk/7REDUhKHFz3AIe/j4uSAAGCIEqYXZxZmMLvGxSgyZfCQRjkBOmOvEcuF5lXgtQGTEU0fXdehpX00Pc44n//////////1CijJgwQcOYUesCmQvLfxt3wbMaJSY5mC+BhBk92xxQRZlm75ziJBmXoLXGCCnjKL3qYwY90UdXelbll7Fsdpu3baW2IbjPwjNqaz8J1WuUBSyd4YGMxf51u0iCmMgYgM2KRoTfcPwMNlQMxgwuTDeSjKILWah+omONV6HiYRnyYUWZrcd+aEjxA3/07//3f+Q/s//9v61MCcC0DNzEJM5d1StOUxNaDWAmNgynoCOMMCHFjA2wzMwfsVxMIDATTAPABwwEkAmC4BeYD4ANIWMAaZbEzAYOj7BhSKj1BCViiHMXS4ercuccNdWRUhj8VDLyy2n6n8LtV6vkxikrWsag9mVVIaowrBRjEsR2M35x85jUYTNbXMMMsgAxFC3DDVC//uUZNcM8q0KQoP+2RA3wPiCe5kgCzQlDk/3REDyBKHB7uSCpMD0DwwNADxgAESBHXWWvxw5WHj7/anTq3Uao2jRRr/7+/t/T/+5Pp+7WYEvAPnCsIt5gV9s+dsAlXG2jcdBqUNOKYVqLhmIAA/BlMAiYYjkBFmO4C3piwgH+YCWBCqLmA1ARQYAFmAWAS6OBZQM0DGzXKU0YfR9rXqdxvibh1X1VbLY61dsK1T+O5bbpiqmbuT0TEPpszw+Irmf6n/4a72c8xue25m99XTvuJfK3Vbt0y1e99S96LlAyVWGIeSLCIFAVOf/6B3//NGFohQBjxA4CYyYUBGTKEghhw6XgaRcLymJwgRZgJIIYYT4F+GBUgTJhyQQ6YBOA0mACgDjgmAdADLLzARQAB3SVqsYVRFrospK8fNK8UvxmPea7zG//szf3itaJf5mYX5ldmS7ZrJ7GM+I26uzS3z3u67NW5Xd5106l8jt35bfa3XjctrztrN57aqvle/ncspn//uUZPMAA3YKQgV9IABF4UhgrwwAFAFPBhn1gAKBLaDDPmAAyLZQwQhMDhL//gZn/+kmATU1VGIKaAb98UhsdfKH8PEAYAk/hoqncGTyZ8ZDYUJhODOmNUCKYPAUZgFAOokgIIZg0/GZ0ami8Y1Yxi0ex/7fTze9vT6f9TGye3d//+8wMwuDIbI/ND9CE1ER0TOzHSML4NkwFRkDGlAjMBcGIOAXHANSgAZnSanN8qjHP////+3///////ZQYI0GEmLCudZljbfKcRgzRmeJJXZjswR8YBp3hqBgQGDANEZkgOJ27QZ+AAc9TJM38mqW+jQpFXt4u11MsYgsu5euz+16V6TNVXYY+n2Ldd6Nmn0dqeCEKzR6tzM+J1NVYhM1UAgTGhAHMrxoF/cIFQsZ2XItUDO7+tTH51iTPiq//////Rp+1dVmZmMBTA/TF6ij0x4AGTNM5RUjMAyNAkCHTE5xD4cGjDCkjP4RzDUkYGBJDtt/KuVu+5Pc2ldz6fTs//uUZKaI0oAJxTd4QAA54TiA7wgACqQpCg/7REDMhKIF33AIGf2WWU9vdZdo1CX/Xb9q/RqMCcDYxUkBDNdcuNjc9Q13SgTAZEOApEBqJGRekVsZqprOSgOkeuVuo7Nv3///Wn+r3f/////330GD/hehkPDY4aeO21G4sQB5rbg5qYoaAoGOuGUa6IgRhgpUGL2FeZghAIDVeGjyWcItthpK0P3qJHHop7qUWx7dG/Tnr+/oTT1f/fiiWeqv2MVQgwNIA0nuYxd22TagTjNOo7gyVggQaA+JKLmBus6MQFbhXHgFvuNc87Z7SSWf3dVf+9S9VX/9VKowMARQM6ha7DKUq7U3snKKOMTHlTCJRFAwJQIHMkrBSjA/RM4xGsApNd1UxcHDBg3M7GpeL7VZBFdYvVjMPDM+kWN55VMdDg5rJ+NppW1b6RsUaRXTLtHi0WUlhA0ppAOIaBTrjTyzVT87BtOskpjWzKWSwuABBNdV0YsHZvfdnoyyH++cG7s5//uUZMsM0loJxJP9wSA4YThwe7ogCkgnCg/7ZEDdBKHF32gQmZYygUhwVYxUnJjAKqvx3llz0lqXqO0eLso/2PTXZ/6mdttH/e+p32/9axGFyCHJlk7KybdciYGaIUzJsqxogZBkCcmD0bGbNIdBhqiVmdSCsehLmCCAWFDYjttIPljdlGAu24GkKFBDAjGBdACGMSmhilyxwSKRJyx+6nbKXV2B2t5Uv14j0WcXILRQ9dSblugYCIMpisG1mmOlga+y75heJRGM2E4ZTwp/8PBeEFum82VhRFC6j/lRzyQVTVrMkk7Hrveru9UZ6LrP93/r/t//9NUBzBFQoUybxYFNiTPFDE/ZuY0Vc+DMQ1CTzC/P9NIcJAwdT+TH/ByMnKwE8DQGDxtEukuyqTjh6mS18XS3Qm5mrYmpUxeMtlW1No9PMXOxRKhXh9pwU73rLZ40ZZEZxh02YrkgbhTAYwsF5p4uaGTotOZAwbRgAESmOSBgFqg2UWbGtJSNCTm///uUZPINg6MYQQP8GjA+oPi5c7kEDLwlCC/7ZEEJBKHF7mSAuAftASVOcu//fTR0b5pbtK1bop2UXp//9l/o5L2sD/5hGYbkZNslJGKeuyphp0/CZHslMGJ8gthh7pEmaKBWYMyVZhzA8HCXY9EAwfMYQHRjUzAYYiQeHwKVWXG46HD4xzWknz25q3vTHqaeQMFlJsJERa3UK3mz08YhClBly3kd5JcetCiYxoeGdTDAoA4MU1Nw00GWDLoe/NF9QwxEBBTCqSzQ8IgBoL0mKZkzhiSessx7dypYo65t4FKjxcolg77WC8z0Iqrqp/9S7aa7NPXad/1qMEwTYJaMlzdgDUmIB4wGCyAMT4W3TF8gzQw5F/TITAHMSY/UzGAXBySNeAjBA44AzVgl9JEZ3fPzqbARGLTCGR7oLhEnmKCOZUNbTUHhiN7qybmuY6lm5K25kVR4QQenaUKKEpkg8tPT9AAAAAsUbbbTBgFmU6Am55bGSvUHSC5maYGmF4WY//uUZPII0wcJwov+2RBFIShgd9kEDaQlCq/7ZEElBOGF7uiAIBQhsLHHDIs22+44lSpngeHTLe+/i6//1L/b3bCOn/+v2dH6TCCgvkzJtZDM7/JEzJxcHc0F9fyMQYD9zDkZbMSYIExcAGDQaA5MxwQGcmTih0paXqa7LmkWMv/PC9mNIrAlU4JniZDa5yFCFzCzHyaTihCefGGDp2/GFC5Mkw80UMYVspJPaxc6YICh3FjyWuCtz9SAOvurwsBCYHhSpi2HymCEiCerjQY4kEYAJWZBAUQgK6oFow0rLLyzj7btE61J/WM9KNrrt9/q//+zv9qXf/qx9NVpqqMAGB9TEVFN0yDBKlMr2bDjOBCncxA0CnMo+dOWAFMYKQNmBbMenAFAg0HKhNQ2b8eoxaUS9K3m3SJwHz5JCKbHNpbPgdZhxEyk8fXaNDITtebkBlyHCxQSwZkVnUuE9mbFjgukXkVIfYt54mA+Mc4jowC4CjBtg8NpIWoHKamGUBm5//uUZPGMg1EMwgv+2RBBIPjNd5ggDig1Bg/7ZEECg+Ll7uBIwOBWkJ2NrYodbEpre780URcTYMWsXs6GvPKOfc3u6Lau3kOr/+07f6vb7FWtCmqu/MC1AgDFhB/YxT0+cMr3QuTLNw4IwX0GeMNovOJgiMC5AMLQ5CVg7lgAHpdS7lN5NncfWZd3Yld7iX3Xm5Ydck3t8XV697pN95QaOpQvkrNrmKel6DUMCsIAKn/GguZuCG9zUdKDMNsNUwMVM4AAIYFEIAI1oHqvmTCoyIfJkrWd+kuxXX7f+3X6v319t37P0f6FAAAJqerMDlCiDCMUV4yb0uPNK4TkjO2gU4xTQCJMHnnA+EGSs0GzYVnEnBikLhjcj36s9lNhz1k9ikRl4Ui5qXaARjwxO3tLlGHhxlzDLXUzb67WNsqcpdg98a90ZbO7x5BndPHRWVUOBdMZcOU0gjaTEuWcPTtqNhw0CpZk34gSGGGDGEmCxFdlvL7oy6YPWeY7Vb2ZVq0t//uUZPCIw2wJw5P92RBEgShge7kgCwAnFS/3JED2A+HF7uCIalNv9/67vs9HuYJ9V3WS7jDkApk0dk3ZOdASfzuaZr83U0vvMYPD7TBrAmcyBAB4MFlBVjEbwHA5PcMwBjCTU0FoYXBGa+AEXmpcQeEpAy6nK3ihuSCer3ayzbf5nIZirnFft2zP2/X0/4hr/5647Zst8av2e9wc7+d5WKv7XzKq7W8ndbB0gTFVTRgNA6iIskw41BjJ7RvNUfHM3i2MKAxBXEABsa4Asld/GC2uuuc0eq27DjvY/md6UqQlte5936Ni//T1blY127aYAE0tVGBRBmphXi0CZOOuXG17KBx8DXWGg6JsYYoBBqQAxGHOVCZBYHQ/lAzMoEC81bsDTkav6w3ks5yTHTblnSPpf8rX1aM77pxwB+dE0z1v1OjWz3tHaguK3Tv7wBjlra2Nwe5lv+Ihi138l+nTTretsxlE44QSc04WRzV6YgMYVZQxrhAzA4DcMo4B8yyI//uUZPqIgywJxEv90RBDgSiCe7oSDuxDBA/saIkShCJl7uRIjAJ6MTa8NB5F+phpsRrS7S6ur1s29Wi5nd/2D031fFvu/7f6RgMJnB8jNAgJw1U14lOBbUETL3U+0xBYMcMNAi42AgRjA3TLMDEJoK/wc3GEBxq5mza1YgO2EnMh1rHgJpTHlQEagoLBRBoqRKlmkxdZKM4+lrlPzGxbhdJliugVUjsvFEwum+hjBNehQ9h4M1TMVAPjAFMSMzoBg0Nz4ToK/TTUSxCbxsEAII0KxBDYqObgDObooMCiUOf35X72eXT4q3/01//9n/mr/fQqMC3EBjCB4po1qQ4qOQ6OQjBcmB4xcoPVMEuEnjFrgG0wRYSNMLQAkDq8xrMYUmd2ImvMTMAhSG4hY+57fKbEIsbGOaVRJNQ5CjKn7wRIrEc2UM2rQ+FWn2NMnSiVFUJi4bwFcis6LKHHAWj05Bwog0TwuQBiQGnpeEZELLBshKaGpuZsYm4bQE2h8wIi//uUZPYIg5QKQzP+0KJBwShgd9oEDXAnCC/7ZED5g+JZ7uRIIxCwLEgWNB91EKb2saGu5qWXP4ulgVb/3f/MK+z1xvbWhNzf/T1L/3iEYPwCTmamjoRjjLIAcvmKamZlKpJjIYLqYEuJ+ERIYanNOT6IYQIAY0gKrCPJwsyLzkcClnMgaVK3MGbctV6uuP0bMf7Xz/j2p71C+t/7uu058v6l/e+UXfM+fbtBlwtL0A5ite0nS9O6ae70rK0fT260ZbjjaJLFAOYbzJg/TZwCvJwMVYKXgEUAKrSqBJZJaMehhA9wq6vZqTRzCbOWW5a7j3jFf7P9G9n6Lv//PVUwRQNVMLjiWTFLJL857QanNR/REjEmwswwAlljKOBDMVwyQzDAYz1Qk0kLFCAy1NUGdmJPVfGjmhseWc06IBhNBZU4P6LBrjR2Glw6mEy5xLVOe+cLAFAttbvXFzCGsgYkhi8IMa6m+JndKjIBKpkklBszwQs3+ro+ojo/tCwxBJkw//uUZPSMg5USQYP6QiBDAShhc9wCDmBJBi/0Zsj4A6O1zuwMAWQwmBGGxdwtMmFE4Cs4JiulDizWakFkHq6Bf07EV//f+v/8jYn20fzYBFTrJgSYGiY5oKdmHhHUhsG5A4Z2YPAmJdAXhiVnQBCcxmt4xyEM5CCetu55mPnO369oN3EFmWUqAYOunGrTs/S+oM5EPii1HdW6USzC3TKNPopNOvCWKOzLZGhD09SQCOm2jBsNDJKYDtGEDy1OTwZUAM9RiWSGSwWFelBkcSY9E0Gi9J832twsQfZQtdX6ew59dXRhL/Z791id/9vrMJ7EejHCInY3AUuyO2mUzTd/xpAwVcP/MEdkwywQyzA/UoMmEGs8lCHqICgJzwYTANrbdaLP7w4uvbQLk/i3D3K+aipWb/vdfteO6/moPt//+XDNdou5ptNG//V914bd40Ha/6sYe5oR8ml/PV4/fPtstjAw6Oq5gymFxzU4B+PndNMhi6BDCGkYIlQRUrCgExoE//uUZO8Ig18JwYP+2RA/wSiWdwI0C/wnDy/3JED+BCKl3mCAoEXve193Vy73MVz6pgs1n28UtR0f/pO/2/3pN/9kcgXzBMAagyrcrINMKYVTfMUlw1VYbEMU7AMTDlQ4M28NcwpR2CJXY0atFocUCgKau5/0lnCsM44w/BFa4v6vESPj+dfqLwk/uLedZHy/+Z05XiAbtCB/Ju3eIHP1JlHc93ccre8f88i22//zm9XM8prWtoAYJQQ5iNmPmaUt8bUYhhlcIdGNEEyYOvB8ENhS8jMEHI0VE1ht63je60SrVL5I6MGCmX3f3I30/ov/b/d9f6G/9CoAJKKqYOIGpmKWpn5lNipUatckqmeJlu5gkAXYYmS8fqiWZauOcXh0ZggcEYYAGdUpAco3BQfWUUHFtLIOPYFTVxfUgiTJVGEoVLXKY9MYTYL4aa6JHc5qF5XQAKUSL3LcWkx1RaiXs2gCKVpjC4JN/b8zRw+zR4E9MT6vMvxJMKSgFqHaeJBD//uUZPeMw44LQQP+2RJAgThxc90ADjQpCC/7ZEkJBSGF7mSAEmLZtQJqjqqN8beZrbSxvq3UOR+wj+OSvsT+r7vo0N7kbruuowQ0BzMauKcTO2x5EzSFA4MfKKNDDygX8wdY48pHsxbmUyhCoLEmDgCVRMLgIZLKr1OK3+r6vxBBY0BNawKGo42KMeLvQBBqSVb3BiksXENJgVccEzjqXq7hz4bQqlhN7SjHorWRoEyNAUaggDAhBoMN4qAybEZzXfKKOL+4NSSAMGxwFwTJdAi0C801Iy+fUHRiUB4daiMqAECiqP091Tada9/+593ur9RQZQS/b/9ReiWJYxxi8jPCxANwI1s7Vu+zLHq5M8UokxbQPzM/AtMDsbcMDlDAcAEAOicFgB2Xxfsyhc8vvf5koQWlqHMdqSXfNNILPPr04vpdajtTf0Gbza7v11He4U7Xx1YEGE+CNhjZB6MZH0bimVdJ75kKpeWYyKHBmEFgIZhQ4EaYBOFjkwLIYBgA//uUZPIMg0UJQrP90RBBYOiWc90ADbhPDk/0RQEXA+GF7uRIQFryUAKMAGAJ0w3HuUoaD6jS7VSKWEa6WC2PaswLjjVKkInnona0A2KvnVDReVFkEvQlTkI7E/vIlWKtrFzaI4sA/xtyyNMuPaTWbykkyvFJ4OUigYDJYRHwwwQBIMisEwTEhAcExncTlMPoBYjAAgE4MABzASAD1AIYF2AIQ6OCaGpQaEslkw8aWcXNzCkC0tebnkG7x+hNybjZrki8wVhSnolR1labUD1tQq4Yzdr5IOHXUkUujOKztT7lbgqtpsw6ghRug0refPr6RieNj7jY1Vo3RM7rNTyS6CDXnz72oQgf01dKHH1GKIaq0MRRRPsk130aqxaF0k7/////////9SDv/////////9SgAogAIpQoiaYa+XHbxr2xnzge2aviHymIZwWBxKS/4Y4uASGHNABxiUwP+YgYBPmDkBc5h3wBUYCyAgKxmBRAGKChWBNwMeeNmoIaaOMO//uURPEAAtcVw5V4QABo4ShBr4gAGgYE/hn1gALqL6AbPmAAOKwmtZ5K6yBjY52p0W7a9li0d+xXciaX4vmMcmSdJxUlHBCBAkayrC81zrskoVwst+gWQQfG0+AJA2G5gWdqyZrSpkrZoLohhzsac6TAkk1wnpkJVqdJtL3Z0vqBl761yyz9pY3//iE9//qElTItWak+9BgpMw2TADStCXI1jVQLNPvUUTB/hl4wqwAEMRmCbDCQATUxqEQaMIyA3jAmgDCKGA4gJ7MDACwGZdKhgHOPuo9HO4i53qIlHjYqIFkIpS2NFWVEIRWD4xZ4wc7kOdZhcimQ5bnJQUUWGXE3UxXY9CI5DiTi58iGMeQgiyJOqFc9iVKWejkjCkGucxXYgqdyWV2OawoxHe6ujIpF/////QQd//lRKaG0l51oAwGCansaYo0JiMJcGXWcWY1RCxg+gBmZ8J2YGYM5legekgQAOAJgYu1DjD70DNAdpq5QPXV1FCFVziFhrNmE//uUZHeABVVwQQZ8oACHqygwzwwADgApCl3xgAEEhGILuhAA0+VzB+ovIpBmWEMbDQvSOe9Feu5HEi/mlT4pwjBiJ7J+RnKnNselNJM2NxXxf5FLGpQ2N/4DNgwYPEwFR//x7//8usVVlMGCD2TGqjr8yGQFTMmxW7DM0SewwlsBXMCFCBzDBQPggCUjA4gHshAOTAWABUuoYAcALu6/tjIU4dM1h6hRsDBafecfM7Awavic2sMSOxQfLN203uBxxEVlGMJIAQteF1ElFZB2cvqbERwVysWZVIDAcDDLRtDfvpjf3iDudQTBwZgsdZmeIQkJ6C7oiwCsRi3a2RFDqiWmJRA+2tm6v1+LL7r/+jFF3ImWX////WptTBuQf4yMBKMNLPWpjHmoBM01AYlMRZAYjBQxB8wj0EkMmesNHyHMUB3MQgLL9ExpItQLIoJQPKAThAqkX6Qo82NogbH1aeXCY3Lkf7feGfOpy/lSI/2uZn6+TeTfMydvNvlpG7V8//uUZDOMg/dcQQv9GbJIYPhle7kSELUnAA/0Z0jyA6KlvvAAr6ZLmeh3zVUY/89c71bMQl0rhLawqDskAHiDAJBUMIMlwxD0XzK2TFPDo0NZBLMAlFMawGJM3+BCJQRRsUshRp4s+VxKI5Ao5Q89d7PsrTWX49WpVHpO/1L66v3qb39KTAhw8QyCliyMOod5zHOpq42JUSJME1DDjAdhU8wTUEiMqcIG3CMCyQAxgAAGTD4NnRmpudotWJ+tc4Al0iIq8q01JARhmhohn5itaRxS2emeS7i3pbdJUBZ/tPIzNDzhmTX64iniV+9OXZjrmVwmxkrg0tcshZBfD5fsGvB0EXL4udKM3TAe1VTZhwec/TnARqGsdTHEa+ZpFo4sQUEBQFqQUUU9aoXLiqyWmpGK3yTbvUyn9Po1d3/q+d1f9CEdNCEVACABg/wSqZFOYAmnMFuxmaLeeaFQP6GKBAepmd6JnwThiGY5xuIhgkQIcNhYAIw2B9mlHqCqYzML//uUZByIw70ZwSv9GUI6gOiCd5sQDslhBi/0RQkBBKGJvuAQwTnOg6cvhXOA485CGEqI8Pzqkbq//WsxpNn103+8l1e8jy8V27nu/0QC+GP+bHd33tTx9Dne+RRCd/ZlfvzhKsoVApMYG5N54ANGMxMmeo1eOgrsm6ACNpEGt3VDuBnC10W6aEE7NF6NPfrvr9WrfiDlKfq/dr/+pkymAfg95iaSB6ZtqR/GZoLSxlSZIiYRSCemHd5nDQiGBstmlIfGKAxEQUsuFiLl9Bh0VdAylYjugsHYZ9XRr3Td09yKkzIxEXd2Z22t3cjsf0Z3Vn/NlO/nIjR2Yuh3d5Xk+sy012d/Wr1I7oV2GaQodoZzzp9CQABj5UfMpGXPrmFHuGglGmdgmGBB0gpwAYomiDFLd42mHcKzzNjVXpOBJq2QnKo21019v9f+nq20e3sT3bfsSmWMEPBYjFfBYAwxM7lMp9RcTEgiQgwsECkME3nOPApMVorMXA0IhDGgtQ4g//uUZBYMw08Swgv9GUA5oOhxd5gSDmBVBC/0ZMkJBCFBvuAQwDn9mt0NpLKU+dI+qKBF5gTE4mYYp2qlS6s8lN59BtbIu0gdTaxguTcsRkbXHnn3libqnSJ8vaXm1I3sqinIhQDDE2ADY6YDSPGzw/aMjFcwshSbYAzTNEATHZVKRGI2VvToltVdikI/1eltVX/6aPR3+lW3196UAMAmBjDDqUagyBhC+M0UStDCLC90w1IGSMdH3PJBFMpnlNegYMSA2UtRMMMgJbwWJBjMSrY8IQRgj/PyQWTMkO4G3OsbMRLds++eu/Z94D7+R7//ll5D5J7W5zZZd9431T7Vs96df9vrJ/+jp/cBano+tCSd2bm55qG83OHOi8mZYFmBIykU2kPESwQB2et9froa8aOeu+YDOxnVZvYEv6Urfezeru03EpLt56GGVr/bie+7uidMIYIEz/zRzOBGtOSmes0YW0jD0EpMYBxOBhHMZAiHolO/RC1UwZfIrOrmzJWZ//uUZBeMwtIJwovdyRA0IOhxd5gSC4ApCC90ZMCyA6HFrvAAKrofPvgmxNSqjU1vs581qc5jgKOeKNyRS4YOlWPUfSyg1XZsZzPeRY5x6rBAoB5A2ZpZN5rTL5/EoGQyIAgKLaFRRC4Qk7ZhwDzkZrErjSv7af9Xp9X//0/7O/+6tqmgIYGgjZikW3mvJOydekJBtbKpmO2B2YhgYceBqYNKAZgBGgEUbZGSgJLyjMLYMEwifS+hqzHsb5cCISSsexts+tDWKRRwMK3LUIY4CvcIidjnPU5+1taEuRJUPUwCpz/DDDGSjTh9j4g2AVlAgkCLKo2nFFJHUlIN3oCDuyoUahLt3v3f/v/2UQAzPVRMTj/UIMilXw16yuTOxFcMI0Hwwo8AeKDbZNMLjDzyxCFzCm2aaFHVZJLRer1JT1r0sVWgnXPU6NheijN2JtXprV7f2XpAAACqjjaIIKqwgajdUU1+LP6rMCBJBw1JFpsLl9fNq0K3CPQ1NrNGv6+j//uUZDoIgkoHRLOe2BAygPjtb0sTCwAhCA9zBEDGA6GBzTCI/v/3ej//06zB/EuBsSBrHq+nb4WIbPgNJk3AmisAO1BkyLADToFLvq/Y0ETnqqxV7LgiXa8/tCiHRi3pVtbc+ZZWtaK7D6kdoik7VsIvc5lrbQhQqRFXSiULdVjErQ+1HA0GNDyk3OsS43tFDPYCEO4DzRwG3gvGdViR9LP/t+m44i1fQn//b/pySbf1+ohZ+xUCDIQET2s5DeGMROl0Ks1JRkzD1C5MTR42YFzF4cNZgAEBQaBzMy71UBhh4SMpME6C8NaGXLixuKOc6q8rrE78W3v7UGbaC232abr6G2SLjkX3fJ8ADAmYDZ5h/JHV0+YziZjYPClgSgtxU80hzlBOMtYvigcqh9w1Kv9urb6v/f//6PvWQBggB1mAMyCY8bHhweDBnzVpGmotGFqADRiGFJVg4+kryYT8kyZD+6YPOeJSLzYiF5jTnmNA8OyyEUR3XOYei19Gur0T//uUZGUM0p0Hwgu+4BAxQPhxc0YkCpQlCC93AoDPA+GBrhggCndwHSkl3k+Rd7HvYw/StCo73s3DUTyRoMsw4yEIyQ0MZbAuFqcKofhB5czH/3/mXzj3OkP3+r0U+309C9nt/0+pWpUAABmlmkxGBjnEnNx7DPM1qN77cy0KiU1JNjpdV0xVq8sd+tM9uTSsJLKizKbOvd93Tkv9EUF8w+XfpZt0RRZP/6SAHQAFlsCrQddF501uAnMKqQsVidI1QpesgCdIPYGS1qy8fJCpsiyh2v071/9P/0+//9KP9/vbcwDFQKccbfIgfoU+IZWMeBRAEoPT5gr4lQTicaXsuzCDhhtoeMNHLLCWePD2mQFRbYvYt+vS/bo/M/Yy5xtOUnswxNuv2aXuqK6pi01GXXQRhg79uMdEBk2KFYczOtLXdBubvtJJZym566mfS2x/93R7pH8Z+hj/v9Xf/1IABR++utSBp1sGibUfbwBpJ8g4rgm4jOS9HpRSf6MbZLSV//uUZIwI0g4HRMud4AA2gOhhc28SCTAfDC7vRADPg6GFzbxIODGTNBsWcHK1OSKdLrxju2pDdaAMbI3dHd0+ds9nLzz9LqOBgEGtZiV+dGKHJF48skrRXKSjcUxhkeswkUagZb7+91H5D1a/J/f+77P/fo8rstp3AQYHB+Yg3ia4cSezYSe9wxnchDsgH9gQ0ErrOoeyf8GDoOAUKBYqHWNE6TIOZJAo9RpJkeeoVUy1K0nlLKkMtyaBY43SZTaLgAXWRd9gk8pi76HN9S2JeExCk2DY9isF1lKyURH5GLwPB2ypmMeyxNGhXXT67P/Z7+//+/0b+ulHVckwFBM1OfEyMpo/b7g5eXUzgAEKnwLLhluCBUlAYzPONaGQWhoDkKskMREy1crSM5C9joxSc9FMlqyjOc0jJXdz+uRTBJbRc5nmfL8PXczvmZnqcJMGxasRltIRnWrEp8hT9OP4xkvmoBp9GuxtpQkv7tAxMc9oc9cfRA8UuYiiEbw+lLbx//uUZL8IwiYHRUuZYRAxYMhxbywgCrQdCC7zQgC1AuHFrCSARCLlzd239juliXd6f/Io2dWn7PX3tpq9CuMMGRAMZLKO7zcPSuPONzEMHQjIAMMiwAMARUGgfZrBT+sVlAwE9q9xFRmXoRFOTYWKG/1yVwyUaG5mqK46XXOmSKbamTnFzgcH5Ez2m89dEiTJu0Gbsg9cmO0qCIWLzsdbCPJqYIg5NIZyDJMzNqFKx6doyEaP7WmRkzAkGxAw6A1XM80c6a0eK41ML1ybS2AQ0EiAInnhFjKZ0MJfm0LRQLHNKilzx2bKPSNPoCTHmUmCO+6wjtstZvXIbG2K83UhdSAJGsYMcguJ2n9G9BAUGZH4fqROKz3hisnoagmomLcZzn2iBMkNEjqpBbEp1PjMXa8izrHpKlM0R6ll0bbKK5E+kY5SKQ5F0PVzYpZ+UbWwpoRvEQ7QTFk1P8jK8NmfNUThQHR8HTgdxqMAhkRnMQ0OGsC1kfZSiVVvB4PEjY88//uUZPCPw3lVwIO7GTIwILhxaykSD5WI/g6YbQkSA6EBrKxIgLWBTStLExV3ek+G3htzWs778oLrFP32121dG7aLa35NowVbYh6rkorCMrKgwFCqyZk2GlVpvyQgXKkEqiv4BgSIJB9QsRn1Vxkz240LMjL8kOLC5EpKZctJW39655chnkXJTbdTlJqfKcmEbM8jNU9pprpmslP03OK5eeicntgsabX1unCUvSZbntXS5ocM9VBhZEQw+RAqDrMiyg8Co5w9cOPCSkkWFUswMgJxZKhQohVljrENzaXyLSqDwqLiB2NfcslRlbmOAipx73GhemPFiyUUWqkRlLukbdwHIfse5yvloOhFJ2EKspvv7dKz+IBjMR+YLrtxKWI3ZUj/3TWWuofqrvtmsjLMrqbjvUZDrArov/so8lVvTOr6N4478eHTOwXisAHKrCtoNEXGdYFBkIONQOFqbHMbVKtg/mF4FHc3XL32r0RdC6ViBh/1Njpq2bUm/74w3X9N//uUZO4I81hZwAOZGTI+QKhAY1gADE1NBs3gYokgg2DBhgyAd7qcX3Alf+Fa8zD+ecU4FTc4NcF+9qX/Mippe9YC+f+lAAatfRaIRNOh1UEFgqXEpgyXRrXWjJc7AZLrPXZMY66pUqtVSAjVSUqpBhXrszUTD/qs1X/bX1h0mP2bUul9I/qxVjdJm/gYVRJFON6qf69WeXVIMKKHSjbEzWH/8Y1LNhWIqUEEQWBoAo6tqqTPSZxMDBSawCb2ZtYBUBbVSh1WagJawCL9Yx+ucY71Ywo1EjCrp1q+CuIoi/iIqd/U8NfbJEZUt/1hpUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZPIMYpcZwZMvGKJWY+gBYMMsTIFs8kwwZckRFB5UkY0yVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"/*END*/; +<script id="dd-sound"> +const DD_SOUND = "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYwLjE2LjEwMAAAAAAAAAAAAAAA//uUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAEJAAGPAAADBggLDhATFhgaHR8iJScqLS8yNDY5Oz5BQ0ZJS01QUlVYWl1gYmVnaWxucXR2eXx+gIOFiIuNkJOVmJqcn6Gkp6msr7Gztri7vsDDxsjLzc/S1Nfa3N/i5Obp6+7x8/b5+/4AAAAATGF2YzYwLjMxAAAAAAAAAAAAAAAAJAXgAAAAAAABjwC+eaeUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uUZAAP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAETEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVWu1SCQSiMSBsJhIEglv5aaJeRvmMLGAx41a1odhWJUK7SbMt+F3DJARa8xHUNPMPZF7ncrmCygIIQAMGCxgOZgWgKDgbqSgGCCmBhJpgcke/AzWKwUMgGPTCBo3RAbG1oHzjLwMriEDHQgACCgKCoCQNACD/wxgBgMAgDAML7gY6FgFBIBhIUg4CAYYH4ChD/AwQAgwoKURccYe6BgYGh84bA//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEFwYWXfygTiRMGhosMBCgxBYc0PKQMXD/plwuMThoyBfKRiRExMSZOf/l83dNN2qSYsF5aLo//+pqmfQ6RsDFTnVvZ6yUWiUWCQSBwNBIJNDlpprZQCY28bHHv1syR43YqA47FzM8hpwyiUZpML1CLzCjQ+xQSVmBygEJfgwZMCDMDQBbcDhiqAwYYQMOPEDpkb4GPQmCwwAw+NwMqw4DPUVA5YOeBmcTgZEFAGEAECIbANBUDCoD+KUC0cLnxH4GHAoCQOAwEEQ7AGBhCBIN/gYMAwW8CdDVMcYb6BgoIhlsL2A3eBEBfygThsRQqGiwtCEFw1cLlDDkDE6/5uXC4xOFxkEzJZqiiYnP/UxfN3TTdqkmLBPLRdH//1NUz6HSNkTJIzOcTQAAAAACUADchlzEz4U/4OTYNYzTJgb47Osu5N5uM3TGAgacwSADhMPqBizAjgM4wqQHkMBK//uURP+AAAAAaQUAAAgAAA0goAABHI1nLbn6gBuVrea3P1ADAzzATQCAHAC4OAJkSDAIQE4SANQSQeXMKBqUnZCc7AhYSlcHxMzkAVBJAjM+zxWaWln4rYQaiqtqxU0aagv9p6/N1n8bW5S2Ydq09vn5fnW/DPtWHYzagGrzuX/+/1///dVcq1bLn1P///X//6/+/8ajQGAx5llvRhISrDRZT19f+z//r//9QAEAAAAoixpdnoGUIUyb7YZJpNjqmMsaMbEZGhh1gUGAqAUYzoL5gDAJmXKAqYAYRCOMNKKKwGA4Ae1yNTIKGnWNwNbncjOFM69wgAzAtxHceYUmC3KSRVfrU1bljHOxM75OORl/9/8P73Lss6QSIioUqtFzBERWpm+obAro3V5d63DNjWEtyTMaUct5JABmWrRWJ0187ydA6haHbdy4pnviSKYQ2VpGJogXRhqgbIYOKB1GAoBEpgqgFMYB8AUGAuAFhgIABcYBcANGAQAEQkA2mACg//uUZMiABeM9RSZ/AACNxHiWz2AAFgCZCD38gAHTkuMLvPAAAQidDBQJWAoF8J4JAhQUeRCDAYEX2JV1frGcNh7T3tp+4ZZ36t+9G5DnIJnOkitHCoxA9JKL0m1hhrtW9r9dywtKDQFNFiQ8eCJLF3iIcpSEEWFHy5lk6mBlhcBoZSGkFWWxsJJOKL/Lie35kQuCmEmGQZRQM5qCDuGHuDUYRYQBgCAdGEACcYD4JAjAtMAEBswDQBS6sArzZml6jkXAhx5EBJCDlmKkZyoMFlEWNA4VmidtfD9/XWq/ebfG9Pk4qmFZ1ikKfWtb375zl7OKnlEu3bp//7rm1Y9uv/utAEKjHbnT40oemoNQXEwjk0EtQw4sJlM+6A5zCJgFIxEgAuPnzDax05MFMuIChlKw4FEiDhcwiElcGEDRUAkFDBgIwsPS2ZsnO7qAclAVMEIXciLfPeRF1xqcaag3GxGGWJGjgInkxYePDVSjHlhodc4sehhzlpruuz9Ptpfr//uUZD8M1PJgwxP7OfBlhMiReyZMDkR7EBX3gAE+EKPOuvABNXp9GmnnWdFu251as81V2qq3W+qrdDlzGnfPbslHaUoCAxYrIzP5P9MzwK04DxGTFQDRMPoWQxZxLDBfCbKAkjCuAQONYqFoJHnVjTeFARGIj4RLJcggQGhjgKfSAmgbZfdMmuv1GOzU2QWxSt/g6Dsx4aEgCNnPu/tn9T87G4KWo0///6DJMoD42ZeYYMsiF2DYNxO0yBYfWMNvFIzBmQN0wE8FXFgfYwJ8AbMBzAHBICKMAVABB4ASLPiQASjShgCzAKR6Q4AfgGojhjAE4uzUJKcyeJqxODJ4uYcS9davWes2Jt3s1637zAw1T2rdd+xvfT///9QjUN9LVOrd1MbTdNxESJpoMnhFCgyGIgGhwNGAgBu4im4Mojk20WsVS6NWAjTdUh0qLSHp8/XcJwxWtNbeW/vekuc/P9Jfr5x87g71vBH3igAFsWuagAAAADPi1s8x9oYOMcYN//uUZAoAA0Qcx8594ABDYpq/zDwAy1xzFj33gAFLD2MLvPAAwjMKAmowFUlEMoeFwTAYwK8wAYAhMJ6A7gMAxGCwAKyeYCAFk6GXlqVYGIiaAXgUTwnoPiOdRPwQRijeBrlwQ9GuKxWbNdTbg7tTD7WcyWrWA/fhUAARKHd4h3eIj/gAAAAAAALim21fdOmflFrPq652SgZ7TYTI31cDQVpYQE8egxyDQz3ai3xE6dR9buqb3i2kO1lo74RwGFfjjRpPDlCZVCAoGipg8xioAiSYbUFfGB5gRBgGIDUNAPo8ABGARADIYAFoKsKXvZTWWBGSjENUqQQgOIv6ScFUe5M2xsi6+vjNv/vO953jdr4vHEYqiy68mQAMGQyQ1DVRjJCBoMF8MkwJAAjHDBoCgSxhtgPGAqCUxQwDAA2aMIrv9C1DUJAhhNidnO1kWj1IXy6uevELlrCxTWNXz/JnWcf/FNzSVkxJHJoJFWMc3H8TYokfoz7sFwNHKJ2zBhQS//uUZAsNkwQbxRP7WXBHo0kKd2wWCyB9EjX3gAFTD6JKvPAAIxGIG3NU2jW342ISAggXTIARyn25QK/RoaWtyBJO0hTZebdIactxYkBEKjV0LOnfM+yXtNLtrHmwZAIwRH//ev0f/9vT/o6rdv6gAIAG0YAANDuCOM4nNrgdM7lSO0YTYkcw45K0RC1mg0GSEhuxnxVKSw4NmTAJWFjswrWt/p8tUY1decs+x3v7ADALgsk2HPWYxJNLZMLHjdDGVhX4zts2aMTaFsDCiwbswMkDrMCbBMwoAslQBLKAJMqgA7rN2bs1BA19gihrLxVlaRwsaNSqsJwnDPTCxB1Pq3zvf9NfdN4t853W3+Y6NQMGkyKYZuEGZjdg4GGyzuYAgm5jLAdGD2BiYcAGw4A0BgODAHAGTHZu7cpdlMVkEsqpNIXAXMhJ/TLs60NLkayXe6zuH8Rd/5vu0Hf1SkkN5e+KxNkAAAAIJEggxR1R9NCYCDjW+igoyW4DmNvVNajD//uUZA4AAwsURiZ/AABQoojozrwADJhRFLn3gAEgC+f3NPACrREcwSkCGMAPABzCHAOARACBgaQDgFAA+DFZ4w0h6lmGkAyKPxMHHe2HU6mdrmZ0w6CVwt5P3s5/nbw0HUjcGdilan79YAB0vrNAAB7pUR92rRz/XJpcDx79bZ1MVBjcN5EFxgCCgOJIGgaDgWXG19SQUAtpLD0seAjCuFjjltL42EKHEbb9D1Vq2HmqxFKYcOFoocfpAAoAAANV+R+TR1RYczCJpyMtrD7DT+aBUxcwYyAQTMYACAIGFTg8o8A2GBOgMwOAD3Cfp/GctXYEIGLmL5fD/Ow7iqPIiTCUpQHICv3SFbXcxzwE2uLvbWOrR///7Kf8vsiv3aQAi7bbdrbLKAAAAAAADd1AFQMvFAgE3HA0JVAWxZIjhFfSRVy8ieLIoekQpQyiVFykLopiVhgzoeTp2no8/3Pfd//nXi4oLE0JAAMZEB5TNHDmQ2HsmEMbQLrzD5QaIwGs//uUZAyN0vYcRBd94ABPAwii7zwASpxhEE/phUFEDiHF7aC4J3MEoAvTBAgF0wE8B8DgJwwAEAgAQB8Xqr00voU0AOgp65iD1qZCk+5IQ5K+WRmj7xavzfP/peud63NjwA64+5//6///oLgADEVEtMvUdY0nhxTBGI6MLUOkwcRUDCfBuMG0CoSCSMAcAIMAJUiuiX4v68svJGIAfRMVSXg4Vy3uTeyrt7Zs3jGtfH+6bhafw0uOHjykMGrQ7DLUb3QxHIYGMpHNpjAbALwwkcAhOXePPRMkWN0HboPGnRqxN3loRBzVlLpNxIEMGQdJAtbPDeBb/baLLuWpPxawfKngmTd9f////////99bDJUKdMku78ygiETFyINMMcDQynA/zOAw80lHhMWHgMbA4ZVxEJe8iwagjgr0jczDclhTXn+hyklILQA0juJ7S74ta/h1nMHtv///+r0I97dKKUlTDnC1A0XEFFNM8BoTFYDSM5FCTQFeNIBEFVJnZjoC//uUZBCAgq8VRJP8SOBSwohge48WCXhDJ69lgqE9DiKZ7aRwGDAGW0mJ2HaR737XU772MCUNhxQTIBUdYK01lMV0n4UDQLtwszpz5T0fmr/R/1Nb6P7v99ZhwMRG0Yz2a/IgpitrAGrjyeTrZnMgmyRQYgFYNHLgFATBzsRyFvciGC8Hwu2sZJ+ptYui2JnOQ8VBW8t9+V14vA4oo5WcPWJ+Mt9Hd/o/v7prQAAQ3IHdpG5RQdYw7rIzCSFXMVM7p9z8JICUimtpJhePe9i50lMnzaY/PL061OaXaIUvAMpANQMmTYcIO/b91+n6V9v9H1xf8Vv/qSg6pZMXMXg0fIzjHRDmMZBCMDNZ+poa2gmcgQGO06E10HWnchhyY9DbLKslXOBcjGE4rFzgmO3tMyz1/nr5u/+ePH1+yzs93+z+jvYj/699MYtENjepjRk27UGvMcmSmQH0zSYSOTl8y8cjD5MVAVUKC2t3YYdGIMQmlD2vuOytoDk1oW88SjXw//uUZByNws0RQwP8yJBLopiCe4wWCkhFDC/zQkE5CaIJ7jBYd9UF0iZljFKLFg6ZkB66k2atP+tfpt/r5LZ9lfa26K9RISqYUAE5j3v+GtcEQYYqtBqhBnJp4NAAIQ4YPEai71gwWNtrQkXDUFz5MaKi9TqBHC2+stldznXWmrp/3bO65/7/kU79/2f+l//6yGHvhSBmnHbAZEKQFGAaIShzOWGbiiZTdRnI+mGhAbKCkRhgqa2U4wiYWw7rOH7ljvMttXJVIqr7v/SVK5UffOCKhYodK/i/6n93f/2/+6z3/vWVVjBxPBMpu9E0TBlTFzRFM0HY6ApRIVmFwK47hFvYqgsstpMEszPRWIpdiaWsZGnpWLLQWWufHkVNeJV2t71/6FUVbv9t9f+rd2bX66k9AAMYpME9D8nD+aEqMsma8xSALgYBaA3hgk5niBkASa77Oddo4AmYJAxqwMwabQZ9jcX/JVsmg442DZhaxav///////awBgwqrmqxgea4//uUZCeN8ikUxRPaSTBKwohxe4wWCJRDEE/tIsESCGHB7TBYoTJljGMHb1ke9VRlMPGTQMVAY+yxkf0mFxsKwBwAw+8fTVBFSRGuOy0cpqLJy9JgeD5ITX0bMUTU//d/7P//b9BSGEmFjpkHte2ZHoKnmDvGHAUqTS7oy4jMCKCIeRuLYCQcSxLxOqA4mtZxVASNwWeTSe8oHEATwdIiayz///////f//oMM8Qs0SL2DXHFSMvEIU+gEe2GRTlaIxxkDIQEDjwj9roPDsdxhHYKTiiutXL8lbaDSm3sj1NW5Pup/7/X6vuZ0//7/6aAMGtGpzSEGOU1ZkNZMOzLwTnJBMBpg2Zc6Qs1JkxI0wwhl7l0+NR/n+erOYrvTTxeavdqWb17C8O7HMQu32e5ADD+BrNc3bA0yBsjNOIvPOGj8Ik3IkGi0v+gYLGawpRhXoaGNZmWjpMPRMNi7Rzj5HzEeqhG2j0b/93+zc7q/6v+z/14MIpFrDIDLewzLsZOM//uUZEkNwgARQ4v80IBEojhxe2wWCKRDDi/xgsETCCIJ7ZhYgYJyjtMSBKtM8mYABMFFoIAKQjNQZXw4WD2cF/bvL1xysY7bKnTbWw9s2nP9v0erV/+7////V6hRWExAheTYDhZNdgLUyuRQDCAcNrhwtQ+KgAQA7kLswuD1CEzxIwLJRjMXOgoZeQVoK7xf0/yX/3WR2+/Sp61f/u6v/oUoVAMKKFxjUMXVY0+oOMMlzJyzBigEoOA+jhPjBnzLLjDGkV3UUuAigBRIqVciQGUSBZORtcVfa2Fwqgm92judHO1tJfzlv/3fr//9/rGAADCWBYMkzIczCRmzIiEdEUM2c4zKsHCB0KoA3z2jkZqmyUbiY0wdnmedLIHQQ0bgY2cIf9v/p///RUyzGFrA0plRsA+ZaAIfmKbDWhj1KYRRGUF5MHxhrAkAr+Et2lxGU8SbduXrOjCARIGjjhTpvsp9a9P/Ff9sV/bV3/9X/SQxXTqjZEaeNuEOAxdxujeC//uUZG+M0mwQQ5P6SSA7IiiCe0kWCLg/Ek/tIsDwCGHF7SRYjeaxKWZIIm4YIOg85KHONTQnRWfIfAS1sDckCUG+g6Zo//3f//////lf/Wp/zBqQcIzt5zFM+PDtTGFxKs8IUCKjHszcDjNnTHjRIJIg2mlkiRiwMvgii7FBm7s2xzw5a2pmRoPW3/+n9Ov7fouV6PVm8AMFo2ow+epjHoGcMDwnM1EVMolhEAkxwTAIKEVYpnU4F8DIW860qQROb00w3rHMHoV4t2f9v/zPp9Pd/3fSu4WvMJ9DgDJOFkQyX0LyMW8B1jvVwxgzNDHDHgFV76v00kNMIDAvIjFDNyMILsWbF/f/s/7Prs7f1fFH//Qmf1b1EMOIpg1+UHzd6B9MF0rQ2iKNMjQFAiUQCARXKok+h5WYlZXEKjdjolmzy0kKDH+pDzptn/VytH/9/+7o/i/RoVhj39kAAK3HFGmzAkAIoxyFKWMgyCVjE5wdswLYAwaCauGspkEBAMvf//uUZJgI0jINxAv6MLA+ohhxe2kWCEg/Fs/sYsEPCGGF7aRYx++7sphEOClHJG3zBSec38d9ej1qT/Tv//03/7HIs0ABWBVTAfDFMkzKE2zhTV/hMGicMyYcETEgpSrmVyU7eTkx8DVyKTyENBk5OTeEmrLk32bW9ftx37GfZ1e+9Wz/Of6NQZhaYwl4UxMupZezNFA2cxUIH2PMZTHxIzgjMpDBIJegvS41ybKqRgWr0gsBJjy39nX7/+n5J398c977Pr/d14CrRUylUkQMT0XM21gqDZ7AkMbIgo0Ks1XYv8PHRZeXnhh24TY7KYWrSExNG91YNCgavczR9aKejPaKv1+ni+n76///+n2KACs0qmAril5lZ0K2ZfaIWmMPg0Bkc+CGQxdPMaIASIl2wcAMPCo1ybvLLoxGHKkPieWen3ADT6dhayGlrRYzVar5W//76Pv99CNVaEGDiAKYvH6hovBRqG6ZhIPhoELZg4DAqBqgSRIDjA/7WWA5yDJR//uUZMCIgiURRlP4MSBDwgiGe4YICIw9Es/sYsEGiCHJ7RhgVfeJiCRoAAsSapm79Vtz/6no0GBsh2fs/2Vfu+zahIzDYw/A0Zh+0NRpEPjKXg688+0DahRM9jAzEEzBoLYGj0tUTRkRoTSCoK9dKlIMCWPUJT5EO2IRTXsmjZ/nrdCdaqHti1nlrdUfdehjRn7ulx/YAJhvEznAylAcgQFxgmi0Gnigc9OJioOKqCADKmiMVZciAlAkEpjAsgeLQiAIS6tUhp4+qjTvcqHC7DTz+hDJdP1pqZHTElsMrYx41jdzF0NakmxetCteYjAch2I0GHosNB2HqjLvQ9AwYAF2AIICczEYu6a50ZAYHP1qBtGzioWbaLdVysmhcFAsZvtL0O7nNgUUoHmd8WLVvnEOWmlqA9pHn17bvU1XTZt0X2P6VGDQBg5hfqTyenSqYqquY2FiZ8jsYHB+YBAWpqqswVm6CieHMaciODpUehyUlCAERadv0g+vmEKU/6ar//uUZOcIwlsOxDP7MLBFIchhe6kGCmw7DC/xIsFfiKEF7hhY0uux5GhVTBgslPsTSvRb3NST/0btaDDoDRNc97M2ShSTG2ENOgTTrxs1RiACmRYZsqCY4jGJCYcdmCASibQnEuQU3qm60GYt8wQJysWjQSxwLpaUPzV9YnXIl7FYo1jcDr9L+zBGIwOQsKLb/X/yMgv//tGRWP+dkw9RCDlKm9OlEPMxWhzjO6LPkBNSowsCAUD0tUeG5HvzqpOHUYsSFj5b3122SHHCqjhgw1cKhilioRk3XlwkAUmVFbQM5xxrioGtQNL3meaFB8JUmDjzzT9cVGOdUhbWuvQ81QApLJbbbI0gYrEsZ/VWZDH0Y2DUY7iyZ7K4awKSaHqecI3MbE0sbfNkbPHSahvkZSFWZTBsRMxfhLowUwECiY4gHNkZ0CSa/HCsaaYiALqN9ZDUn0w1mNXMXBMVgAqnEMd7ZttmsgByxt03UApGDEha8NROpU2WgQipoBkjCLJD//uUZPcO8rcNwgP6SSBQgghAf6YIDGRrHi9tg9mMiGCB7iRYgCKJCF+UJSKpdIHCgIoACg0AkDGR09Fzs0YMwFnjrtqzl6n/kGU9DMNRGZ1Wu0UKfl9oJicCSKMyq7rtmlmpuUS+ipquF6/qrWu4LIqv///kyIksVAFlgAldNttgAAAwIERkRmynComJPAgDY+UPX/qQAEhwAekhD9GZ7RyOG+KgApJKyukAMowBOcEcPVUxA1WmRhPHXjLmJIrmAsHmgAImCIOGNY5mJwSDoCGFwGsnTvjFdtL8dACiAuKwALDAJgEURA/1fHzhmbX9c4jYbdO4z/e3uWaTP1vWMyRYti7gnQE///oIKxp20HTFEYwJhkhbGngaZrA5ggEIDQUBFqF7FutDgmer6gKEYHgMOKtTJ0EACow9an11MMXLyzTa2iYwxLxfOLzYoTFmKcQ2LltVM+WF/DEtAxcyg4XUMZ5B3DIWy1UxfgCWMDnAGZwwIcDEAQFYYHsAxmAF//uUZO4AFro5zeu7yzYpIgodaSYZzLB7M5XXgDjOh6TCuJABgAgVqew+x0BBkTmEWieqJbwYgDs1cJwBYKBXdf2+3jnzSS7+RB4nskVNbfWecie7R0lNS2rFH3tXkCUl27bvTWE3aGmCwKAEIhx7mOsS5Ao1zADAHV7HKsHtuTC4/PbUMaF7nOEX//mv/+8AJYAAgxqnUTE3SAMmVuoyFT4zOxAxM6oi0yshSjA5AnMHcIswvwZjJpBTMAQIcwJQEFXmBcAY4I0D66jwjxhGZEQzKNzCUKwK3BEJayYZAR3o219vnkT1jrJZTcvS+W36+VLK7eWcqj1XtTuOs8Ma2HPsYX8q5Fm59KW6r9NK2fJ/y2h136tX0gBIGRWq+RheKdYcRVfuGG7wOhzGt9YaHkS1mUijaRiPoMeYSYIsGB7BBxngxCmYOMBWGBAgFioDAcQDUwAgAAMAzALjAAwEtBMBGikg0RDPGbiGFIlvqImAQg/4RAHQpqMYp5lrrrP+//uUZM8ABWgkw4Z/IACI5Iilz2AAFsyVDjn8gAIGDiKjPYAAxeWz7hPvNSKmfJ+sO5xKWXbdim1Ws51It23S47uVL26qgswWNSr3AQCLEmWOlyocPOJbGJWZCIscba4dSSfMuIJhEJJTpeKjDv////9KQAAEIBQAJDdUFNMgJQo2zzVjL8P0BMTZkWIymTmRMYEQJBiVBAmFMBOY1wW5iVAoDQJqw5goADN+SgSoNEwi1awxrUXEMagtUCRGgt8qhFFvOxPWuUsTcN9qOjlz2X8L/YKllnfaWjxpXCAiIAkI1kksQt/obuup7eO1Xf1XXs/RrQU21YzC+P7NZPds8Ce5zWMNJM94V07ThyDEwBHMJgUIyiwjgACqYVAVLIhEAi/aqi+oYjByvPxzFg9mB2JixAVu0luKtM1f1bTlrX613Y87+6dq7QcYeEEdv/9/s+5pNBDWv//6QAAVSQnLQAAYbXnZfBkHeb6NmkS50x8WmCwSDmJBt3muvI0x570n//uUZEYMEyofxx95gABEgxlt7ZgBDkRrDg/7QsEMjuRZ3Yi7qEzgM9RIwFLIH7Sb7UpbGYfkcktHnKsNCEDBHExIwr4MwMemDTTSFCTsylceHMdbC4w78aTD2H4MMlQA0SRAjALBEMdsSowOAJjqvzRCQMRQ7M4UCS1MADBRijC4RtwxoqxdCXyq0NuDK6Whpaem+htTOqHkcoo1Tfj+FOaMjSCyQxE8nVqTX2/////9P+zeAgWjCJFzPmcjP7kDE8dgAOhz0LYXGwCqBNGOg5MFudF4FhcLiMdjEpj0btxupqNVv6MnsHUtTI4zdd1q4QiGPyT//TBOFBwxx3wwN7H/DjL4DEAxkMF1OVhF1DFsQHUwXIgwMLXBcgIBnmBFA+pgR4EuFHBzBo0YMIDAAFGVWIHMkBywylwONq/AoVDuoyLAkRkVJ6VzsYqk7w2JVm0bSZZ20q+cdhOFoUU/kmGzLSKXNxAQjiYRUmYhO0B2DIta5i67ry8WSU1PcWeh//uUZESP1IshQgP6StBjQziBe0lKD0SBCg/0x8EVC6MF7aCziniqZRLpGkDAOYK5w5natimdm9eYvQzJgZGQGkufKYUgDhhZlMGRqG8BgTDosAFGMERCgFpD5QBLvZylElakWhIZy5Cd8aTmfQQXcxFIu2uw9NOjlPjfc0PjSI00BUq2f0f0f7v/3p+Kf09pghCJMaf2SyGUhdCJiLRZ2YLEXTmV2G7hhZgHqYIuPHHyiRGQB/GUK+GVo3FY4GHgTlp0n2WtEZqWUYGIQDLxLcAQCrAMhLcpytJdGal1PUqhmkimjJFyBDSMYjOd/bZjumApIyHmquJbFbn6tzH6/p6fx3c9vvbf1bzGI4GWYGiQJmuEymO2BUYBIVpnTgnG/iZkHAZCNBBYoAgBRCae2kP247aj0xOypkznoFz2EQw1x7lWWtJPZAyIIq3eC6QpzB1BLIwvWd7OOSWWDMSSEoxKALyNZMH7TDMANgwh8LmMWWADzAaATw1vcD4DPvmg//uUZBiIk7kYQwv6MlBFovkad0knTMhfG1X0gAD2jCRquoAEFn33ZVDSwDNV8xyAUU1LWcQHASVimYoQcMGBR5AwNVpCyEqTEix0qYBy0wksnTf2LbZvdV3xGt6lntyQwEN+5+1/mn9I5YAFopOIAAGJ5pGbTdG5fMGJgKA1EzF5BiAQYKw0Qw48y4xdKVbJn8LLiiKBUQsE2jUmCdMzsYw/Y/vJf0x9AhfqnefrTqCABbTjbMDRBMDIuB+U0lUq8MVYBWjBYA4Axh0LxMGHBCzBAwREwYIACMAzAFTAcwCECgA4YARwE6deYfhAfLEgoSKgqyTGW03XD3Jr5qlWhmiJXnKzwb1q/0Xbe+6U/6naP/U5H+okC5AYih+ZgwYZd6GLP6KhuEYaCggAwVmCQYBAZKKqqT00/F/C+JAluhovRJK3Rt1FSShsK3qWo8yo5FrBEgKMgEARKEDAbCPkzRgUoMrbmkzDvSAYzRgbrNBHFiTE+QsswWAB+MMwCwjA//uUZBYAA64YRbZ94ABNIqm/zbwADdhrFrn3gAEDiOh3NPACMwMUxFAIMMD7AdjAKgAliRMAZO6YA+ADNoYiEC7GSZyAFKM98T0vaMQMGt8brZ/PnNc4v5loC5ifIUN+33WN6Cn9ci1+aV9rNSQAAAHeHiIiYaOMAAAAAAADK+U78fOEaDu30zP2NJMDFg9dhfZL0WYk1iZEqGAgdcwUMds/WUghMSxBpMrhMiTloW4nj7blilLVksYZ0gQEAAAGPxibhnPJIuaAEarGbVinhlbMiYYXWVDGCRhipgbgBEYSGD7GAggTJgDIBYYCWBWGAkAALjKTcUoAF5YLST8nx+F8Vp7qrZoIOhKSKdQLZgRJo0bHmxn51j7hBO613//////3//+d7v//WAGjLbprrQLAAAAAAAAZSQSrDD+iayZNGcxGYoCz0OmMOL7wlKGkL0w8vSH3zOBIhAZOoz9mUk+iMtjbGwMO+HKqYgwUUG7NBvYvTYYSHc01YVFMJmFO//uUZAqMEz4Uw4994AA74umK7hgBy2xREDX2AADzCuOmumAFDFqhw8wfQCzMAwDVTBcAE8wDYBgMBWAEBoBTMAtAD0s0JEurWTmFOWRcbFuN+xXrK09ccUrNeaNWaoOi8Ug4PU4dJLTX3N/6LF+3/q+27/4W1awAbvcjAAAC4xMrCA1vgyqMTFglMXDotEnOUAKKsYSBU1jOst/JRLBwIRSscyzju3glAOPtVaWsOpOd39wMYZ8FyGYdI8JlcUFqZl6UOmK9grZlyIDUYJ+ANmBMBKpgDwBOYCAABoFGAMACxgEQAAlE79NdzNR1EVcEJIJ1VIqJRk07SaxX/oxE469iPs3/X/T/8l/+n//pAAEJ8DRw2DFP4j5TQDeMczCYcDiwyzAAIB4GUJacpggEaV6Bz6U9nuNiCsE5ZqQSkSWp3eypITy/RkfKOgAIDIoKlA03sUaMq1UXjAtjAw09ySqM0eEdDCiQNkwB4DjMHVB+jBLwFgw64KRMCGAIUynF//uUZBqAA80axA594ABL4jjVzyQACpQ7Ft3mAAD7i2RruoAFd+BC3rjHMSNEIGMXQsCGOAk5O4rOUrnD14V97riuv/bHxeYGB4rNsRGXCy5Jx5FyOhHdt3D5h72FKpJvoG+tzE/s//9IAlwAABm5rVGLQOUZOyJRnHEUmX+CmaqwpJhcAxFAHxguhMmBIACYpYM4QBBGtJ00rM6cUAIDZIDuggIquMCQ3vcEQzu////////+kBi3F8YvbCxxA/VHPzwKbICzpmCA6mskIeYLAJJgzA0mF6BgYGwCJgPAJFyhIA5l03FO1VoWmRMjLNL+WZLwmI1Tcy4ee2vZ7v/3f8Xbf9f6eilPxgAF1O0AAAGEQbGJskmF/gmGhAmJxmmtIyGFoAAUIDAcBmApquq5MFRqm/iwHoTixgteo6+6Ypy6J0LQywiL/WQAAwjgD6MoXVGza/kU8xc8DNMGaCfjEmQ1szJkNDMj6T4MXQE1mFCZgIW6UvDrP4qJpACRIGEJ//uUZBwNkncUxBP7STBD4liBe0kmClQ9DA/7ggDai6Nl3BidGmTEv8arZNMTWFA07Rt/////7P///9dAGBmNcAmJTOGiGMwkQwwjiSTQmJaNpFNiyFnauguJVoTpVfHoYktHgiRkQgcPFDzeytaTUQVbcBV2f///9/f/////oMCJB1zLUm7k03uhQMFfFGjGROhN/U6w562TPS2OKgsGnUyUFhCGQ4OShWiX2d0L6Q+z9zcez0By6bpxUKiUFjA86526X7+n//+n7/69n07PH7WJJ2BjCUhnN8R/CtRsMI5guSRqAiJ+Ia0FiCAxBM1t04VDPzQQOFhexWdz2SOc63q/rXVlzCZnJQEDDSw8gzOhEsNbBgGzKIhEcwZ8H7MZcC3Dah4zSMPRLAIImOnymRAADwK8i0IxBlNMiOLGVqWRR/2WoVvviqETDXLmm3f/6f/b/9vs/1f/6+ABwSpk9jbGyBGqZAIIYWDUM78M885ACFT4JzCERg2XrQnPtgjm//uUZDwM0pAVw5P7STA7QeiBe0wmCixJEk/tJMDqh+IF7SSdzUh86Pq0wTLL8+sEQYCQEcM7Nnn7KmVUwJADsMWtVWDU4h+YyUcFeMHYBqDEKAik+QwNzKDHS4woYMJHQ4AMJBGaWJVSbKGAykZKXkPLL8mBOKwuET7hf32qxhCju+7v/0f/9ux3QwwfhszCnZ1M2aPQxGQKjBkF5MLYfI8VcCijTPx4qZIUAQaCj5O5HZKGxxwgFZLeNOgyMB1dwpNoXvyqJAADBzhIUymM1vMk1Y7jIfANEwUsDyMW5B7z9S0zDEOlOzDwkZCFH1pq0RW3WmIRQpqRtlNE7ZfzzzezQghgtb2Ut2f7f02mP9X1//pAAiZuAAAAxBIM2geM/NjMz4EIxCKj2BbQoBIEQ+eMuquQBnAub3JiFk83J8zjy8NlNW/MlPW9qghuaeQoFDmHJKB5hzK+mYOmCgGAugpJhP4PQeMeYbAElkYDMhFMUmamy8/RgQYXpI9y+pZM//uUZF0AkmoUQ5P7STA3ovjad4YXScg/FM/oxMDJBqKZ7ZhlWEYFIXktm706k+Y30f729qUlj7Xd/9P/9VCAMCsAUwxEtDMqNnMS8PcybOP32TIwYtEGCA8UFrkLI828sEZytBQOKB+WxmaShK1oVSyMFPAfTLpjaAyNpqsMRvDNzBSAh8xqAHsP2CTc1AykuBxIYcUl6goAW5QT1OYswCN11t1kl2sOSipJtGgglhPc1GmtM7Vu7ea/szx82xzOV+gan3LAGQAkFIZSqCRiEOrGCQJADAbzGFCIOk8ZoNIZFkDOsFgKFWFxzjzgZkSTQDJiTCXxfvq098wtKrJgXoWcYQSrFH13H8aVxgxhyjuGM0R6OMDESw4EjkBRDME+43aw7/7yw7Q9zqAALyAAj0yaD79ifWn9/+b+j/7sm7XT//vgSBgaBEmP6rSY1bSpiOB1mg55jVsYIEhcYCIFPkBAEMugtaZA35brLSMSw0QAX/MEOs1fSjCbAgAz1Env//uUZIgIkqoOwwv7STA1gdiVeyYnSRA3Es/7QEDQhuJZ7ZhhNqQlVzE1R8cwSIGEMffApTrzIx2ZQ6BgQYWUlgHFAOll4CzfQSSYYUamTBAKmz54qPcU+7b1c2Y2kNCshtTn2pVkna+lAvqyObSt25jfSBFgDAaAMMUJCM86LY2lLAx2BE3yB8MIBBGJCA4DBFnNLceWG4scGU4MSwpg/jnak8A2C13NUYHsEemAXImZjDSTgYayJHGBJA/4VCCDGYwwsZEbeBQxJJesCVJztYSSUSSaUQIFHvE0UGWV+wj0buv1/939v0el/9HdcYJIsRj6K6GqGzUYuQOps+EbD8rkMVjy4K3QUCOo1iD+h/qI4EBQJQYEzx7HhaPATET2v////t////+uQAwqUG+M2+DlTdnzUMyZoXTMGxA2jGFgWM3vjOtPNFNNJNlqScTr39c8DMK0mnBRqNf5n79exBn7+uhfu/31fq7Ebv7e1e//Z0jQQMwkhgQiSGMShIYl//uUZLIIsrUOQoP7MTAyQdiVe6ILSRg5FS/oxMDsh6IB7YxiTQYsIiBTKfnIwsJzACPByHi6ulyhVHcNv4hHHCQkLXCCtztvs2/7f//732en//7QAA7pG5G0QAWeKkANMJsxGBAIFk90H2Js7ZYjQtM0K82a81606sE1ZNBECQJgwKV9M7DuUsbl9WG4vADOJBK3bh+cwsTbK1Ttu6ADEfNnb7CxyaTe6+zCxyd9YcHnX/Dgw5e/PSJYzoYyHnwH/+fAGEoD2ZSqVJtFJkA5Ns0g+T4lGMmDEAmkOi4hBphQHp1ORPcmVHPEIFYRRQPqmXrQjevqt///dZ/Gf////9dlAAc2sttsiQCKBlxxDqUFC5gxw1sRcM0C4ywkOLO5MXEMRsVAMdI01jTeLWssQlP1Kpt/YdeJQF8mBFvm7oOsRdVpTXZa+rWWuxBOZK4HwHNntDonbVay7VlIIz0ZJj4yVZCTaXOnoTFS17LpytgXa7Xvlk96gKjS/Nl/9dBy//uUZNmAQlQWQwv5MTA8AViWe4kWDcyfP65pifj/B2HF7gxgbNhed/nBQEAkAAwZwnTGcSxNMMrAx1ggjKpwPnGc0YMkiTMoGRXSugiHaJYcD4lq7kgRElW9vL7xV7HEC4fD7si2kdAUMBHDwDBqVAUwrljnMF0CTDBpQdgzeUElMHXAMDA3hfc6wvTNadO0WExEEwcMjLoEFgGzVGVR1XzDUEiM6esoQgZURi6rpPjnnsuAqMNTtOW4xMxC0hW6j+73f/1///t/6rY3LQHgMAoL6tFpwF+8MXQut6AN4xQvSRE1ItnRzC81jRmsCCTtQPCfaJ6nTMk3Sh0KoyOlkDv0TjOC7sI4p0szHZXVMPx5IyjBODHWK1MX8GcIAjMFMYAwagcAwJgwEQGmXQMy5WJYGUuW6AyMFHatYlQpWS2FPiLq3g2hubTqNi9oMynxIHiFNX3ed6qO/96VP0aWRmjTenmBsXrFEAAApqNuNAAACEhi7wZZAIAiUlMrJAwg//uUZO0AlCMvUutZYn48IciWe4wYDHBpEk/xJwC+i6f1l5jmRwQrYq9TrNBg+JS9r7TyWjTIiurPEh6hw0Ew1HzGjrpl12766KmOCM9q3fMgH5ENVeTTTZB2XU6qAubM2cPITiVEpAxbALgMHNEKTOmwjkwbsRgMURE5TDiguMwGMAaFgAMwEkBVBQBGYCUAQmAigBYGWIKHA44C4MCVA+URiDgCZibQueFqDhiRC0jJFQsi2FEgY5xsTguYqLaREuqNkD5knWYOo+i6CKloJqXRroIIJMmksxTXTR0mQT16nSpUmToLTd1a216LLVSpKp1rM1tEzlgjCTIQLBkMEywhtEZECCgSef//7v/+xhrzpimqE0CZbxfZpcBiGha5eYaDyBicg3FYk5mfiSmCABcYfgR5g2AxGBkAgPAPGCWA+KgMEwDoAAZNAvQA8FELYeBWHhUhosRPwqCmKZDILnBet7BMwk5ZaatGxT4hTAYLtOh2OIhppdgjYEImUKFT//uUZPEAA3caRJV54ABDgwmdrbABl9kbCBn5gAJCDaGDPPAACHTChZA1rqCQwgXWdrcm1GWAdKC60XOxwhVGVJ//6X0ZAAMDDBozGVR8Yz7pcVMbzDpTAzg54xwkYHMFUApzBEAVAwwEFEMDQA9TATgLIwB8AmMA/ADlSiQAE366l+k4HKLGqqp1b6hepyMyPbPabp9z1kn1nVK+3k5swz/////////QAAA043JAAAE/TIazgDjChAqiMwPL4qCoxQ7lep2vVYjbwEEwlNjWNtNHVMDZuv9bospaLxnmFggLJg4gruYrec4mVXMjhgyoiUYBCGTmRdh5xnWMph1KBvaARg0OohK0umhq3BWNThtllRJsjPrdLGY4QQIhA2zqe2z9q5vI7Z7f9z///Xu//jb/uZ/9KRBxAwdB01QMI5bgwx0AYxbJY5bIkx0BMKSB+GBgSm45MEPK/b0O3TPHNTS4RqVchzTf73bedgdx6xVp5WgqIAwO8MFMekQgzaB0//uUZJ4Mkx8aRJd94AA3Avm97RgBiuRdEk/0xYD3i6Od3ZisQ8xngKNMETDlTKDA8YwQAB5MAwDQDo0gwwHNWjgiDDEBN9CUonDL0ZJmxVAoGRGTjTIjDQMpqbW37jeI5HhWzp2f/6/9Vn//q///6QAOuvwAFXCC4MM4QUElUzMnWAgow4g55yYZnom5bEorHXhi0xbjNSmnMVuiBtGdZcJh8LyleSWRAgOJtNMwOR9DIKj+OECbUyiQrTDGH0M8Yp8yQmDKOmN4FgECIBIMcAQsAkwmTw8zl0XtkdKHEjgdwJ7qXc2zGiBJ4PCjffdb/TrT/3fdkv/s+r9ZYGIRRgaRjpC2DMEIDGgazdQIAkYCbi0YyMCEuJxz4w40RmwIcCQhhN4SIORL81VCvRJVJAADBBA6Ax69M2MF+h8DCYwbIwMMGMMm/BXzQENTCVtDnATQQLxZowCBduoCA5Dm1hLJrqYMWh11BlcgIFw0RjhmLWdyuq2gUeetc1GJn/tl//uUZLQIksAWQ4v7ScA4AulpbyMtinRbGU9wRUDKCCNJ3QylvR+c06//Z20AAAAxqKAAABGgQdU8+XTQwJAJGNMJVdgAoeQKBEobBQKVYTIh0lAGrGr1LyV1g4HkYoIgjQTFFIDBFglwxlNFFMlTclzHfgfEwSkH5MEyExjB8lTDUJTmgGjAUbigWAUCYCAhfqCFdDDlgW1jsPEl0YXZBWkaSOW5WV8puhJh9m5SbdIx+dYxGv+rsxlE4QR2+r/T6b/vwuMaQTmkb48lioYaSBqkck0AVwuSUUrJWmQNAsDsN3D6QGLMExJRA5kZekJAkZfASWkFJRVTALQBsxn5RvNQqA0zLcwUYwlYA2MgaCpRbCDHUzzn4EzBolDAEPiqBANAZHWwrl0Wf217smEqa6JRYygVBFRXN/9UmSzbOMvShhWdWr3o72LJoz1l23GP6qqX/StS1tcCsSPlGChlckkgBACcYRixQ4/kYYQFxgrhFmViGQYYdmPQJ2B+FwAK//uUZNaMkssWw5P9SVA14gkNcyklC9hXDk/1JUDSC2VlvAzmEoQChQAaewWHm1p2fM0mAsilpV6tTmOYMN1hoNm7v1//9X///////262YRqBkGU1IThrpLuwZKEBjG6NkHXs5mXYWGNDmmzAigICgMLIKEEiAJkM5GYgu1NZiUGGFIwRwDlyY3cenv+Y0ukbdL31Iy9FC0XovpawT2OOVf6nV0SqnI5e+6QU1ayVgpUhTAGAADcY1Ynxl7rKgYugy2qzuTyGiyZAQJikHLeMHApHRWxMFpdG5cWrSZ7SoQCDiIpAxcLzclkMZu3////7//+3/0IwVUMYMZzcRDBvMpEwrcNDMEdBHjI1wYg0HBwwo1k1+HcFAkIRMMBQXVkQCggBFxp5MtUggnbUSoNEqYeAMSHk2U/imeWScSBJUmbQJH2qYHOEmDlULtNqi1J68NhdRwy9CZposmxgvGtYAb1HArOwkKiLNsf0tWAADAADtMQlO01ojkzIDBtOwyD0//uUZPMM01gWQxP9SVBHoiiBe2YqDJhZDC/1I4ENCyIJ7gxwMszgUIHcy8ULLmLnpMYL3YVHocnYFhdaWEosmxMzEv9KqaOZjhBD/////////pf/3fGIoQuaianp6qVBGbyBSb0qJ86SGWBWYhdYBDIkHjCAPRCTphFwWYAtG8zWvu7T6VGpAoGlLBQgNUv3fpuumX6d3tS5bLSxn/6e0WeO/d9PAoISY4A5pnUwZGJoIMYPoRRkBgLGOicy53xgoMyikc1AGvOKVBgyEjZA5bYrzWhSzaXFrwABOL///6f////+qkZGBbcxy59BOHPUKjJqBgowfUBTMVACvTEwTTGiAzVUDwAFpjYCAjA8dANezHAFJFpTFZRWHZ8yydNjlfIuZ2Z7ctAkWwtPODodBBAVaYTpfnkuDLORaczwDJuYsmR3K5ipYjDryzFKSQgQYkcgks2wCCxtBogDB7DUMT5QI1WZczJUFZMhsE7yijBBFMHDYHSpHIHBcMAqTKFC//uUZPOI07YVQYP9SVBCwtiCe2McCkRDFs9xIwD/iKIF7KSYesIfWHnShp9xiBjFccdsKIr5oXIvY06RtN5Hb/os/d7N1d6O/20U//91ffGDihCRkwAu0YBzG4mBFDBJjO/x8inJgyFhhcKpoeBYWC0wzCZsjVKRXK5Yuz1+mGQMNuKtEc5RwnFxIlRdotDC3xyYuhDmOJSJ1q76zazCZX9rZcwKnmIKKASJhDYkUt2+5tiBTMbIYMQFxk8moBnMBlXhOGEkDKZaAPxSGmLmgkPCwCFwNsDqLOjncH1gWh1FNq+dTtuYt4/fKU/1VN2J/2+n2fTVz27/sv/9dQEMCKCRjEcWnQzyaG0MdFFZDh2dOZ9QqBcxufANowQBTDQpFACkxIGVuUyeHRwJBRs5MVKRcpNQ1Ppf3drC5uv7cc1Gx/X2Gsdo/QRRY8XtV7px42nSuZWZMEYDExMzhzLJaQGh3DFpKM6VEoGgiBw8Z0IS7ioIk5saVNODKjo5aUfd//uUZPuMw6oWwYv9YTBQIjhhe4McDOg7Ci/0w4EbCyHF7ZigTTvpEBSWIMijvOe1f2a6mY/m9lPRZz1znPjXJBfU/UuxdAqyyYcwThwToxnufCGb7x1phwggmXsBGVmAJUPqhYiOmBISzlPWdBMU2iH1ZTrO1QDBIFFDohPklqTuOs2MFnOAGtZYQNPRXB8znTSu8XSg4bqaGFQvrUlE4zuh2iFJykgDBrB2MhJOEzRZlx4okwaQGjHJCUOWcMXAMQaQzLTpPwWp5wRPBAXDiYOvdf+3GYSe9i4fBhDBtmrVF60I96u2n+X0acZ2/3K/ivU+pKowWYIPMFxgxzX4IgEyccPqNwI2MdoHNGNjUag2U6DDQx0bKAUMB0gn8cOGnjmIXL87uf1d7yuEwVQoWDJIEu0mbaKixBicWCugw1tyLeNU4Wj7lmpwrYMZeWPDnNmh9Ax4uo/QxAzRiQYCBpJlCo6Gl3WHqHfG1okmvG54lUYS5gjBKJihImEwTNX///uUZO+MwrQQQov8MNBOoiiCe4kYDAQ5DE9pJME3h2GF7TCYzys2KWao3pYGQIXhISDDYHmWdF57Rx+23Szyv772/f/dq6a9bndZgiYAgZOkGAGWDwsRgggN2ZYi6cFicdleYj0cMWYc6NEFD16MHnM8IxTQ/n9YkGSbgeAxo2q13as1qemSbgYzoT+jTVp71PEdOj1KWqR5yLz/WupiqsqQg7GI6h4Z6KVRjqBIGNqh9lqYCWKHCMVULGi9U5CDYU5g0hQqbef1HfbB9o/kbB9Lmyenn9cts9zl2htWrR3bNq+r3M0vfXVRCwwicQqMP8Y1DUn2IsyEAJkPloYw6mjavjGPiOUj6FyiFrIHedWQRaF2qeVflrHdM00KzRuePPpVgGi5QgJC7ka7pyJp0uMGKeNcg88IrQTHMaaHMFH6ChFFbmsjL2qASZHzYEGBoBaZBqgJnzTmmH2JeVWUeBD4WQK8TMBTGxNUfOGYZr353UttEAZAcCBQ8KbScakn//uUZPOPQ0kPQYP92IBIgViGd3kQCoAvCg/3QgEtCCHJ7aRYOTfbmro3mj9LmBJVKOxCU7aEUp/Up6DalmPr202AUgBhfALmkGVYd6yB5n+jxmGUBSYRwDh3Fp22QfQP0YO7FNk4MOtGTRghIYORhS7SLUMYax9dDK2yOC5zJ2uQGNEJo8fMIEakZMtMnzZhuAtDoJiF6hVg4W1NMmpQWdfprYbAKZqqHAkTDMLzMchGQwuwmDJITG3DAAQCOLKvIFQbOHjsYiyOi4LvLwukyB9FiUdZYxz0mfqqRm57cnqt+1ErjNtNyUjsa/bu6/T3KgAFbbbbbY2gYsnG9Qppv8YIkG3TAZqkTWHQpkxl+mSwCaZl5EhkADgmR2B0ZuCfxm3B+mOKF0YjQaZjojxhB4ph1gbGGEB+NCnGC0BoVgOGACAwCPTTeHTiJcBmhOwHhN8IHSjwazw5cWQFjwwBVdA43ADjXNqIyHDHXACANLEQCc6Yim6dC1F/tRac01zo//uUZPkMgxsNwgv80IBQAVhRe5gQDFxjFk9pJcEviCJl7Rhgchl1l1KRVqX5A92guzEcbKypdK6lcus6sKi0ZlVDKI3GJfRTs9S0FDc3l/bFvC3zLG1ax1nh3nec1vv71vWq9QMPfUWv/1AAra7bbUAACaL+Ez75RCQz0gAI6JJxrGcWTJjUSAiyl0y0u+VjF/NqERYcaKa60qRcPpaUx3fcfcbeLBPEgkGZwngiDtq7AANId5h7dYYl6cIyaOq10QW2PK4MGNMQRCUymgFjHjE9BwtRiODWHDAoGYewMRiqEYyAJr0j58sWJg4RgXDUwNFU1IFMFAICQAdYoAVwYS06PyaV0Ft/ZuzdsU7+yKvIJRF4pRzPLQQZFQEVEYJbHVNSW/8qSAAMctskAPwIACZYlDKwMjNY2mepQW4DuN1Yu0uG1ApGYLiW//QPdlFlXw/d3OcHgAMV2TUyNF1jSKQiMYMP02V51jCJBbMGQG4ChPmZIL6b/wZBnWWRh4EZ//uUZPWBFq1Azut+ydZGYyotZ0xVzrhpS+17prC5CGm0/DDfkObB+qeJjaFBg4C7ODBVOjCwES6ZcILgICgJlV2GmvUmNCAaxo+5n7lnNIVRZCDbzJDv1WAAAAuRy6AABCeNNFET4N5jZnyrMq06SN+TNV5naZlAK9AlYzVCe5974/xPD9EOJhA1CfiMFWGI4qaZv5XBkpoeGIsHMcNzNJiVAXGBGAeYIYSRigLuGlAF+ZZBRi4UGCXuY+rBg0Plv3ZMZigMMyEDdWtJR82maGqByOM+U/fsilSaGlzkntK6rRpv/2gqAmC6brJaTMTM0toI0xZhlggrGRdKmXq/oYdebVBwmRaKEeGhth1n5leSiTBKTwu59Q8DAAMIuC5zHvR/8w8YtTMA9AgzR3xoAwaUBvMAIAKzAFQL4wNkuOMMoBkTOsbgUEphAGZ4sopmIEgQNQsBBgcQYkJQQFjREEaY1NZpXkvU8eKBGdbs237M+JqDTdf1SnLAAEMNuSTQ//uUZMONEv0YxxPdMlA0IvpNYeYdi0RlIG9wyQjIi+gplI2OAAFScCqAsvOpZRCVzFhq6o8vahTVxbRyY+uqkkZ6m4c37CDjPv6MQo4m1AzKmxzBGHAEZhi7o5uYjUX0GGSgIJt6gfMYSIAeGJyGGYYQuxlH6EGmSOiRBLGCwAuYK4RxoIjhgoGYxQmJQox23ASgQgZiQWOBIkKxiNy6xqVyePyuxTWu2t52tYzeMqiWdKK3jG45n6P7wAA2bmhhkGcPZGc+BlQicklxj4FoIEGBifiy1QANPTvAw+h5yqUkB6irN5CFy3LIbbCTDkv8t/3MqDmBQ84e5SGOA3gGARFjAID4gwSwvmM2/C7DFBgoA3es6nMH5BGDAQgH0wFwBmMO5FWAUmRGaQZmMoRGIKgmeF8GQQYmFQKFszF4FTIIEREABZZlSPsAUTMq1yUNIDF/V163+/WyH1rdHaDligLA6esNdp0DokzLCf4tv+3TZv9fTr3qUpLInvrAACZN//uUZOGNEwYYRhP9MlI1wuo9YMKDjNhhEi/7ZMESC+Vpvhh05AAAQk816ETALzJRQa/6JjMBBQNMoETDes8QHctJIWAAdOFYUp59QEKM4Zu+Bo99cH3GOWzf3r55NY5LlT5fyrrW2YP2WFGApEZBkWBiKYaWG9mqFJXRhGAJKYDGAHmBMA95hOI9CY4KB6GbAtmKYnGPQaHxDxmRAHAobwECRhgWgEBtrrX15l691AR1fyYtQKt/4u7putdts885w052f+j////+sAIElyy2gsoCQ4sUXmYuo50oZeqUaImEqRZgk7xj4nQq2Vwoa9rA5K2FCjdumk+cu0U0VytdqV2qMKTEWDFPhtUxCI8YMCkFRDIYWucwT8EIMAIGcwsQdzGakfMAIbIICdMDoC0wZQVDQfDrMIsBkaBDNgA1FCAcENAaMYWFQgGlkGPlR26rxPZKrVq//aL+fN1InfmcL1Tfu0e6v97lCn//////3+kwFgZTKQYlNwSngw4TKj5F//uUZO6OE+oYxBP9YlBGYtlKc2YrTFxjEg/1iQDgDCg1p5kn9NMmUO4wUgFjAqCPMRSY0zWwtDDIRKwaYEeRv3DmCxawswEAjE5GAwrKwOvNIRj0PXD6BcVS+MSRfpm/9T81KfOOBkybR///1/0///osYMSIkmOFDapkEI9yYOcFymPNqShgCIG+YBCBPGAugRZgoIvmYjYBHG8BGMlAwbRTBX2CpVLrJymDYmCj0EAZIhDFEiE1gPGK73SqelmGb/aeysDLq28QGDqj8Yl39JDEjM3NkqOM31TnTHeGtNij1Aw4AFTAKBMMCgGUxwGITAuCaMDlswWJTE5xOJqwxiOxYMBwhKwQYzAinn4nm/oerGAV4CET8/bd7u16UeS3R//////pBSoTBxx6UxUQUOMSqGKzBMQj4y7BPTMF6AgiYA2MA/AyzCBAy8SJ/DDAGiYIzCQPzjgbxIjQgAWHGBAkhwNPAgo0tZuEyKh8c7gtDbUvz+Mz45hNg31f/T/+//uURO0N03AYw4P+2TBdQviAe4xIC9xhEi/xiQFXjGJF7hkg392r///0AAAnJsAAAZh5y9GXRZGB4WHeRjiwINda8YoBKZSBWPBSsClKYaBeiM/7/pqtcmn9Fl/oX8o8vEM8JQ/azaYcStUvEdVu8IAweQn/MU7NYDA+Ug4wnsGvNslYsjCgwVkwMoBCEgdYwfIVOMYcB7THUOjE4NTAMrDkc7TB0EAgMWZGFAxGGIIBAFqAJpLhyoQSDEoIFE3Q2lWqzzVSQIFn0/U2ff9/+1/9P//+f7PSBSkAMCxlMCTvMvxOBQeH9pKmEIDBxCY+HGFGR4wWXMT4CwecSbJxyukIQYKwZH81C67TZlxaavdbXdRcHDRLW1RVoAwaQSzMhnH1zCUzzExJoBKODuRBTCmgKowNcBVMBaA0DA8CWAwGwFnNsnYwaBzIKFNz2hDRb4MCRhkgBAWL+FrraEfL4CnCvPlzm4173wqrVmzAkPFv/sQ29rH/9f/+2z/6gBJw//uUZNsMkwEVxRP9QkBFoyk6Z6YPTQRnDi/1KQEFC+Rp3aylAAACEJccCnMAwXcGgamiIRoYNQBtcAgIAQo1MGR4HiwdAATIkSNAl2oipguOWy4DFXVZTYNvlk79SiRSi7ZjCGAHwxPkzTMn8BDjGHwdI42sWCMMtARjAxAC0KAYBgWpHUYsqBchiCMTBIx0hj7BZJBUCQIouYnKoQWi6Sk+qmq4EhDEwHRNG8v1fUi5ACDkUrdGdnN+yz/v0f//9X73gW5UDFwmjDdDjDhdDCcTD5PATE0BhIBRGABh01RQF4GOV2CijUtIg2cwNJX+oqZMR6eJeRQ2c+w6E7UlclRAx9qqLmaTAHhIIwXAO4Mg3IvDDeAac1zobMMImAczAoQA4wHwBIMGbCkjELQHo4gZMHNwtBHavokVJGtzBwDIoMeCZU47kWHY2trX/zN3OYNKhZ9RGmRPL7PV//Z/+j9T+2ugAARjYAAADBVMjIsqDMsiTFwZzsT/DAoIwqBg//uUZOENkxoXw4v8SkBAotkHe4MrTChZDi/xCQEFDKQp3JkdUAYxBXYSdcaCAAKpoclICjcMONB6uJXEkD40D/xtqm7vsXWSahiSaspWGBsjlxhHJkQAT38wbMM+N3BSvzC+wLgwJoAiMAuApTBTxQ0xYUA8M9BMyOMDGp/Ortox6GzCQEZgYKFQ0H0R1LGcM1o86CF6kk244vjrKERd5EVJ/Z5QfR+qrZ3drvYzov6/215pcDA+IAMNcj0ykCuzDbC4MouZEwVgSjAkBTAgBDNFWgkzkvy5QIAYykGmGnphwwQDRfMMMxbixe3iwvu4dByPSnYggg+QIu/lAAALtYRMXaY+uezWESMMT8gs59rJDDABjAIHGAwgmK/PmoI4AYMUE4ECsMNSHVfMQGQORLd2o+z95br1b/0klqfUepAASDIHc17/6o1CaF0Uav9XXu+///6mgAAwUROjGcFjMQM0IwKAmTJjgEMHUJMhAQXIYPZDJjSgeByonyFQM/MY//uUZOuMkv4YxRP7MkBEIwj6d0ZHTNxXDi/xCQEfi+JJ7oyxBIM6i0DCQ4eE3nfpfLbft5KQfOrhVUsGyhQx7goLSlSYCeAWGFMEvJjoIdMYTQIvGPvqlZgBYHkQAKZeMwGIXvMEiAiBs4YMCYSIArAGer3Z+ISIYKZLIm5rWz9BZuiVi7coQIBIusAU8xf02dHo6jvUm+5Gz92NZ3O/r1BmAwnQOzHWHIMmsrkwwAyjZ2hHMEgHowKwHzBABBMI020wlwNjVAxZuZJSey8IjK2EGzI0U+3/gdw62Oic6cRQFUvLbf97PkrNJXf/96oAbULSYLSCnmLvBSpgtZfiYH+KWmPEsBxgdYLGYAgBKGAfgSpgYgmyYdAAgBh0giCB440ThpHp4wQEs8kctUNahzgOhk4n2z7JX8cuGCIOs01/X36fvciRWjt87dG6XWp17WM2X2MTNgAASsAABY3zAZwDmAFTNsiTfbWyMGcAcxGCIwdEMy8lkzaFwBA6heIR//uUZPAIkswRRsue6BhHgtiie2NJS/RZEs/oaQE2DCJZ7RkgCMjA9Wop9lxgECqLLxvwtFWDv1Mvx1Y3c1Mat+UrMqrS00YI5GZjGTvGZ5TEYeaABnrCWOYKoB5GNYdmAggGgBen0gvmHYECAAzBEDTKAbnSXc5QyBi7o9KYdW5Yys2z4GAJ4qbaLhI6wkhn/usVeoftmcchpX62u2bHJ69iaEQA+gwKBjzAfGPMLUtAwpAZjoSPrBw+JjsRjoKNxJ46wNQgriILmBQwPYNSpnLT00kkX+f2nZrrwTeQUIFIsrE4cFgG4s9VaVMCMDwzFKw5IxasRBMIuEjTe4VQwwocD1MDKAZjARQDAwYAVKMQUAvTeU4ywNRkOumVnNcVwYSIsifZ5IKhnDIUO3ACX4ZmedN4Ofc1E6ogpUG5QqE2tPNW6Klmh+cqm2XLxiGVUaqVWP0/a5stJwAYC45Zh1GQmLaiiYFAIx6ogtCRURhQAYoEAN0Qx2A4z0iMcAAY//uUZPaMsz8WRDP7MkBJAiipd90RTEw3Ek9/ogEKi2JB7gy1gmvTha+WxIKCC144/jhPyHo4UAIZZTYdU33/0atf/6u3///6P+MMF0EMzEP0ZszFg8xMHxDgjtgjuMIIaDEBCBMGIRgwUZwDB9GtNtSjOikhPj22IibkBreqACwhOtwVkc/uNLP37lSrvdZ6klBKLlj7Ul3KDRDQLlrskrs4udbl0KNWMPWvcuWT2Mi4tqZtLu8qly0eMALUzBjDpH2NIqc6vD/CYeUODswsCgxXiQ2JGNKwrA4wGEIy2DEICBnkSLnsTlkPzbn/r8v7vVgPtY5RZtCb2B6j+hVADASga0xEQVtMaBRADCkwHQ6BkXFMQFAiDDCBrMAYOoxr2kjTlCPMaFQHPMeRsmYNC3ZAoMSIQqagOsARgRQPDougLKDYUe4rite3xSpG00OdQf7WdEUWpfq97/sTVtdV9MoAAYRGIay00bVvcZEHMckLWpg/gMGDQRhAaGVIwHFo//uUZPmM04cXwxP7GkBMYXhxe7siDeRFCA/7ZEEPiCIFz3RAYJjuYYBiIZZiOkEzauKAmrRC5BjLt7/ndbrVA+ccJ27pn9P///0AEYdaHRlyduHkHmKbPxUZ0fCj8YgYB3mCmgOBgXwDIYRCA7mMHgSRvhpiRgMOm9liAM5DbGARKhcp8OflZygsbZhtsKCcDqQLn9JyittAHmbacW9LO1jlTDss+9d2bU9qVy2TsRckdSZWAwsLY3pVM2g00xPUM2iIejB8BtMOhgxAGTE/PPmmB8oGBIRMinYFAt4YUg6zSS4U87/Ms/3/26VOXOBFd2z/T/3/+v/3//tVCgw7DVjXliCPjpGM2IymjcOWaswnMEcMDMAYwgCjMDdF3zDcQOo4RHATMYwig8OQ9LdKlApQhHJKKJdri4osGwrYtRKbHob1D0kXte7S73KGX6A7VXK//pa9CUiE1/6VxaUAAMPEKMfZPNOKSMUWCMmWXEwUwiAAAiYBYCpgglCmEyDE//uUZO8M0xELwov+0RBGQgiCd90QDRw1CC9/QoEgCCIJ33BAXJDGxghYmlHiKKz5DIJYtFRTeWtZayz+eCzDrKrK3ev//pMCIAVTCyTVsw69AyMQQDOTTEIiwwRIHNMEkHIhC9MFh90wkAkjMCUEiQUPTgA4aGHcXiYCEpBM8dOCJZr6t7eGOWrQVWxhBQum96xVjllmn7Upsm2tHvozD4sOTNvFRzmxhTF4rdQnRlrBWHgFSj7zDN6jYAzj6cJTMBoTT3weBAPY4BUicYMBXZh6AGAYkyQASjCK09GXuCWigp1YTXr3v/PfN9ytjDyWpX7cRR3/92n8x/V9qP1/b/YxSnvMLcH00Zn/TZRwBMJAoYweZfvEYJwYIjKDBLM2PROnAyLUFv2WCwKu9P8EAHubKMoclvSgEA7rlxARTVMPQxjXf0a78gmzFu0Xfob/RUjXl/aIlaK2AAAxDPIxsno2KmswLMY3KIRTCYA0MJgUcCBkBvjaFGg+om9Qs63f//uUZPAM0woMQovf2KBFQfiCd9oUDgBDCA/7ZEEviGGB32hQchw072o9lfL6g4scHS5kuoHL//t////0f//0xcQQgIOMWGOdDIbibQwysTpNfLlYDB/wdUwtA3jB7D9MboGo1VAOTXQoyQtMbHDpXFnSdKA8CAiSNPKbMP0eMRYDMNUbmdhJrvXfnUuXLCtKd3W/jvX7SR7W5R7mtc+35d4m/Bax+f/jaX7ilFm7vYW/hykzHVWafvkgDAUkwvxi/HLGGqFWdWLnBiIgamKAUYDABliBC5hD3SgGtNHoJfOmAGoZiF2hpAo465jig5V3eIln7v9z/Zf///+n7zvrAABbv77GAXTGJT2MpFEgw+hdjy8qxMT4GQ/YwcoZY9B0QlioEUFTvEIcduHadLaNZ17lGgds0OclzSb2Sly99jqP+m+u+f0faO2r9Nn/79PAGE5CmOlIm2MGmigfHU4cGYegFxi8EhgMMS1A0oGUqk/UqTEAHepocCFqkoozhHoR//uUZOyM0qwMQ4vf6IBDwXiCd9wQDxwzBi/7ZEkXheHF7mCIYsSitbrp1zf/s////u6f+/9v+wTJVMYZg0ZoJx7mtxnWYNZHpurpqEYYuBOmFsBgYJIFhgeKtGBKEOEA5EwBA4AETDQKqPfDLAlRyuvSy4Im4nDzja0JF/tWmU+7r+VvVV/F9a83H+7eVdb3Z/V8PjgH0bGuf3nOffdRr/5q01slPZYk5Q7A9gL+giBPWZz+vk988OYMQswuQBAhUC4IYj5GXjL2viyAMU3tprZCAwrCtSWVkTptnq16L/Uz1ej/7v1//9X9z//MAuCMTDZjAUxF4wcMMjBjDn5A0Ew8QB+MDxAPTAkAIwwUsTuMN9AXTFSAsqOHR1rmmg7jwDoTIp6mCJmlsWIoWKIyIRVbrfrKZt1eq7u5lN1kPffIzZ2q3s7TOXtIh9DSuxO5HM7UVHdWRO12q6LMiotc0zK0+UaRUEDx6sVJAsMSssNh+8M+jzPMpiwxGATzPQYD//uUZO6Ign8LxcvZ4QBEYXiBd9wQDqAvDM9/wgj7BeJVv2xAGBkNwH+44GF/BCHiok/MASkkB4zFqeJSN62Cc5KyVD6vV6dvTZkuv//V0U4p/6U5MPUUY1Uo3D2JIkNJwbw5X043MLzAuDAsgD0EgHBgi4IYYgWAMGoUmHAA1mfXgpmxKAyAStWG+z8mKhgRAFjQWMiEc0VWI6DGMjrQsJbVjEM2mXFRomhZ76iVFLJ1z1qWSUBXNveaXkQisqaYaeLvMi1MuwACgZ2lQYWUyYUoMb47fJhYglGHCJKDGYmIfRKEsOcsDBq4ItMM5ytWs777naWUJXX7P///+///3U3UzArImM4HFI4msAzMvPbN68XojCfgXcyjJYxCCczx3k8LIwwwAdxgwGTOIDVCWhvImg8jfyuUXvx7+9dq1BS8sAhOxRdIAMrNLSRvePvWpR5MVULsCupWfWn1ThVOkqRkd5wato0hPSgDUrSfYq4AIMJKSSNopIumK2pzBkYq//uUZPiM0/pZwgv7KjBEgXhgd9sQDnwvCC9/QoDoBeIF32xAumyVsGBwEoclUyGSHOluxWCXWtz2F+qDbZlTWDSCVCqE9Of76fX/Yj/Ur/7Lupsr1vQYpwl5rYq9m/pvUYfqLBqDrnqYQ+DYGA2AQ5gGoBoYCWKmmBOgWRrhqY+CAQgOBQ10M5iTAm2j3ZReEI8VhRQaLgwBAyXeD4aFgKKcYaKMMhMw1+9sV0AAypww02nOJQhCC7NaK7FJRJrpoZcEFk1FZFL2AJRDM4kHOS3WMmGMME6dEwEAZgaE40YKmKJiXquqEOKmeN6WSj7+6LcQp4nCgdc1cU6uv/uV//o/9v+hFGFjA1CRMozAc4n3CzIcMnMRUa8jAtgUEkAgjAAQGEwJwQtMI0AaB0K3IQlTvuFA+YKowrK1cFgieoEFQmGkQabLHzzpd97g2ggRSITR1Axr+owgNj6KkTMlgQgXMxk+eZWhkfPNNlS4q8sxh1tq0AMAxsMhIQMc6xMB//uUZO2MA3gQQgvf6IBA4TkNb7oRDnQvCC9/YoDxBiHF33BA2bNYGTgwiwaTCAUAoRC0bAzUQBsoZEYBAcKjdI8VitYzDUjZziz/b/10/6v/1df/9nySAGGuK8bBiaBo7RMmLOXkaFtCjmDmAsRgJoD2CQAIwSAJdCCC0wcAGg8iAQmwij/PoqpFr1ucteGQeDQ1ATApmetKIOumX9o69L5hKBOanKZYkNnd6nMewyQFSLo0kJK59uiBXUhp14u1UGxyiiUgEADRhVi7GN0OIYGIhhvSNSmE4CEYGBpyGQPY2Gq6oHoEIFyzjMAVkHvqqyp6/4rjQw13u////q///9X6PQpAjAWwXAwQU26MJVOaDBdwW02SNi8MLHBMzIgeDFMOzSa8DygYTFcCgwLQCGxhWKKYzizSx2bzvA48irPqtLnZ8E9LB50tWGm3ZM4ivYLOe4+y62ucYzMmYycYVFLqSyAiNWadYqjHpDZ9l9MAAASzGGAzSsiDAwsTqPT4//uUZOuM03oLQpPf0KA+YWhxd9wQDcQxBi9/YoD6i6IJ7ZSgMN8CkywGAxOaFFDY6TBbqLrZPepsZVnrLl/1MTOCLMzX1//+uv/d1f9f9H41qzBkCpNOMu0wELxzNoDFN9iRVTC+ANwwMUA/MBOAWjAXhNwwOgCjNK0ABUu2eFMjW1CNoeyClDjECBySUHzqxAcUPIse9kaZaMS0lDyihpZpRUu06RfPGBVd9bR6I+TlWqIkGMVSEn34EusENsVYE3KEIJmcTvmDU+gYoDsiEZFh5R5iMMChHzEgeWSdGhBxY5NTKNSa9zd9gZ9E1Kf///1U6d//6+3/91UhzA1gmswK05HMokAaDFPwoQ5nEe0AQ3eYiABpgrgTGCCssZL4LoYAWzBJcwoAHEmKSy7jqy7gCNseUq2OnV7XgyjTdUvBC7NW/87dwy/7x+/utPu51GTnkdKHyafxXIwv9zTjeLZtK2wSBevgcf/qfFPDv8kAG6mqAoLM0UAK6wzSUDcX//uUZOwM818Vwgv9EVA84Yhxd9sQDjApCC9/QoDfheHB32xAJ1MJsAoHEKVxDLBDu/VPBCUu7v2QqeYtsu0YlAQfc5rer1U6f9P/vo/vY5qOv/9dFXRmGGBWaiwKRz20PGeUVmcEQR9GGNAQ4Q6ZkEDhofOB7SG5gUAi8ywDgqHbkVaRRqvuzje/zRnAuVTV5hnN95K5ezt33L9Cy2tO58n/ff75+g3/L5fV9G99g21FdhjuYqtu+LJqm3O6nl35X5AfJ+6mlpAOY7kRsFAmikaa9CIBg+ANhhZACZdsNxopenkDpHnaLnjCwtWgc3RmGvd6KLuv0r7P5D/xXX/3/+4AAAvq/swDwtDCZSeMTFlEwORoD6Vk1MV0GQxsC1LzNr7PQiQyBVNk5TvAcvDla3kTruaQiNAaA0IbKRkBqffFrqSAomITyxdcVYHxG2kWDOM3s2/TH1IRULPsStlNLAGEAsmcafnCDWmZKHGv2/qYN4KBhga6RiM2Bw4ta7S4//uUZPCMw6kXQYv+EVJBASimc9sQDfQxCC9/ogjxBGKJz2hATDgLGjsRuyQFEJUt5+5AtuSv///1Zv/T///2/9/SYGIAMmJlgnhlcRTEYbOKhm00PEBhIwLaYCmBDGAJASZgYYuCYQiBoGaD4FAgQDnJFSfLTrDP5PKtNZa5nCM59iQkJX8nv3//uVwh7OzelkLTxtTmZXmK+PuayAw36mH1b9K+Z10lPqNxuf7WkhtESlnZedH/NVRL7r2CgNMSINMqbKMMk/MjqdAdBpC4sQh474mKhy7ZCVQAwIFilavEKK9kB1HjC0tAlvfr/mPVRbpt3f//R9+5PdT9SklBoFmYRIK6mQMiVRhpApcZUK6qmALgtxgAQEKQABZgCQsSYMkBFG2HhDIwBwGTXEjVyYn7F1RKKbSQ8iPRCucj6Z5yKRU/55G8JyNKf3+flSCWptfD9flyrdR17QOc11D9qsBCotd0K3J/9LSLv/IZ5P/x+7oW2UFECjwzoWjnt9Mu//uUZO2I0vMKRMvcyRA+gThxd9sQD0irBA/saMkBBWGB32xAMwyxGXTA2A6AoCrgklSZEe2D26EwVJ+fBVdBYoyY0mp69Gn/3f1/+zo/t0fo/+tUjMqiPNToww4qTMGVEeTIgWXIwS0FpMOiWMQR3M4L5PFAuFgfQGFvzIEGGT4X5J9v9Y1OEZsx3HXnnHpnjhxc1cLki4uLoZn2NF4kONduYLu7TIsNnHXkutirMFR60HU0NMIDAFagZJhhm4Heg0avgJtguOGEaCKZwcCi5orYTsfOMQSnpLbHKe0J6ECj3VRjTVX7blM6vt7TCHf2f61f//nvk8vVAAAKBDATAG8waURoMUsLijA9RDo1XRmJMHYBYDHEeTBIQDO5UD34azMliqBCwU9BxtYXYlNN3us7nHPaiW52Zp0AnnI07CcPqMs3f/s+0KU3f2at37O7OGGe5mSqVL8L7q/+mP6WN0b+t6Nv3/M09f+P59CABUGmMzIaH6xoZdHFUoyYYADw//uUZO+Ow7oswYv6GjI6wTiSc9sQDLAzCE5/oED+hOHFz2hAChgQAIlwa5Z5BbVAgrS75ESdw2lwudEhpaF/6v///37v70/2W/+796QDWaVRwAHMCpCRzFSQqUwXAITNYhOwjB1wJ0x6DABDsZJb4bHDCYagEGAiFQMMJgpgqvTTNGFkO0pSeRWlzvL0iGx2DSbC8PpOxZ4lQQYLhlBMMELFGYHd2i0INUbfrNOPv3GJmXYQDNh5IdtW2RNMgKIWG5heXivpMUj04TR9gcL+PGCBMAQAcctvRQ0X7pv7KOAFaEwQGqMidwobV7+j/s/3d33L9O3///S66hVjhgKwmqY3YUNmCcgWRyXRb6YbgASGGyBSYMgThgxryGMGFmZWWGCBIcBEZkRBTyz0timdJQhUGPmIFSfDCv0PzjUTbU8uK109DryDN676N/zzv99tJt8h9BqPu4/AylrDGXKzUkJb2wX+22/DTcv02sw1ungBUYmTGia8jBg8cnJ4QUHD//uUZPGIw58MQcP90RI74Shxc9oQDfRnCy/0ZQD3hOIJz2xAMkyMBgQCyAKR25Sluaq1n9x696kMBgTPhiNgO1P/b+v/////u2EkYBcAumCAA5xhDJsQYO6EEHJOi3Zh0oEMYYwHpgVg8GG6hgZy4FZf1CcMAKmCgB0oLW2zCezoQ40t1zK/SKZtxUralDUiyB96vG8j2fz9mWtIXgz8jf0VSO7/9mdMyv7Avk7Q9rfUlhvtq75eln3B/X02X17P0GPp8c08y9PDuMAMYK44D0rzCTAmMoBC4IE3BtvLZPGUabPfrdMTWbUaQGBVr3lmIR//f3f9Kvd//u/7P/qVMnT8NTLyMeHJHzD2Ayk4JRDcMMoAyzNsQDHwGDTUuz+sOhoUVLxAChiMGa4ZPi4T34ZZXpLaUTlbWR+YfhmdF9mnqYHd9HsGth23//5XJa6rVY1w+S/t9mv9NtvfrLVVv6ephHP7gZxX/ezp3Jum+2SASGEaH0YaBDYWElNtRzgw//uUZPCM86ILQQP+2RI2YShxc9sQD1j3BC/4ZUjnhOHBz2hAeAUzHjkhTMlw3NLZ2VIVz47tHPGQtzFTaLdOS7Jyz/9//+z6KO13+tUZVMgBc6DZjGGgpEwqMJUND3QHzBYwL4xkDkoC8xxv82xGYwsBUBAczkeLN18KWDQqMUIhq2RpI4fmGOTW91raEvNJWVZgCHb1LU1wTPODLFxVtUclSiQCFrVlE2WPIWOGrMnJRyb2ghuquwQCmUspzG4ZPNGNuhuYCIEiMsGDP4s86silMe7apGxyqKU3dud/q/cj0dnsPdSvd9X9/T31qhABdmA/hYZi4J48YVUJTmlTR7BgpQPCYCeBYCAC5MA+HOTBFgNo2iEs8FlZwMSKLlRmCd3Luc9rB+KGBAQrT+k4Lq/KYRsGYD1oHP6cih27jAjY5/lmzhg++l+VDvWiHStuWZLmTIWaQUU6eef/yMxHRWv54/iB8+Kcla4RR80io8nAQQBjOTdPEg0zdIzIZgoM//uUZO6MA4MLwQO/6BI44Xhge0MmDPwlCk5/oEDkA+LZv2RIAsFMKCAhCQpNgIkhucia9e/9TYMkUVJRQlTL1Mp6Uabf7KNn3N6N35j30IGavbUERgGoAQYKwE2mH5mBZg0gcsYe04gmAhAsRgDYC+FwDswNQQ9MMSAPCIqTCwAQO0rX5L7NUSSA2ZXiwjjf3/iXPNvKPyZf59tpodQ/OJ6T7E/+HUJqU0petPvnmUuXOe/PxAu0+Yr84QuoxurDqL77m/9M3xmjWu3LLKruMDGYz5IzAKOM8BOcwSAHi47yCCCTVJZL+PeSEV73Um3CZb+lNVr3dFm1W3tUj9sn7Hf/9f32dFUhjAHAB4wHEJaMZ2EyDC6wxUzyla6MFpBBzHwbDFccTQoLTzwBxYYhIEQcARjSAaZ0Zo6gxWiGLsL6+Un51zG9uLQYU07guj6UlXudT2mT2tX53/u38Py+6bCPSoenO7T+6I39Oee6zV/+iLBR3c3/a78ehIBKExum//uUZPcKxBFMwAP6GkI/AThhc9sQDwETBK/oaIjsA6JJz2hIDLHkMon821U7jChAhASgDAxI4RnHXlldrkBjARuoJtmdyAfCYx9f//7f+vVV//aFUVYGQAEwG0DWMRVEtDCIgg40Uw4JMHAAjjGkNzB4DDGibjbQYTA8EkTUywYDkjZwqpjDsm7jSpv4bN0YMqX3Z3fIfvGYHv3n6a3e31CvKPaR35Na8s32Vfy0/e5DIPz6tP/fE7vy/3/U3/vPfEExATTasfNHCY30C0TCuAKDnZEAHKAKktKjscZLlMFySB0wBZskNMuvMt0t/+ir//Sz2f//QhUAAAmRBULAEBgFYVaYMgRamCjgzJrqJQgYS2A5mQQPBwvGEH3CEWwwShoAVpGJwHvwBggNhijzQEdZyeHKyTL2O8S6ecI/Oz2nCK9n3+WTnIf60uEUyn0ued+5eUzzyzyWbF6E3/nWyfpnnkQyoLUXdNp79MRZPnIACAEw+jTpzLNViA5CwtQ4//uUZOqI054awYv9GUI0YOhxc9oQjag/Cs/0pMjbg+HFz2hAXoeSR4KEUYiO/c51KO048oOIa4f0VjyofaBfZ7s/Zb66uc2vU36lf//jdv0lUARMEGBKjFHihAwPcBCOPOBpzDTAD4wNcArMB+AaDA/xLwwwMB/LBYtSDRx7TqfUtypccrf0eWJlIUR22bEdOwnLsIiOHT7FKf6dThcOxVf8k8j8fzWZkVOzB78DLMLN+4coOEKrafo1/LviDueMWqv2fRjoF3mulGUQFAzDA5Na54yoYTc0NKMJABcOXI0GJrB3OapY+7HO/d4VYi0G6UPOFCoehwn7en7/9ZKnQe/uq//b7fm/QkBMAPABDAVAgwxgkDaMGCBzDZpSjEwgsCAMdgtMAw0M20EO9QQNgks8KImDU8NTCSfnhv633VrAM4wdDFq3gRyBQZjQM14qlsVTGrCji21rgC8g9FbDYw2FFOirOlhhpYz2GClw6XQSDBEJHnpqqXG7RjSgIWF4//uUZPIIw7xTwkv9GTI84Phhc9sQDyy/Ag/oaQj7hOHJz2hA/V00xDBRFF0k6iITj16IW9OJT3M2CNZtb811+zoX/9qNP+z6r/p6evpBKrUwKIwxsZZjUGBmB2JCcLDQxhQAomLhgkAme/57oojjD0YFh2Q5ctDmhCIEoS0X62vbdiRdRRuLJPKDbxtr2NdEGpbVzi7tCTzHmgw1iakIHYpWilD2JOvBcBAK65QWAguyPIwMfNNhqeMCAHVWgEENgpVlF+p652xTFM6Grrb069lemq/R2/1/5FzOp7P6v9MwIwEPMCpAWzDjjnIwh0XsNbphqzCTQeowHICpMAfAJDAbRvEwNsDSNmLzKwYwIGODMWz3qJsPQ0EgS9J5AoSoQJEg54ykvABgVePXaFBdB5UVQZm5kP2ghUNFrmCnI1BoJECxHCRJZRmT5oWFT3MjhoPS65s4MqHqJKwQGyKyxkrFihm1S6fNYAEnoYlVUDiCBY05yjWscMkPgwAm9TAG//uUZOmIA2ANQYv9yRA0QPiiY6wGC2glFY17YGDdg6NlrshMBAFQC0jAaECnMx2D6Vaz1vJDTLCaCCAXNqYJ68f0++j4xstTR6HqFpFTLU+h0rS535n/W3aQ5kcdG0qOYreItmEFBqphdSpAYCwCBkg2GBoomP3Nm8goJlsoHQHMGwgidz71j7NzO0BhjnJOW9f1ZHT335W08I/3a269WvubyzfJuOt/3rx1YIpq+un8sTQdWM7W15dqcG4NiqLL7bJ5fc1PymHGKYS1phdzmeyz4YLIH5gBhdcwGsM4Q/SV2gWcPu2HRtEgODsMttQ/r1L/o56MLP936N+ijTQvt///aigAsMBBBCDGehhUwhUOSM18Z+zBYAR4wrFkDAEaCQefAAKYAgAkWnQPHasyX5XP5b5P6xurGT6/WViuu9LUyMj0MHn/gpU+kUPf6eh0gTQ6RIxFaUVvi+5X/E5KJ9HdHS/U2koX++0X2ayjTwOktGZ9YcjJLdDaxCEKOhhA//uUZP4M5H9UP4P7GiJFQOhhZ54ADawvBi5/oEjwhKGBz2hAcR6AmWNUpKUwagJgMTWIDDo+BlErmIDYhynnGlbbhhcgjb3/f/svs5nX/+pP/1VmAJADZgLIOIYPOTFGA9hH5p7KZYYQqBumOgkGJIMmeU1HWAmnyAGKgUw4aWJRG1hfxqV+wRzK49wxAthoSB4a/Ka0UM14Oz1z+uD2Wktn1CfCmTf153kns9nYNtfKWfafHmd2PTUw/djb+D/f73bx7dhpAVfYLJh4akBDs1mypjBpAFDlFJmJYPqwuxXga+5iUoZHLC0ChObW4YB7fu2WWei3+j7//nv7/9tdMZjgxVbTD1SBswnAFINigNVTCVgJgwJ8AvMBCAQDAOhBQwOYCLN+ACAorny01e9bu5fj+Fy2lpajucUs9aK9eD/VCdMRnXXP28Vf/LfqPce3+2hMo7bmJO905rxv/5F7vC0lhPsNO3yDbWXHSTN7nl0wCqxgcAGfp6YSBZtfBaiw//uUZOyPw8E7wIP9GWI0QOhxb9oQDjQvBA/3JEjmg+IJv2RAiJQnT4LGRCS+0pyeRhZY2qWk3tDwzybNCfcyjT6O7/X/6LtfT/1BaGUTANgCEwGsCzMU6A9DCtQCc1zcRaAQhONJCYWgsYQ8Qa1h2UAcvRooQTj2386EiYikIfKkLs/COOI62qo1I3aR8zPP2xfBi96eZ0Y4+Tqx8BkZykR/vsujHE+tlncyOlnxPncs/JsV8zIvtpzRTW33qnf9+g+mJdXAABPdTCEFsMGUG43PydTChASCEqVwzuGqd2thH9fqvzWf3vdQTucapordYlWz9/2d9Or8d+3t+n5C1f0KFgIMwOQE4MYnJJDDOATM4QwlyMMeAhSZvzIALjSOsj58ITBIAlgyEGDAsN6YLAASwCBOCLKr7W0Hp1T+diMmww2dX6528d9BWW/NVPQ1Puj06b86Wfab5xPOsU6IpYzGZNqan0BMnnSLmRFn7hDT0OA39tvUaugOqe6ArSAA//uUZO6I04kMQQOfyDI2QOhxc9oQD21/Bs/0ZQjuhiGBj2gIA1S3KqYwohONQTPlM+In4w+AdCptQagp/cxznk3XLipcRY5Yu7vd17/7rbUlGDn+j8Z+z/7fVAqgDxgDYCuYEePVmBHgyRplJiqYOiBJGKgTlADGUK6nH4hGEAGlpkGTD8DH/lG487ni0qj6p/s62PKlU2MhkRz0OzKxr7EejE5NXsrWZUe7obuiPvRfZvTsbtV3bTe3SuRmtu793cpAmG2SN5IWFkAeQRHB5Hqa3ZGgurGYH4FgABd4wdQ/ppsPUsCcy5chJ7qUshxwBBqzdt/0f/0e6n/9/7/t43+2ACX4kBOLaM5JEAxDR+zwU4QMQAJcwHwMAABqYJZYJhxgnGcCqZOU7gm1prNt7gogqSFpdKJUQKWl7Gn4xhtAsqyWRqDgo0VDMeFxcZeWCSSyLAXePNvXFjQWALlGVxbetrporMhBdjqgAACGWZJoAi4wHJgshzEER4HAI0nl//uUZO0Og+NaQAP9GTI4QOipb7gSDe1lBi/0RQDfBKGFv2RAGx2amFV5qZTe94wYabxrVhch+l7B/q3T9VXf/fVHt2Wfi7N937NghALzAcgHgxpUPSMJWEGDI/WkMwCkE+MAEAawuAOmAYCgZgywDkdg400ADApm/nbEF8yzwnKn92rkI86gOwVBBEkqGV9CieRHkq7ZcHZXf2hzZINY2szDK37fC26abg6+Znw5SkzMVeonIX5GaWv2GTQnPNaf6qPLGfQ+Rzw7pBywrLBZKVm08ZlkAY6CKJgMAQIJ4cJJo8ckF/kKrQEWaL1PGukSA+eV7E/0botv/Vbrue2hq9K3//zX2TBpGMCzUwowdPMEBDOTG6U/IwOcERMLhsMLxdMso/OdgaFgjTMTrARPNXvdsTvcvs4ZNkv/VQGIhs5Zehy7ayq0+WM/irTwKnqYa1faNT+Dr73J35NKv54s3fP/Lzpdpf5+1+i8vt03Xn8w93Pnh0kFR9BNJbT3fsjD//uUZO0Ig0QJQiteyDA8IOiJc9oQEAl/AA/kaQjsA6IJv2hA0FQcAmOAER9iL2M5YSKue50VkopWmhrX+n9HT/b+3/2e7VR6vvsrVQGTAUA9MA8NgyIF9jB9JiPRr18xHAxjA/A9AACxhDBtGQ4BuY4i2V8ngVJr+E0RiYcLL0ZaSFlu/plnXkOZoTytabZXMi9pxdJT41T872/kdkTJXnCiUS8N7lzLIjhdUEcOmT0ck+qcJ+b3yB7Xz7glgI6bj34LeDD4MKDhfA0pWNR4wcwaACAxNTQQMBqsSu3IzpwRUhFppwueIizXDI7QRDitO1mjX///9//Tu9P01J1KMANAEjAMgE0xIUKLMDUBxTT5zRgwZUB7McAqAwxGPmYGswrgYWhYCRCAxgwEMETda+KI3HBSlZTWdf9yKkNbnyS3U4/w/zmEk1lWmq8fZkw/+8/RBznb/viWaXadT6v0SyS8n+bZ//DmSfHpd8/MbnUIA3QAg8wzQTD4fNm8YgME//uUZOsMw2YMQQOf6BI0QOiBb7kQDwVTBk9kaIjtA+HFv2RAXGjirgZiEisLsW3vBlUWeE8w1YHDygaUKvqp//9213t0/2jde+v3K/9H2lQFwAgwBEBPMTED+waBCGywD1JEJomEeAqYIQLBgioGGIGDuYCIChZ1HIaC2XFO24iCVnhO1Y3Bfb5NxGS6eea516XbULc9mTvvy1CJ0K8KnGyI8janTKL3KO1P5rvE7sjXnzMyMprqnctS42SZzSZdpkaNicBad70/GWmemFEE0IwLTZKFaCBASI0lYFbwYriVVQA53vHoYMcARyhy2DbBoS7Kn+/1b/obs/9dNKX+zaUSdo/xJm4xYpMNyjCMRYgwLUEINYrDqTCbwCoxkBUwLDoyBnI4jA1YVfRAAphADD+5fjz8e3sNCNy3t9NGCJSR3p79bqgDhlDHyPufrJM4Bsy61unfkxb5qvzCu46srR93mt85hcLJ+e9/uZfN9t+WEa6AgEXf3VKU3DktCowO//uUZO6J04sWwQP9GUI6gOhhc9oQD01vAq/4ZQj2BeGF7RSQz2bBQpI4uEKoK345cs1ud1gila6bGXUUp/bf9vz7Pb/Ff/RT+q4ITAMwEwwDgDjMGLGXTBSAik1nAu6MIoAMzIEGjEQCDNEizq0IBYKW3EQAGFwNt5anKAPGCCZzLIhV3po28TjeyGZ3g7TdZBB5nZz0e+hUN8UMJ6CUhm1B+qpecJe/QlhLTj1Al19PtE4tsFjbY7hTfAOncLBDDeUoACEosqtxUATKqkwh1PFNKMKATQefkKij43OZSOq12lm8Ex5axb9kLv8d93++zdMGbPr09dX7nHf2JjAPAEMwHMBaMP0D5DBxwrQ0SlCVMDDAtjGoQQMExjvjJtuNRhcCoCA5posWcDgChtyFC0Nt8s96yFKezFz+gttHOEU8rfcwfAhi14XMa2Mk6vjP9WMtoEETm9wO4/oY9OO08mpT/8hzvGLxa9ax4oj+zKPCokAiGJdYyysMZrDH5TbM//uUZOqKA2EMQQN/6BIy4Oi5a7gQDyh1Aq/0ZQjmA6IZvuRIAMCxBC9QhphyZ1Z6lmCYGNDnOWSIii7u7urYto9lrZrztGmv5ZuiWYlTDlKv2f0If1X6YFGZMQcYWZaI6BiTDvmzZnwYBgZJgKgbDgFoAKFMFYDpALARUOZNP7Wxr8/er8ot77TyogHVNmDSR5Uc02hpza8CnWgKtazptZVAy1zC7z78cbMhClVjSQZAGTHJ5+ouBFHTu8QGdAwFNnNwRSmJym4YAIFoiAJ7iCuGEJXYyipgvc90gkVujsymv0eu72kdGp9/t3bL+L//6LOlltdAQYAKArmEfDmJgS4XsYdEshmBNgpRguJ4JBEyg005gCxElFssoYkAuySfsUqMgU00IMxhTTpmsZrLSmbf0kmz+YwYs1Uzg5lH7PomVdcxDzvLoTKbEXiD+sUZM5wklMylyTOrG7e917+xHmeTrYVU/Onvr3U8oE+jUwJSVQY1w1AkMZqiKpgUAMFx//uUZO+Iw6skwIP9GTJA4Ohhb9oSDIAvCK17AMDmg6GFv2hA3UAWbgxUXC+gO+1bLz6L3vdSkqt4WStp6+ptX1IS7/p376E1v1fee/6kovs5CBDAKAJMEEJQyDFJjE3H3Oq34gwXwgTFIOjDMNzLIEzjAARoMkgUxgEO7bY5TFqqIZ0WUqU8/rt1pW5e5lPfvS/Cpl96rdlamR/L9Lf6pkZe6fGNipEWTK2WYWh59fBd+A//+fP/UP7vAtcWSM/TNpNbnRUSYxmeRk5JoJlyGCmACHAKbloCuqR2K9BKOFWKwWJKNvA6gg8UCUtRkFd9r7v9iV/+3/b6f3WKBFZRICYwTAIzNzF5McwZY/nsVTHCAoMVQhMFgKMYIyNghTMDwMUBT3GAUl6nGg8Eplxq59T2OPzK3kbq/KnPEkVCG+bwszPytenk6G7Tj2HkXFXhZuSzEidsj/maQPCI1YjcijuDB9Phw51LecM34TNYVyhudYPDGCdLCKEPDgcw+U0Q//uUZPOMw8xiwAP9GUA/AXhhb9QSDj0HBC90ZQjgA6HFr2RAo0FhaQgLRC9tByYSRlV3UhYq11aEtWYskEsOa+v+z/9aP/v9f/+tBQUAKGAjgTphoAiCYOqCZmp1kZRhUIDWY0AkGCkYQcECRPCA6QNXkYiAO/FfU2Kqo2bGWx8akiVGxYvrV0JZTciNL3pwjOMp1oo2sqv5xLnySH1unf/5NzlJCLzjmXrLf5KxQ5Wn25QzSJmTauVHSG14TtmTJCMWj0IRvWif2jOBgBbHlYZkwxlALnEEjr28Pt/aogipa8kwUIkAuE4qyY0/7/rpZ//c9Cfo/7P8XgAQlelAEywACcYD5fRg3ABHZ0CeYfQFwGNZhQFGO7UbhCasjosyBwfnhJqW+PlLnFEU9EIOGFOJfBhnvAL0aJ6F5ZHSPppkxVYelfdsln1CqMx5cMiGqfTLgIE/6teSwhf6kZzpHzwNqHueYPrEWwyFuy/1KwBVSVT/JeHaYa0Z20pQOEFS//uUZPAMA9NewRPdGTIwYOiCa9kSDzV/Ai/0ZQjVg+KZnuRAT2CORFO4TPSSCPecU80KxdfZ36Pt/5RN/2dHXt29/eiv7gCpEkrpAAC0yJzEzA4DkP2dkEwxwMDJgLAoIM6pA92BAUBVhkPQCDqEQ4QoCrTaGlgmu35zPIq8onuxd0zekiMR1Ye8d9dqxGVPedh5GRH5yXMihkR8TRGB+4k/OpkOexehzet6l3Ur3PqF3l3/Nv98OD3QIVIzNDqlxnKJkYR0jIJhOBCxYaamRBUE1IYyi1+aDQoKNHUbGf7/9Eo5306s181or//V/0UbhCAHGAPgKJhsALKYF6ELmghGgxgjQHaYmhsYPAiZJwmbjiEBglYk8gsPrUb9mtnhS50VzIL9OJoxLumvlUN5wxEOmTLayBKLN85BoZtauRkuP7m2wXaPL4F7+REZ0SCAX6UbwZnCivKEWZq9KAEmE5CeqamHUZvwV2UHFx4pnneSMADaWpbSjZgswWnmwVVG//uUZPEAg6g9Q0vcGTozALimZ7gQDn15Bs9wZMDTA6JlrphQCwDsiiIrcaVj+VPVFeskxrQsgihqW9vsqfSLZW0ds+v9uSpJez79P3fSwZgEAVjgTRjOoOGBsSAcmu6ZidhoGAMBqy4wISxTBFBEPARYSwh9TLiJzzxLUuTBel7wM3RRb4TVz7mvaWlree6lZXbMjz3Q5yEd1r+pxD6VLN0bPkhwlOdZuw8sz+ka07zjFea25GVxItIWXp5tgagtp3WMm1YTo0THcDAznQsBiy4gOh/AA61Ems4lZRGbGaTxG4WnvR7/9X2UKrR5b1f0+rWqARSuuu7MAEBAwBATzDDJjMEQVEw+oljBGBDBAqAIhMRWM1sEG5vowswSBKdurkEsyaTnO8Pmcmh7BnhMXxbMyXanv9Lx6/Mq2mtQtfuRS/EQjhG0F6nJB+oakWfllT9EvHtspl/89C7Dm+ZOvCWUz8FHoUBAALQNNJIWbRWZeWbD0gYPgOWoS/EVx6FP//uUZPaMhBdSwIv9GWI4AOiWa7gSDn17BC9gaEjBg+HFrqRQh85Xe7Sja8Z3kH9P9Pv693/yv/q0flK7Lu2tyzBQHTDsXDx5mDO+RDha+jMSgHYwCQK0vjCIElByDqYzO3sKAfHsn8MN/dyo8unqxscVKskiETA8MKCPpzbFW0jeXEZ/ttDpcY3MyNXPwbHdzbkqmaiPPHS8nTzXJ4v6W3tEYsipwkpyIY6FWMIqxDUrL8XhGRGkdBPCAWAENXBJVBgDPGLKR5RXph0CCKbkJyj+bmdyv4q5BFFYzLNIYFa1HsJTCv3d+cqbou/H26EIr7BbwC+lFX1V795zAGAWMgcpIwThPzqnoeMVkH8xIHgUKzNlAOtB0OESXKVRgEEv9WnWVpuX6PxZEaF5rL7pEK83p/yama9O60vkbLM7kJuFaiJ6+tHRvPnFLjRXmzRQkxFaSdKs5Dd3WPVYyyMcXDn2Pu77cZEWbiUps7oqfPWWGBQro0N+FrEWZ/DOe7Yl//uUZPSAA5teQ0vcGTA0YOiZa7gSD6GDAi74Y8jyg+HVvuBIOpQ9ooPEIuRFd23+3+ux7I72dtH9eb+vorALBgDgGGAgDqYGiI5hVBdH30/uYngQRgpgLmA0BIYCRNxg6AkmcymLlnsT3jCq2qiIZ7FFYzmzFXX7C39DpfGJbSb2Z/l34suCRyvFwZXnEVUizhOewMk1PnpCuRQzzRATyxrlEfhklMU5Un3WHqzcd0U2pm5JoGOvrq4xgJU3U7jNkJpMAvhwTJQOwQl+Z7DVLlIPNGygmRvC7hohABxA0kOI7G62v77epP6n0f+ub/2+rb+OtHoAIMgoYPA+YjjUcycYbEg4aSGGSCwScHAcJgDYA0YA6FcGCQgE6bLTHpDAGiBXw92igvVSfMvISzwiWqgOGOb5HmIMyQYnc7x26blaVy5SLk0yzXJHQ7I9jHsbrDqHSdS8ypoZpJxnNv7xEJZSGIp1bZVZPhlTD9Q3VbJXF6AQCJqqmhlRYwNgAlDo//uUZPGJw2YvQYvcGTIzwPiCa7gSD4l3BQ9gaEjrg+GFr2RAU1wgWlYXeJZItiy62XbU73jEOK2q0v1N9GgLZjQWv7f7vR/+r7uoSFFMBcAQeCIMqMMQxbgwz9aScMV4G0MWRioEGd5MC6OpZAadwhBtwTpoW7LIZ18i3b/hF45zNZmhg7a6TY4xH/zSTIIlOn6nstc9tP2fQi3y/PdP4qa/+cmrEX6kWYuj/FdCoG9XHkcJjgK9hlpRRsgAX/a0kaaXCZp+dcPoBhlWtAqGRWCcTo6gylzn08fdKzlV6F2e+iz7ae/v+dZ/oZq6rO/7UR4AZMA5ATzCNBIcwUEGSNEBLMTCDwJYyAJS3xoeun+x0YaA6AZHUxyBofll25+q+p/DOR87vykrffI8z3aZIXxCRY0IiFMjrzPKS1H6pWMQ6ZXqMW2lkNYT5ytShoeRMrc7IxIczjqynXM4DNQwGygNKw02JCgr6OfVEBC901P64w26ZeR1/vlrnJphkZEa//uUZPOIA+xaQLO/GMI0QKipa7gQDiUzBE9wZMjUA6IVrphQW2sbFVXSneRCb5Rj9n+3V9akfd1JXtqd/W6zYBf63P6AtzC4KhiCJuDI/Z4OctGJ4DKYQhYYBBmYi2uZuigAghUGSpMLgEZqKARbeDDBnYwBLSNqrn0ttLSg2Jp7kdofOIUM3I7G8zTfQsdCnrvJDlMiddjvlFUluS3KGjGvSN+Ldi2NVUkNvs7b7nBvlp7sWuUlMeDz8yJQQUkjT5Gz4GAtmTWGA0D19NlJa2dVeaqtVe9zJ08Lio0YlSK+vZ9TUturf/0MVijUJUwfQli0fTT/1fIqVOYFACZllk2mH4SIaj/65g6BIGBQWg0DzCK9zPgOgcDKkkJgWBWMU+F3erO6K5auquQnhq0yyzhkjkxkhJahsVVieZYMj8oIVPB6lf0cToZZNQkwxqe6E0tJLKagn+MR41BNwzyfzSfYzSIZELIkhPD9xdIiiLejmgXtJLKDcMgWdMEYDXTX//uUZPWLA9ZPwAP8GWI0wOimZ5gSDwl3AK90ZNjug6HJruRITAwGFM5YQZkRlFf5IjS1rY6JGsMmRqrw4JGC93UoUQoj+b/75FVn2/qqR7271/QtVWPlUAgwEQRTIGHLMNcck0T8CjDJCvMGDQwiMTMlFO7AJEt7GnhxGg8qS04Jhk0zMoZtvqv/OPS+E5Ull0/qEsKSqMmYQvKuZnsELvMFh6ixncUTLkYJhqTVlq0Kv/+umExHFK8kySUi5GO0i45fSuSh8IxKbYPMAgQKyLLZeY8OjX1OFYsMDwBQUXegJJggH5aHc5aK4ucgMVbQOQPT0ra3/3EPR+3fsb0dWiHLf+/zNQoA+BApjDhQ6MK4hI7ztszDEDUMhjUwIBjOTUPvDEwCAl8sxMRAaiHwxax0RLE4IkyvLdr9TsN7akQ4pFZt10SUxRRH2dRZd/ndQtBmv64PdCEHESnOn5tahdfS3pkMmuZUoRdZbMzBmhSEz1aUJFmhgmpQ/TwkANUs//uUZPGMA+RdQAPdGWA7oOhhb7kSDtlvBC9wZMjmA+IlnphQzbgQ7SjUUOWFVFg8XvDiSg8BIZUE9N7fuowumL9zTE/V7/875zbmP+n+R28+2/alzwuAMFgVzFbKfLAip4HvkGFqByZABxQKjH3oNjCkODyvU9jAgHmHBZm9HP0irFMs0LGJjhvZAzFfFmy7ISJqpWkvxJtb5N70i5u/ELhtnxQUOd+vyqRdBrI8+FemZxZmtpWP6+fGzmjJf2h7PZ9DKZDVDtzjAzMYwMzPXy4IiPVY+YpMs2j52fqjHuqMNx73gCjNXadW+aX939CPRv69n8Xv0u/2qgAQipqqtrXMXwAkwHQMjpeL5MPQAMIKZgoIGD8sZfD5cJYV+isJycmoR5TblS4JzXUnX2hQ+x5tdTyZc5Lirn6+sz+Q0b57n5mjo5Ekh0eZkefvqbRT+MbmRFxJ545zr2Z1FYjZCSMaUG61DONtHw9hPMAtLIMd0MQNPwjOJiNSyaYKhYNA//uUZOsMg7ZcwAPcGTIzAgiWZ6IUDlV7BC9wZMDLA6HFruRAHNYqAJr49qrZBle9obDi0que1rqR33f/94C//bqWn93/ci7UIBgBAA2FQHgwgUOkMBtA7DTzw3Ewj0A4MUgPMBQuMdYeN6QHWFZkQACYKAk+uH75lzss/dUlMMbgkHlNTUvGS1E4zC0IyNjM1Ncp1BVOQ6lAE3VCqUw5McZNadCtsavhqRqZJh+z4RXN1zNYCTw9rQiUKKJvMIzHvTDZyZCpkJJj5zx3Y0SEZpjkpxWIfcxaAhiQpa0IZFfaXLCWpFzAqeijAZbXqvQlGvk+LWlmttiZMWlWzd71OivfmdzJKY+Rp91Z1i9LqjARAYMBoFEwNysTDuE/PXpyoxOAIzIYEBw8M6ls8eCB4AyNeBhAByb+YZ/+E7r9EsM+kXSpVCYyLnYp+RrDzsMy3uGXKnq+wv1kbIrn+fnCnd5ezyfLm6nULWlUL/vlO67SkRUyMv/kR6V7mmfO8MOO//uUZPEAw5BWQkvcGTI34hhha6IWELGA/i/0ZYkDg6FFruBANIiQAjXPsn6aRySxzhKQjAoCV3RVG8iBqfQeq51qWY0aPRYSUxCvzHzn9RVOrY3fdX9Ox9//7t7BhFEzJPejDyCyMMEVI5AY1zCFBSMCQBNJIwMRnTDVAqAwCqYU6PAfz6WPkCPh3Y9vkMvZDdbJRXCNOuRF+mOjHNqN+0yyt19zPy27grGMr33ufOParHPZ3bDw9LTbpemXJsvyZdh7lbeZvebFgtwvSjmynk7KTxJcEdAz3oswFAxnskGQfTOGpSY8OvRtGDuWpQPXuytGv2odGfR/fzH9fs6qv6OjXQAgAYSGqJh7hzGBsPSbOGnRgsBVmAKBsOgYmAAUaYMgHhd14kpQoAdDokxUS4bU+IjGMyqE2KiKhGPbdXV2MkpNs4dM1RTnHqRioc77EYhcUC1xVhNtO8jR1jh8dp7yATqjaV0BtXeciQIefcmwuAfnuaNHkDdanAKADhKq//uUZOaMg55gwIPcGWI1wOh1a6YUDkF3BEx4YUjQg+HVrphQmzbmE0mbunFgDVVGligZoGV8IZ6o9D+ciMhdImljb9tFdky11X0tumE/RyimITpdtl6dqNbdr3wKb21F5SQBQwdAYTBWHsMmDWkwEgngKJU9zKXtO0ApqLQ1hAEQYEHw/mFDRKC+7/T3xTNYh67FCSzCf0SURFTpeQKT4c28/eO2pjldTI0PrRCU9XqepqVyYvKZ7GVL03C58vk3YnCXM0OOzKDU6dKKGSsHhbdWXGFgclgUDky7zCwAljw4KbK6WL+ovpZWdY75WbHjY59sM9HqnrFu2Zfdsd01PTfci0p1fdd0U+x5LtVBoQgHmC+AWYJY2pxF4XGGeEOYsFxhsQkUEA9hGg8s1UwKI8KvX/w53c5zCAwdtr7WMcueReEnU6VeseM3g+8SFVI9S473PhJQRaPtced+5A6G9PSHHpPnFc7ltfKm8XPpmHJKvzJck0MkI2SEX9ORx6vA//uUZOyLs74vwCseKFJAoghVa6cWDjFlAA9wZMjuiCFB3AiYGlwglrvEtQzLM8zj4xLAdKxl5dwoDEHHHCXTZTdGao+MK32EmLxzI+tvLOf6a0S/p0RV9mLue+Z5tPnit3VEcx7kBf9UhgYA1mCUHccwjtphyAjGJwaAAIYsqBtoTltnBeNF6fI4ZLBPYnwGpfJyZOUM81KsxdfNGLmTc5MvB9v9a0jPyPNMj9oXfIz0fIpn59O39iPLq0jz+v/b6J5XWVOEL270cG9DnlNV1MlzemiKcAh5giYKFxM91CQGkDQGVAVsz0Ud33V7czDDzDEGVIrUt9qU1b71/37f+j6u7/o96gACkqaqmWpnGC2HbnnEabkYXICIkB4XSBArwNAPTOhM0LAK0ipWTYX+bW1SJbCII+4xam8HjmbsmuZ1sdZpFKq5wG0NmP5+Zw1YguRak7n54xLiNif0JF2Q+HpJeqhFnl0MxN1t1Qub7F6Z0w9Jo2W6H/2KveZlYC6P//uUZOeM06ZewAPcGWI/wghRa6UUDU1ZBC9wZMjWCiGFnpRQsRREhaRZeYkDBk4uQwPevIKpENb7IipKXOchttW9bbPH8km9H7bl67Kq7idLv1E++wW2BCyqqlvgMA6YMoHZhVAYnoUAkEEchyLMOBIyPpDh4ZW06K8gMI4tz9/bvdnv1AejEM5lNnhM7C4pL5kiOLa0oKXa6k1c8M4WB1SZWnldzPtGprrUDR41IoDfpqosnbkIhBF+0J0co5ECQTOv6liA31FydCeItJQWDfanZTxxKWl0gFKDEhO6FaMMABV9CBGCREC8Vw3OlyOfz8GWtWwGyT2RTqFvi3n7vs81F3rX6Y8wJzL9KbvYRIL6VQCBKKiRIKBjRD82mWOT89swyQJgEB6gKMDkEAHEBJ0u0z0v9XmhGwX7ZmqFPdP0+FkppnMzlyqyZgmmiZRYnXPI1KUvLzfp01pOdvIz5q+8IqxXbeqT5n/0vVAQUboGoeRUq5yEWDF0sAJAAgBR//uUZOsIg5haQkteGMI6IXhQZ6UUD7FrAy9wZYj1CCGVnoxQmAjHDC/RyLAZhWADOYk25MD1LaYMZ6K2P2Ld2bV0fY36tqv716kfp+MevRm0ysL3/T0+1IWlkUuUWhMCEIwwVBBTiCbiML4EUw2CAUCTHsVNwhQvy/UJIiDAt/HLHmHzn6M4isuZp9CVMOMPSMjDgjWeepgzMw7VSQ6f5RqUorDUjuWDw0g0Iy8vQX602NYZqcL/Mo7SOwIqSSLaUvoh+lSJ9UrseZRCOnsYMy8RoRoCgxFasqekuaVBObaySKL7TJCNZy0F9Fupu3UZureRT1+23/dRoZW2XSf/7P/kHf/SAQJJlFaS9QFGACDiYCYhRpfulmCiCEYICaxwTbTBoWL8tOhgt/0MJD2WCfZfZC8nI4ZLBUkKKn91BTkIi0uTYbP982M17u5J68B54I0xJsKyOk8JTpzpkedTMjylh0yRfe1+pluOZDWErvS1OkWJTvwTjxHlbgAAQLHD//uUZOUIg1ZKwbN+GMA2wXhlZ6UWD5GBBS9wZYjDiCJxzAiILPhAgQ+GBkTggAHxqEoNxgS5QLvsrd/b9AVbrMV76/p/q/dr/b10aNNFSE0XpfPbVgEFwACQBcwSAWjARGlMR62owGQgRALACKTHXZOAB5pcML7MFgKXp2wIGA6kDd93TDEhFEJiMilFJjRZrIZVTZLU3M/JkaSiqdaAlNwimXBZi4uIJmLvxCYpdCIhTnM469azquqxHi0a0aC1PjCySEcLM7k6B3t4OxKkgsAFqajoYMVMzmv8MEgNrD7gU0FkTaWq3XLyFabr7Kb996j/TQt/m//6lM9CUO5CypnYh1KaAANAVIZWSMAGGBWAIYGAkRo6QkmDMCwYDBtIY4QoeFXalc6TCCiu7z1hYznv+HK+/766KZo8pKDpw+7EayP96rAzmXhACXEBAbqzHHMuzSDn625oZXmWeud2oJlU9tqal+VgdEj0juELNtmmbgiUVpA5q7sH4gEITOdE//uUZOuAo7FewcvcGTI1woh4Z6IUD3ltAC9wZMjPg6HhnkhIGJgYHPUNmDgBKXw4w0jDczuV/cNiNmLLJZCUqAL9uMxtBJfdhVn1oe9u5yBZNVR/FGzOro5YwmqzHZshAAaIFwCzAGA3MFcQs4ZX3jCzBTMSBIBB8ydADhAPFgkv1W4LgGmEzixZWOqjbSLCqRUClNusfB+mgpKwuxwUgwJ6pT+MNPPDUAO3mS4/bny4Bzju6HiFvQE+y4RrZ6j/EmIC8dr07kvnEemle1cC3mEEtgYzePLzv1UhIW1IPOA6QOaVzRMntSVTOf32S9ru2iK2atCkK0dEUSubkJRvaP6+b2a1Maj+h/RV7N50VAQAQYKgQhyJJXmGAA4YlAJgQEAy4GNBChKazFw4FUdn8cu4cov85TPZqsm7kRAiJFpqXMtp5sTYHo6PTNZZkmbtXVoU9ilrnlS7m5laqGRX05YZHqX6Nz3ZfKsdfQmLypseZlAiFPoMjipwhHe1UufI//uUZOuIg69VwUvcGWJAIPhVZ7gSDiyRAk9wZMjnBiFFjsxI2mH4nkQYkQ4q1uoShFBtNVQO57Oiv16F326Eet/ZtT4ZZWWz22wxWK2Vi9vn6WPmKEogHqd29VztoxDUOEjBSgHlZ8wknDRUQ8ECYA4BhgOkMGHKAyuJ+HxDAQKL+OQIT0gbOIM9aIUs0RoyV0ev0jgfKIU4Il6PZizhEJpMJSna8QUL6ii2X2Urax8JVQjGMl9KePm0ilGz2bMtTVsgm9ByG/kdjHTnEI1JVlCA1FnBjliMigtLRilQCg49L8WEJtZcSgYnmtA/VNm6dB23vMMsQ0H3b36BRdvV6B3RV036v9j9r/fsbV6KoooBAAF4jAgEwU3NuhzqQMuMPQBUaBaMBAAAwSwug4jNtKFqiT9bndoBm9QuUyQjIiq/wn43WPO3bpZ1jSm6dTbvC31szpSa3JyVflDwA8Yb8T8OP7/6YOe/iTiG1e7SWfrHP/uLJLSk3PREwAAgQCNR//uUZOiMg5NWQQvcGWI84phQY6cWDxGC/g34YwjiimHZjohQEvIFgnOGmGCApadJlhitKYN2vQVvfaP/nFXa+yj3z1/qqv10ym/HNf6/2293f9VnsWADKSzVKoCW6AQEhgbhJGwKgCYO4DQGEWVGPuJ0YEXxa83EMLpelyc6PorG2/O97S9j30Qivg9KaF5kJapm5KeZHqa6rYjHqcpP0NbiI7usC4tjh4L0dMLoiR5pTsHc59sT3z4X1ytZM5h1tW3iRIRGjMBMqEYsOa60tk7M0MgFEpGpGdKbp69R1prdOPPH0ftvf0/KpV3L6Uf/3Ee/RQPWwCPZrqPrEmSqqoSgYAKYE4DxgZB8msE10YLQIZhEMAwHmD6OZTCydMPNJBwYh0UurwOz50mIKX7mEJ0NxkzwamzqZmelR1cgxFTeQ2lwZoHhfKZkWUIJ+h6FrWhzyTV7SLKOHPG8yp74JXQj7Z5MFOUmd9yhktIGDQWqTRVGGq9rcFNAAACMJqq6//uUZOYAg1I2wKt+GMI3IqhodwUiDbDVCS9sZMjkiCHZwwkIrR2XkFzFEwWSvNYJN9m2RpqQt/JRXt2N/r77DHO/9wn8UQny/sS+xSu71gABAwAMAIVBxiQ8mDcoZpdepgOg/BQBtL8wFCATCoAgEgAWLLkCgAd6/qvvLHsgwxwAZTUnLQWeCJHCAKPvVNj/D4QLi3dVNxxLJ10IgZjMTCmelnTNCpEVHjuHcebZ2IcSTKMRk8FlGzBtKcmdJCZwaezIcL4RF2u2jIdbxl5O+zn0BEMKq1ZIoaMCg19u0Ejt0hI1bg5yg3V9TdU1/xriRRiIt7nbdn2fT+Ae3xapCPo1K/+c1QAQDGU/dXRJsZACxgCPJq32xgOC4MAFuzGURuJI71tKK/NaeJ57Ss+7nbdDjrsqIryavV0ux5DuiPY1T27y9zaL6Kkq1u8tyUdkonclUdTorjmnIFe8JMASjSySmj21lcWhoAAkRCQTCnR0BvTtd0DAu4j/q2lAEHZR//uUZPAAg+tdQTPcGTIxgLisY48QEGlw/w54Y8jPCiHlzBSIHZk29dW/gxhHbQ/u+AOi1b7037P/7fXqpu3+tiYAACMC8EkwihRDk2qPMNYEwxaDyyhlhaHZhGWadFsYCDl7dAzSFASdIKlvNZP5rJLt0X0Zu4ZrutGNTZ81++p63yTkbQPa9tffWbmVrzORmEtyr01zfV/hea8dBLu2KNb9p+9fYu5T6Kc6dPffeNPznGCZj/WRLvaaLLK6OvT6e5mxiKoXMoQIwivZxY4AQfSgYsLG3liiiTIZUC9LbdOo0BV/aXq07yHSMOjX7Vf8khPi7O3a67N3IrRov6VqAAKFACQZankGAUjQU5sypADQm5MWIDjAvo2YFWdFYkkTomOm+KvQhE8K10y8c9mrlDn2f/wQZkv9Jnid3M93/TT1JxDWpCOAhYTpSuOcpplVt+7rNYWXlO6bWcnw3OU78aGXPzGXc6Zm2zuS2RJWlANHepRDwxrgg8YB5fr8mPfM//uUZOsAgxRHQ+O6ETAzAoh5Y4IUEPWG/A9wxMjdCCHljghQbQ761oMxdd+Gah5yza1K1PuYqh1aJycSwT9swtFVvCtKmVV2RJpX+9aAkIkUW1MBECgwVAxTduNNMLQBsMA6BQBhgTiJGASAC1p/XaJgDrUtaGba3+nNphQzP3Tbyvv5mExHu50TXhbG/MM7maFTM0tO5kTJa7nws7iEddlIn45ZHubGHyiIe5Qpe+mfXhmZ+5dKXO+e9ITDnErJ45HJq9hyj2prq0UvwAtObDAiAL3TiTzzmxHd+K219dDOnaUb7t138bZ15O5/u+nrVPN/43uHVQAAi992WVEAUweo8RIUwpApMV4gCEBMH0VtVZzEjLV1b2eGDeABc2ZckBahR46nvYwkLIGsOzTIpuRE0BWlXSy0MoVlt+5DnPbYSvVacGhAdWAIQKKsrSx5O8hHhtmoA4BQDUMMlJTtqZfztrPgrnq1DEdaLxHorlZNjv8nb6p3ysV+k/l7MW7V//uUZO6IA1tZQUvbGTI/IghAdSJCDwVdAK8YbYjCBiKY/ghQdn/SUgIe6mApQ/2fFOua17rj+M9td3+teFuos+zny/vrLGyhpuqZBVF84eUgwIA1kUAiE43IFSB3SRkaCtRa0mujIivalmGqshHOTUMK2gVt0DnRgq5iH3JWxgoKHWH0JRLmYUUu88adFxZwkvLTSmtW2w8vUrZIAQ2Cfire+jpt4hYdtgGL4l89EFzVTbpMK6ZbCQqFz4sVS2n6XY/NF6T5H0pmZPhZkZSefMuskhvIdJ2x8prOUsz/o/gzmIy7GISs0YAvmRgCslwp1Vy89rH2tpUBjzqGFglADgYbQTRHJguAxwWam0LgMj314+bU/TX7wque01BDZxxfxaZdEdj5+cPcvnf/OWU9TL3LOw+IKPPoO/V47+LZLXmO3rqfi2YhDlehI/xH4gCASUwUvsYDY0pmERgA8coCsMWJRW1VdjZ8tN9Yyl9FFOeVFMJPz1RTThrDjoRB3Pwg//uURPKIAosPwsNdEMBZAqhJcwImSmxlCK7gpEF6IiBBxY0xO/3fPVl3IpQNdw7hLjl6LtkAmHT8t5etUTdjENv7ajTLn3Ux9PhpO5MIC+ASHYE0JD15rxVBTQWosG0RT3U2BKKi0SqOnmpeIgF3FTK/tE9DZxc6S3cqfpbf2/efN9DoGPYKnzZjwx7d/po/2nRKkpVrvz4afqZt+4XGaPHhq1KrYhxAJsxJUGdyiVlUVp8FoAPTah3pkUmTWIiqhcVWcJnyLoLrBxrLUoMWKsFRaVbNtM7DDNR5dtrc8tfQ8aQAqXzyUkCB8UF0qgKf/VlTUBGCDTfXU2lNOKB0iGBXiRHZnZIYljOuGUIMRcMagQsoTbEVRMvJJdbZPKNci7ucQqx5MzYWNS3KO8yIlJsqXJTlJqXMuqfrJenI9dsvdU+ZfnLqqKPXpXUX5P3cXQCkwOOh2YiH7ZokFysPGPvOLgs484/LKetM8xo6mw69hm1HyKGewfMp+QCZkhEd//uUZPIIgq8lwakcGDJZwpgld0MmSrhDAgfxAokvCmGk/hRQ4ucK7ak7xZHs9VkI1iZrFzRUwqGSmvTznDKUyTrQFEgaRpWrS4UBqaoVqZXIs+SmlKCaBBoghrrSAIjdpDqMWETOJjyFS9snXSlWmyuWauxT4srPDq0+P2pQK4V1p2+lrE0WWudSu5ruoULLQRT6PBWZG07aNKwKFywt0UIzpUF5UBEdYlnmdulAZOVG6+6zMjlFl1x/4u2aby+F9unaFKsyf+mfB5oP4atnbHvP6mEfvtN+76vnpafGF+4liLMm/xqnoB0CiIcGRu2gkgOYRee/G1VdrboVfniP1AZaI7a6qGm9jaWIqG7sRZiyllKq9F93lKtn2VvmRfTk3MebTPzyt9FNGLYjBMhnzZjNSP+Ue60y9MmO+fDJJa5mGb1gkqXM42VfUAACTNVT+m4jgCNRR1JCN1BFgbSBLAvwGXNspEfcfhP0pPJSLTuyUuf1c+N6p8PPNKfdkth6//uURPcAgwhaQauFGnJl6OgAcMNoRvgXFSDuIGFTiWDVtIkJeefvgsuVqp8lJTLZk+nt+ly9PxMgpbywSDwrXXUZJLnBQuw/mE6lbBASETe9oDAe/EvTsc97aBmpr5pMwmcu3FnMCIkhGVF9oI0O9hTmsHxLiidrZ3G37vCJ5T/+5SmbXSj3vXRsT2v+jXd9taNl3WM1Go7+sr7Lk/AysfRGohJ9p0PAaFTutYDgu6kaJCoX5EzrIpUid1VaZwuA2mpT0FyGK/BCDueffkm5zbu1OANj69Lr0pbyOpfwR9PX7rJ0dLbif/lXfyuclsP3x7DJia6q9rk9dIfPYjUBBCdsN8j4c4gY8FHnkDclvmOUCceO2nBFe2aYLOu6JSFC/mtfr7ZZX9s7Xfy+5qa5/XaBrlmlp8hVCi/VeZcIrSNSZjTCmV5t0kMqrsGyofffkXJ9/dDt7p7Cf+84AAQgDanUTwAAD5TdFhQzdyAr4NJ1t5QnEtD5pnENnpcP1mO///uUZPsPgyVXwAOIG3JbCUhZcwMiCxxlAg4saQlrjOCVxI0JWM5kr4972/+9d88cm/kTkJhbek3rHJH9jW9f+9TMtY/zmMbT3ObGRp9eQz+Lut+euSuZ/AAIUD3C4FUCPnaEWpD2u1dWob8FoTW179suQKHgvbBy4doqzkg2cpuz5rsnkU6kPrmVl6fOlyHpI18/45aavhJqa0jOv3OtNnbu5S4yRBAOhWN4XUMbF8tYB7JpAMZ5cCER+EaFAqc2mCpCN1LrUD5vczsGpPnzP4+ZTLjaTmZia5Q8sfDbaaZv/y1//EWv1v/FIr0dmfvTTM/XqwTbbw5+fDS2V5FdO+dQdyT789bzuszqtUq7gsEn7DIsAxalY21JVYCvu6owtd0uqjkVlOSFPKeKRAzbwqmcSUm07g8s1X6hGrwlqGOf/XBZShXjP15i2phF+WpKvl2CsX9kQ77Ys6r47l7Rf+5Dynf0EkWaqyss6R5OSEBEmRWRVJEoSdBXYr4duc6+//uUZO8Aou1CQKuIGmJYopgocyMmS3E5BwDsYMlakGCVzAyZzH6GR/f9UKi3NOBP/s2V7Ccc5L+/kvP/MTo94n7elVmz2M9TTve936SFYj/ux9E50xgkGmspf9E3WWSOhj+m1dHLQOjeqI1wuj5Poj6WUOE/DsMecV0gst1FBBX2JX90Bb9yd59pvr+4Y6KorXgzebL1Cv0vhTHUKXeqi/v3937mq0L133SAAEgslmmS1EYMOerlCqW05IE5UUzAWjFaSDUnXUvIMJMoNzE7m1hR3lBilqii0yRcOzkzB0esKLD7MtXGAKEZdbRU6yaRYx0Rgg0Walp24jWgWte5K3jqADAAg9YsITbNbJ2bmMFwecQ7lo30dUyec/Sw3+ghQgLJKISLlI2KtUQ8isdpaeFX3SfQyLMefJtCjZ8jOMEUi94EPLFTAaIHxiEIjA6yVTKShAJTh0uT1gqPiIDowPqhndcM7MVNnXLJLK6kozLLmpYI9drIo75i7soxEhwi//uUZOiLAs8twINlGnJSwPhWcwMiSdgnBK0wRElcj+Dhwo0oIdCOxD5UDyXk+Oc1XQZ+Hl9Jf7f7bNbtNZ892qPfbztzpkPODqvafzeIBBgVn8GtqKG5rEK/s/izWDZ1zIJvke6Dbuiq8BxzsQMZwGMVeFlxO8znzXIm9snMmM4lTt4adItLPuRnoTyDOxxZHu7Q4hF5kLvMrvDJi7dnlzpWH/Ac6nwJhE0/kppmSLnFA8ZHlqVEToFVEmUQRATvRzgjSM+4kzIr28RiOn3I983bbP3tPcyLHE3X/O7w+chwGSm8EKDmt5z/fa1WOMsI7SC7zPRRToig+i6rtL/zchgGlTVLTF7CnaCmmcCgNPpKcJ0wpojYR71FkkF8i0BkmeZGFuXCeL4M3YqRKrKiucctPrn1NEosiEL5RXOpmdy/BisHtL1QoDmGIiW3Zywu857AxsERzJ53TffU7NKMz37VAZj+q8CioKabfK7llskBKW/v4rikTbhxMJAjI27F//uUZO0A8m4YwcMhGlBZ48gQcWNMS+VhBsyUacloGOBBwo0hG/mB/ODQ3qj7ui3W6XIVbXsVGYrnTQt9mWW/KpmTqjvL5WQlX63e7oO8DNW1pgkOFKllrQzLIBRx+8H2hwDxXyv8UdkzwMEFFhGq4CvfHFiUdcdoepw5dV+/c+vNuWF2c4RWTk3YvnZmZ8558Onvn588QtYvx3at8OXoMMoJsq8hr4Fzr+wOvRO0E/3d2s6R9kBB5V2LAD9xgZBFX5cuRf6dmKYvt+AxTSuWUO8axQlnOqW6XzZkUlSvVHdCOejp9Jxzu1zP4H/VuSc/o3X+Rqt5aCCT4UpY3b33blnYHW5jvYwI3PJVAACBEOdfF9j3i3zO4Khy5karptmoISWZ3gck9Ft4apyncsKxEMyXFb/nJInrSeWd7w5XZK/OHRVvLVUeRN9X/295Dxl+jfEj5NLC/q5oUlBPyXMyU/oD12/tdr52qQq+c7LlxLK3UPMuA4cyajzptQKR1O6y//uUZOqAgtgrQANxGdJUybg1bCJuCujxBq0gaYlgmOBBsZWpnblH+mtsufyYCgkY83lH25KI4MxI/1/f/qecSznO+IV/ZB59ZP3Y5+ru/fMfJrL3+5JL7fcZQo6eJdDtb2ytWoNDhWc+cKA64hsDG7TvocPSr/cqs2sgSfjpgV50E3mCHi1caU4mt6i79gesUFL0G87QiV8AW3xEp+/d/v7EXEOg2dPGy6bh9QBAtWarJQX2lADJ7KS7HmsHKlLoc7vOqkVes0KW8WliLc64xtXdWkZbtq60f2Kh1yC117J0d0eLFjFR/Kf6JPtxRrJjjOAOoi46zFqm/S9v5hbd+UcA1QQABu5KlZDdxZq7psqtHeBFbhZtLC2tv3QgjpvSJxJ3cfbTtpmin7Tp2KcL+/mbwypZL3f9W8LSNCK3Moae450zdjP51dSs1MEiuvnQlaO3o5tynGtze/k6uY53mdmwqAEKPeUNSKVHfn6op2+nbPDGGA8+lpWycRnon3jW//uUZOiIgq8awUNLGTJTQ4g1bGVMSoB/Ag0UaUlYnCBRs5UJl9SFYLzBHxL/c5IWrVKGXN1eHbeO5lmbNCUxvEGdKJnySwtjRvIvcHbDjKmS/djmb7bX/IzCv5htK819lP03ZMyATW6FsYUqles4YajZBd7pZlH7MkoWOW5Z47WilZAnZ2xnJDZQ6GkPffQ8dWma7Kbw4TOiIug69AnFjqEyQRaZ4xZqFDqg2UMAJCp8odsQAQAGC2kdTuUnFe0U9OXNkocLuRkGtfGd/TK7p2yWKqzlsksaSV5mQFEOdlY6UemkVkJFa38iul8l4DWAAAW8pYnYaiu0ynqDg5aslDoSNxvKfQfE0cg3UbD68rk9e7D7vKdQ1E80vN2OnktMdzQp9vveySq3P+/YQdkv7f3Xlv3uMLiLnI2M3SmZOKK36wmGimlqM9Rp+5F0zG5iRJaN1iDmxmxpE854XrLnMNQU9WBEH8IuYehugiJVCDJaF0lWC+vRHRUz84Z+hxxl//uUZOyDAv1cQCtIGnJeiogoD2MGSdh1BKyUSYDqg6EUPTAIU26WftW9UW50pfQavbc5Fqmp/Js+531XR2Wou5DPotq3Mz7NrZljv5ffTWPd3wAFSQm7yPyynae7Aax/w03c4p517FwlDPf3cAQiJm+iQgWUmuxKZDTX0SlZkyuJVvus8E57mUUuAkIWsdHJOGDWIzyEjtllt/Q/mkN5iuGy2tBqYs9bA6e9Puf3RnEFzQ8JTkQMShB8zIjUS/tIJEr1jKuGMWAEEUiKzO5AZj6U5FC2NithqdXosO/T1P7P8d51ORUrob09fHmdESIaVsM4RvLIu7VAEU1lZbeMlpB+mEX9Ps1KiYDu+ES2oPb6Il0JCtekyBp2scHBCTgKQvUYlM4Z03ZiuF3N4tUigyCxfvRvwJpxTCmyMgjSaFXVWfE5bueJfQjhoFimClzY94UuGbQilCNu6E/hAy643BJBHzQAmsq1Ld5uMHBjIHYQlqyV9vkv3Ps7baRjyJHE//uUZPaPA9liPqtJMnBRCPgQbWUmTQmK/g0ga8jBg6HkHJQIXE6j6ppMXaa1rAbA4lIdNLPgU+KBaOecXkp1aaupZ9BKG61UCjbq3oAihAABjnSPscsa+e/X230ZLgllZVSgcPzpAjqoNTJpEji8tG8FFB061yNva5ZzoTwVMqFCPvU4UOk2ZmIm2syIiGuRob7tPBO0p/ARoaFd8zUEsKPmgIa1lNGhPnYPR1AFeFm9Mk+24AABnnhFjw0Gz2OwTB3FK91NzUL8SkJhqf1nm3XXhiLkIRwcy3I8JmUlZ9GTsjkyJq9Ka1uZ2qaW7Hz2dmfdkl1ih7Tva8q/asbQfSUgPP+Be3V4xxh0yucpminlFLbVDXtq9DFg6AjTDs7JDSQZWMzyCjlIDZtk5Sk1NChRI1ydFLJC52mdm9q6P1R1dRTkcMjIohEpy3hO+pnuZPbtrzu4/CM6YgdtXDUX9x+lGNy8v5nltABNG0szOyZdA0AkqpMxaVTZ9aPPtR+H//uUZPIAA11dwUsoGmJGoqhpZOMoDMFdAK0kaQlcoyBVpAk5opEcpKq5dUjbL6vWg6yyCj90jKrgMkS4KZUQuOkQ48gtbmZJa1Dn3EjyxSIYUQuhoccpK0Qx57O+s9kNwUAcfatc1bmNQ2vxMmYqQiOG6CPBn+yk7C9scTIT5wkS1yPpm0PCA7S1FF87w8AcUutY6sLNZKjdP4OBLJTa3f2ljqsfv5qEDCL749bllh3Iak2CAAP1ecI44R5smDh74kZZqGZ4HSbONLxuHj3whnpR7Q5fqP8QjqqWS5uxHGKmTeDyz3JY680yIvy9VyxIpy3XLS0DnB0+GhacMkHIotBU5In4BaAE0JeiBICANfjWbGakUusohOzjw9Di96Ce9bZxmZkfDaso0gBcG6PRsK65uU4Y//ilyt4Idr9CnKG5WpPlYDn3OfAv9lvfU/KYftr46xaUVtjMxBZLBIs3EAgEQGudxpgWqRZQkMeHFfKuYK+SKR40ji1WxC2Wm/6h//uUZOgOAudOQAMvGTJHh1hpUCKeC9kNACygaYlenSBVpIywabBA/SgtgTWIIbLLAtyhWI1FQnTIxSiKj0vWuMFSTrEBNMuMvGgvYfMFzrEUtKhr2oABgAAVb5ViA4e5cAcfEF2tL1NiVmYytg1afwb4gJoXUINq/An+O+z+/kK2Sot93ZfqXVu+/WnXydQqlKl5JQI8Xeoayfg+wYvCm9n3zOTbX0PAGWV5VAI/SQW9gxNsqDHQpAlDmJbAo2mgtNtZLvRJHZMy6dGbcvKwpT/5veCu1XrfFiLIkhp8iJQ0QvL2AAzJzKWiZyFta9te4RLWlR++JHxd4Vas/SArBaq1ypTHOEj5nEoexyglq3zvjl2RvvpErNvMi88QhfzppGtAiVl5ubgqqOXSFKwTyqQUMK2I14yCEvSpQuAzU0NVWQ2hqeRlmeYJXMPx0FrdIVBX61mEsL8F9NO2d6/JT/c+8cqA87jE5o0jlzbwaRNOJCLxmrSTSOTN2B39sHb3//uUROiAQqckwTMHElJRA/g2YMMiCkx/BSwgRYlkEyAFhJiYmzy81ygvN5rVrsxfvdYrfNFvL6sP+7SvfBUtXT7fH53vLLHr6MrRVyzT7AsRbt3e7JX7o+L57vDVWutWz+v9cK1pmIBzSiIzf0oB79xRHtlSXdQEK8+lCMLL6XKJNnHCIJCgyDAqEZFx5N4FQPSl4HadalbI6crIoZ0UM0eS7UAFPH9p6sAEIFRWtFSUPzLwqrmA8kaYqTcpUoDqt0O42hIe2SNc5LHMUKOUfFTabo2lFjHl0KWiwv9SNkPxdttTfKUC1kw1CmGLCJEAbB1Sn6JKZV9G8PUXJmXJBTSVWzcqiSziWvMsSutkiRyu5JI0iAQCixKe1ORRw6YzNV2/+7MFXYMK+KFEwGiobEoK6eCx6DRUGisse4NeOeJQ0rgriKd8GagHxykUQEEbDuIjTmDoJfezFtddueWRNOQEgyN2EysqwQSoe62LK114zdOjh9cTw4pcLWg2Gfuf//uURO2OAxk8PwMJG2JbZEgBYYYmSLhdACgkRcEHDOFkF5QIRXtOrm4JkZesTn/WaZW5gToUtZgbfBq2Ri9mgKkDBSIN/d0g+tyW8rr+KBCQMCgwkCAxAOQQ2Wf9pNONKLMPMPLZyiyjzD6qqoldNNMr//+VVVVU0000A1VQZTTb///////ppppqqqqr/////////+000VVVVU0000VKOkBOgc8T7WkcDXDCjGt70ZDMGc916p6yfbFYdohnHWlQqUEtSz2UlLMjoEaap2P2ZEhZhJOaPt6qekjWraK/6q28duKb9LHCoiv39YspT+18qvcZDm8fk+j92s9/PxoAISlJ3Z6lIaPDhk1bLRk2PmG2J5rnmzCOF1GWZubkotDg4PmG81l4wwsQdPZ0+7////s7toUww44zs7OMOOIBzSNYITRl0yYNaDLAoQCABJTTkqVCcs+nOB41tFBetKN23ExxgVAT1irrbEveP2rEFRAXTrUhSPWBkNe7onxYLoXa//uURPiM4u4vPAHmHFBhZifxYSguSghIfEMYwAl8nB9BJiD5hahi9Q1sy5w+LFUOIkscGA+Qc6DiKQuD548XZO3KI1Ggt1jggxiAtILg84I8FrJlIpmFpEOekhwRDfcptzAgtO0HP0zh+iJuf8CJXcRQ/Cf//qSRJdG0jeknST5yZwPn/83tf8f4DxHSzv+cz+vwdx3dAQiAALPqvb9eAYrwr6Vd7wSFd2QJ6iBtC2Nc5Elpu4WqCSJ9TWlRLwLKcZcia+jNREMm03KRmaO7W9aCwe79q2PHkMlQWlLIiI5lkSHfjR1blM45lHcVZW1Nm5bDaKWTqeCBUK/wVpQAJPcXEeYBMxuRizWCuSdX2TZlygprNQ/s9J56f/kPlsj9fVlSa81x1VOnm6E+jFDyLFOUqCTxsIi5QFD5S/bTUyiJdXBkwo1jBXQIfWu+eaul5DpGe7mZ7JFRqZOimOe+UCUEfaBAAA11SBhUSoSEKtXiopM2qhNhQFF0oVRBSYK4//uURO8IAlc7rbDJE2BEAphdNGU0DKi48geYcUmbrV/k8w2hFDQp/wsnxSQ38RWR3gvwsm3fwK+/hRwLl8LFgKNOFBQTC64K4+K7/6C//xXf/+FcChvnDcn82A6vW4AX234c/toLCLenJkGo2N+wPHfKa8WMjqYYm/G31iBb3HTaRHVzcnQcoD3+YcZMjo04/r/QmQ62J8TbzfMhypw8TP3/sdz5RMnv33bm7NMU/oFYQVgoIGCBhAYOBQQMEDCAwcUEDBAwgOxZZUtlllQODBBQwMEDCBwcUMDBAwgexWoIGEeyywGED2KwMEDCF6StNN+VVX/6af/lVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVRAAALR6cKwIZO6L1jMNPL5p7pmVcFaa8UGLl50b//uURO+EQzRcv7HmG2JTIXe2DSIaSpSY/CC9AUkollAIMY0xP2qg737anoanYe9+J277bOUwmveR/usyQzbnvecO2XXOpCBEVSQp2sRsS4MzHLGK4ydegk77QqtyapK5QZDzl7Knv1a0aJp26+aXyMa8aikf4EslONh/TB0pAYb0y/k1iueqTqBoNcx1qlXS66hB0P+rUnEDCNVZiDM6ZHJfKM3IUm7vLEq0573E8Y10lsXkILeD91zTVQYHUmxW1aOF0lVKKKSfMNfGcYma6ZUYnhWypD24LRz4o9VMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVQAgAAAVr2VinDlX+ZQXl958WlXfQqauhkcuHYqbBV8oUX+sjM9jx+GWr9bTxK6Q0vqWcGl3//uURLyG8ptHv7GnG3IAAA0gAAABED1+9swwzQgAADSAAAAEJGMXjF2QzDFFORihopIwY3tJTzNnOB/FfkN7As8Ei2x4W1o5aQPlTQsh/RpfhxA3NDiYN3a4UJ0bkhCIklJNw9Lx4KljdyVPMbHgusatH3BKOBBONm5oj1n052Iv8w/9TWgh6WwheKM6tvccylH19oPQVhKNqU3Rwx9M5ekOQujnngWz4Xs5tvCD6iaeybYAfz7K2tRbd1obtmmXzIDHZsfPg3ezmYXOn1CRDDZHTuAyLw6QzO0ltDBMQU1FMy4xMDCqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqgCBAQIKoVVzAG+7bW0KCfIVnGBi5JN1xWRhjoRQx8SJKNQy71TO5FaQ4xvNXMOoaykcr//uURMiA83BWPssJG3IAAA0gAAABD/2A/Uikz8AAADSAAAAEm/19vshU3jZ5euKPyvrMohdTUwxiIDZ82Op5z4F3IzPMsG9WVbPWVO55wDzyfxeapGdyjCPQdTw0vgFOvW9MqlUoia0nxw4hD2W2ioE4wXKnjUswvKC0uWnJmMa6gMMCjfPI6ikNCnyTi0QUQLJXkvzAsJsaW2USnQXwsrZ5hibYlSUkrR+YdZ16duHW2vukW2LxGTY0jSBNCjkrgvUb12NnDeReLR/OeTkjTFsbG3KNSWhXnSr7JVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uURMeA8upVQeFlGvoAAA0gAAABEcWK8Cww0oAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUAAHNCQsXMCYhYJiyMlFR1hJ0SIomNBi0EjbInWidaJIpZEqwWB0kBSLvCpJ6iQUArAESCQFIjCYSApEFiJ2kX/27AERCQFIjUiwFFaWmmPTH+siEg9CoZGIVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uURH6P8swUuJDJMFAAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUBgABAAAgD/RrBgAIZtqjFHDUPGX+HAAieJjXwIkY+4K2f6a7XDEYgaYwQYCO04Gih8Bg0X+8BofgZ5LYGHwp+BigGAYxCYAgs/8DGgODJgMDAQMKAYTB/934GDQKJOBkUYgKBoAIOAFA///E3BdQDY+NNQbeNb//xSgGAgEFjg54XABqwZAQQ///yISsISDIBYYI0IOLgIp////mg4CmbmxFG//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpBQAACAAADSCgAAEYoE4cJxv//////////zchhEKBQEBAEBAIAwCAQAAADuTUvIyMVeJMvgCcTO2Rcs0IQNTMY7AEjQZATkxT4VnMA4BGjAowDIwU0IfAoCGYJsAnBcDHAoAkGACSctUDG6pAz8oAQAcDCAXKZFQAQMBhIKgZdCQBwVLIfCWncAoqgYeIQGOxsKoXCPkyUsEBJAxWKgMDAkAYjgHDsZEqybIDSSWoCAWAeFgMKAQDFYQABBYyxoOUM8PROmKlVYesQ8fA3BlBlC8XS6yJDkV2+IzHcJ5DpHLhAUSitjI6a6/65ElEaXhyScWi5itS1//5mianzJZ48bHUk0P+j//6SR3/MAzACUVDVmy48xkA0G/1TiwAUZd0LKGG4g1v+CgBBd5gA4DMYEcAmZJmgGmSKAsKOm6gMYAwDFITACAftCxcDCYRAOB/38pAYPAoi4nQPj//DLYyYW8EXIoQP////uUZP+ABaSBQKZ+oAAAAA0gwAAAHamBG7n6gAJ7QFzDP1AAFzjjHWLjK5IkX//9QWjiCY4yCEAHALj///yfUbEAIgUBlBwDvM////8nyfTppoETKBEBZY55P///////////kENFAADUtMBbmYyZ2NzfgEWPjBHIzhh4jNLKGML0UgxEQtTCPBFBgMxg0AKAgA4rASZc4rX12QUrwEg5VIjIjE7f3PbczDmY2YYNBazfiwAjoYFIGOPjfnLf77hvlyGHTUxAQeDBoFGiOZhhQBkAs+5T5zlvn8/9///+8rztupE4vADiXZdOZ7//7////3+Z6sbugRrPTShtibbg84mTmTASnzZl827JNssrBACAEo4cPS4dBnkaXCIZDVEaRAMZik0YSAmYtBqYSgyFwCYKzJFZtWsNfcpqL+NdW02Y49A30dEuC94kB95VRSOQWs+8z1lq1rD6WNvA6ltpDqSs2A+swyJA+CCnKZt7VdiNzNKLj7i+rGo/aPe1VKIx//uUZI0MRUAyRp97YAByY1i37uAAFHjbHG9g9QF8k+NB7CE5gAIAEpmfwSG2ZaWYj49J1Px/mH0JycnAHJjVEyiwlhgbA1mEcB6IwO1kAYChrLT1p3nkeZsJdRFYwPgmQEE2PAWmAaB0YcYbRQCYhku9ci9TGMQQMAwe83zJIE3TescUPFLpvpOzylJqyiTVGUIDmnQzXltN3f//6/69t9IBe+HIej0HMLiTiALGxBP///uhpYHA2ETX/rUlLM+mzi2lVrbHGwsVVBDGzCCMO4KgxkCxTCaAUMewKQxBgVTBRAUJAHwSBeX0RmXO90qijyQI1iKPCh8A6A5DJFDDWxXDuYVIU/Dpu0zRhj1vcziA5FFKe3HACrhMUe////104yi93FFPRVUAAG0gJpuMAGGxCbME4CcZs5AGDCecVUZuHnKqZkRbg2dwc8aX5nPhoCYTCxIaDE8pfc381IuZurzmn+YWXpMJRjSCUxkQQoMdGDDx066oMvHTIAEvM0Ol//uUZCwIBPA6UOuZ2SxQpRpdYexPjgUlVayabpjuGiu085o/tzWesabWest95nreufnhKYMZcKAZgwcZqUiwOv1vjAhUxsqMYCUECBzgz348///f/////+//91aCRKYoAWVQtrLMEAbFMgAAXECkq4gAOWbIxAIMgKGiP8rA/9OShZKpKTpNEOMTe/JX5k1r+A5F1H8odsrAGrZGavmm/M98ss2ifpubb+mdrAZgcce4rCCTm3bf/wawy4ioxHcKBGuZWRKUfI5CiKsA0MYPQMXWmk6xEQZhgOETBd1a5AycZPHHenZSw6wCrh6TUgHaME7VFxDWZl8YQoMky4ymT2/SS6y6z//pl8csUGLeQAnC4QMQnBv4NgsPXHPJ83TSf/////WmXEHfAAwAALY22KHAApkai1lmQbGuN5f8gBbCSJKG3+bEeIooSvozmg9Zrhh2/+MiWTM6pb////6nrjPZ4DAaY9UAABOtolpmMFRUuVKSNhuAP9ToOElvplDh//uUZAyIA51MVmshVww9ZnstPMpnzbUNX0xB9bD0ISspACfOJE6hwgaZSpZKmBEB0NCGGojKpfLZ6ap7NNdwp6XX93S/j+5Sodn/5bxZgHRMpjNWdrVcL+NUhhEaYTuL9f5zw8ASHxxxpCFyFELR3/oe5qWdf//1NKkpucPhsAAG3XE5YIAOyQIsOBpytoJJSMvJrJhAHNo+eWc1vXOe8w/mhKPe9RIAiGJrGurOymOqaf///iGOepo9GQ2aigWBA03QaVBUbswo1hdIFjAw0mC+9pTRrzD9YtfXpbzlFmfr26tjWCkKJ5ulZBC7fMklLNyGEEW+oc0Mtg3oAeDkkuVE6CBggh///+3/8CC0EHDuAECGpeJDhLYcBLQM6FIyPQEwf///7TmscAAKSJQAgBFxWg+hNTYPqIzB8RQi4uc7MmM1GBupdeozTdborR70//zI/Uxmb2+tP/8rUB0E2s2vhG0KItoEgOGpOAw87MdVjvi0xcNOlQjBycJfzF28//uUZAyMg3FBUhtmbLQ85Do6ZYJmDmT1Om7tScD1GimllKmee1DMBiHnTNVFkL4zdKABUMuppIv6e0SBDW8VhyLKsjZ7bFCZ7zzSTKdEQo2pekYhJSik/+iy1stFkWRRpa0TaldFqnpe1fWYmv6iSNvwVBp///rdyQAAGlECZgDCbDyoWD1FjG2SQ4ke5CQPDEwtdy1mydu5LRu8eixqmOd3FeopruJe62N8N4J/0Dfr/8n/kesAAXwAAF0YWm8ZnnObUqIZLG2cPFQYLISbmj4YSkEaVASeaQlYcYMPnRNg0fumtAKuCD7UmrMPLtM3btBrvONFYGij9xSnmqGUyqMxait0lOYaahpzOPRx7gDSOvuGR7f/W9a7qyzR8zsxxATmL0/8hJfIgsAXQOAkehARTtVAUxDCLNIA0mkrpbHVnbj1dfKQ6nUXvZiy1b/yjftVf80Au7mTfNt/uj9DXa3JW/qfw3UAp2wAwZ+Mxih8oPI7zR4g6SXClmd6xGB///uUZAyMk1UzUJtvHGRBpnqnYKtvjjDNNG7pTUDykSrlhB3uRrxGakUq5JBIyAzL5PvYAwWLA0In1OThjjKxSun8jg/tEhz3gazjV8ViTbxnea13+J0zVvjWpZS6LF8Ztf/Ne6UnhPCeGlcLVyJww3RXkA1qcAIJOWMCmpVjK4biNAKqZQMlaGunWbNOGUYaxDv/7OzvPV40VXuUqYFH/RRDgnSll1cSp0+nV9f/FZjU3W1Rqf/lwAJuwDBtCAqcphe7RvzfxpcGR8iPJizDZo6vhh3YxiSGJmqbAKG0BA0aOgQRDMuFBGZjeRBWwv8mALLJ5rcWUWkUw0x3KeVxKSWdQ5SZ162uKK10SoqEi0EstflB6FcOH1a3ezJu7a6SQrZJhCDwW/++6775HFU4W2BXpAFTxUHKH25HFjvxLYpVEapsLMj1nnsQlPB4+DN9FZKn9G5T+UCA4+zYs00EEjmzLImAygAAA1G3G3GBDxgjoVXmgnmTKgE8SORUcSDi//uURA0Igskz1GtMPLxZpFn3c2geikiLT00lcHlWEOhZzJkmYOICvIigAhDxepSuC/1WaKPFNnI4qr4hIVWSapftAs1ms1B1AerO2FQcY6CKe79BUAGLa////nC5u6C5/871gMBKdgACAWZOXZmeCmpJGcjLmnWhBemQswW7hNKMST8hEDmhED9Pu3cvIRBFqEvBO5wWE4OmDjELLJdjDMhxvbPcz6IP7gm+q9iQCCccNxMsHjpsLyKxIdxQe2m5WAEtw4wDEBndBnhAapEDcABmSixoRGqJ4C7ryVfT0XLUnSICnKvWM00XkUxgJEdlwyojoP5+Xz58hNzkDs/7icASszUr6OWGxb/fiA/7JiCpyBCBTHQaMjEoy28jPREOFC8wEdTAgsLSAaVFjaBBgQ3I2cROaSOXLMQ9DzZ/uCiTB0o5IlSIU1p00lzdd5/KS/NHHfP++oBXyp2bQ2gu6dEP+jlaABliAMEcP0whC9jJDIcMW5VE3xEc7Xf8wrqc//uUZA2Mg3Yiy5vdYPBAxfqaYYdNjjC/LG9wqYD9kimdga4e4ADwz+sk2VGAydSUHFKYOgEblgEAgMAwAonGAYlBgdLHayqIaBpxU3jIIBWOgdOkwlFeTZYuuvjb/XG2py2TkV+85itMznuLmeN48ceWvWoQM/+gAAGik3WAGitlKhVgVBRKjGU+EjE8Q/pCSN+peN2vUljVvLsq7eGtv4qahplsq+2eWRWG7/oBIxdv3W31Xd5R+XAAeYAMCIScxWQKjISRZMJpe4wyBsjR3G4MEUrkzDhFzF0PnMD4Kw6sojBYEBCPNtQIyQBUuWcmKTWGCZQR01cpwrNeddqr3qaa4EhdCs+j8RSBhYQCY5mIFB9zq5zB/YJiv5zh8y12u7qImqy95cQdkBlJSAANhWUFpF9i2wCCqqTsIcRVVl2YdSL9sxOex1EoQ1x1OsFle1bTOzh50X2Yy6oWj/2bA4dY1JwRBQIejZUAIKTiAAUETG08ziANR5zIn82QyMIQ//uURAsNwrkv0Tt4QjxUBPoHc2geyeSTRE3lCLlkkSaN3iB4ToBsxRBDGQ/ueVLQ/Hc67fFEuZX0ho4tR0ieIIqycaPHsRF2PBwXearcNrTQJp/yxUApEyqzfCcz7NHv/cQLH80AQhOYAAGBUwumzRSsM+wkwq1P6UDFlU1ovM+rQFSBBU3peQzYNHhCitsRkEM2p2BbFWxNvC5rqt3ER/Ba3HGUVWNEGOL+hAA462v668e6ZxOxwKyF7cgQdm8kZxHAaS0GuFoKBTvDEOLQ6FNY2ZZUTxpRTs8kYBEWQ5xyiwNpgo6VVoXZuVrKFThm6Dg+VSkWvjQgHJVWGdC8WhyYnQ0mQ2/nQjL6AAMDZj8WZv6QRtFKhpX1HSJyYsMpxBkmaiuLkExwI2kmAQQajBKLK/IUMgCA36lkkemKUkgzYjEY1RgsyHMBM02rZYu9cQDNhwknr+DgAaDqbQ6p7fKKBKlgAFRYDEgZqXJynEmyhmbXDpVIR1wqkPwDtTS8//uURA8N0rojTxuaQdZfBDlje4Y8CjyLPG4ZMVlJESXJ3aUgtCoEzSJJuALQXDKcxWrCpfSyTB1xMQcSotZpZpQ9UaaNHNPx9Nl3/7kAuEA1AsqixG1mv/bukf/vAAHQAASDMYKRJxiSHbmXifeY5gvZonhHmDaDKZDQlpjz1HhCaZFaA8IDAZfMItIRgVbVHCgwBMdkDlCwLqwRMN5G72ki9Ah6KJnIo1uQ9N817QzWB1///kTQK1upH1qBTg4LDE6sMQTw51ZjfIjMrlYURBywamG10IwSIia3UvoY6D6fUkiCh7Fr8XPsinYqPCTtObprPG5RdwvGnnhnqQQPG/7ywRK3vVInIlKaSf/5BeYECMZqvUbReEdQYgcoiWYiukYBAIbWEqY2ygYJiaWHUBAJiQUfUQkw0q5HRMtr8Ny+CVYoJfERnVJPRLHBqNYxCMfkf/932qeqKR03//+XUfUEgAMBgJgxQT2zMVUDNvNjE2KgeDamCtMMwzg0VgxT//uUZA+Mkz8jSZPcWmY6RDq9YSY5jQiLJm9taYDHkOt09hz+HtQEMsUF80BsA5xmGx0flU5pEHBYBKBFU2mEAEuiFKUlYQTlYTB0NTMspnKiUutxW9atHGboNyJSLJcmgr2o93/qSBovX8bPLgAAFRxzWUACgehUqgbYh4yfytLLAONWDbDNgqmSjqB7eRKSVNPZKkH8/PlkiWctpZb+AZv//LJN//UAREAAYCYBJihJgmfYa+bwSXBrVgRmTWXWAD4jLqBcMg0QYzQQET99UxMwMnnzPegHP4VAiUHGkwHCqqpEAsILwrVUOS+bm16non2h6GZ21fNl6JPLd9psc9sFXGw0pv/DxiZ7xdpr/WXJLLrhuFOIIOUdBAFhiqueHVQt5gWGT5766/QO49ilrYXTTZf5UurI6oxlSL/VAWO6VQREAABoD8xeCIDKLUWNZMhU8Chw+fOoxhrAzkIg0PhE2VB4zECAxPAUwwA40wG8DBAydJEqCEsOgCZm1hQe//uUZB8Nkv4iSxvdYPo4pEp6YYg5ivCJNE7oyXjcjafdpI2UBHGBUECyToSkvUn0LHLvat8/Xf+s/0d7cdLqZNbWmU32yAACpuSAACXsJC7yy4JICmkDpWDjwtVXJ5pXEI/WnRYocsTasWOYkZLTCVFyMlpad7hORb/9oBelAtmBIEGBrpGd9fn2w3GncemsxnGDzAGLowGGrZmCoVHZHGLBCpA+7tmkmgQeBiwZprXoZc69TxiLz9i+QPgsy3pMimivN6W/Yc9HywXjfNs0cEAIgDWbghckqiYUCi2g1h42gE8YEK3CKeypqYOBSOnUAXAQlhc+9zQ69w6I6AYh0uh7DUjRBRHPxEl0Ai8AAEilGAAAoHTKIbOBJA/GFAb5HLF4WxjmxAwChDoAxAxTyWWBQpfs5LyQJaZXljiJMFwinLUOa1HK6XXi1x/yIs6sI8Jv0YCRrCrawEDeAACAJmEdg9AHpRoZyhxro3mEMRjowYVKh0oWRc9LcxEMeMQB//uURDmJkmIiz1ObQOxK5Gnmc2kniJyHQ05pIzEgESelzSyvgAKp8kLrMLyYhesXKE6r2l9y9bq9y9ZH08pmF/J3K1HumxzEAMnLCoBxRDmeTAc6JZztpwyoByAf0YxKRgACOdaTExiEoEYMCVKLRjJqQy9dWwhG32xdZkqdLZfgLKTqED2Kl6StGymQJJrxEAQY3TW8sPCH8ZXZvcSmLlGzJGWVk+EAhIGewa6qep3EeKNSCqfMmZBkdMzYc07OUc1U1pftW6n2KEebSOOvj2UChYTyAADAWgAMC8C4xJAnzFnS7NPktI6VfQ2bSYwvTg5VD4zERw1wBEwDKlBkQhIY6iu6SsEACMFi86T4GDdcanE3Zze63MrbxPp19MzrnUc27pbQaf5LepTJn6AC49lbuNEAAF9OS2gACAW2LIEVkS2ZsaK23tPThQvBI45gm8NpWyluYrrnn2pC6zS7gVVXUVKuh/rChZlBITxgIhHGAOucaBi7huYMomuobOad//uUZFWJEvUjS8vdYO42xFqNYGWFi8yJJu9tCUjhESj1l5T2gWRgtCNAKE4xEDpDEABjMCuAEZGLmp9NsCi8wYALmKTKCJt5fFXOjEMRCDJXT04uLnnH3buXMy483/8Q+4gHLSI+WAdZQQnXrAAACdjjctszNkYHumQYThOMU1JJHpxzkOFLNh+G8E+0xiUk+M43IixPvKqqmudR93zP639/2C1pAAACbid1AAEFGIghEpHCyQO5gYFsZGr8YMhoHLdwidSwo4OlqT7EGgN46sUpboDB4dO7fPia6aI/PIAkVS1lgUdOzE5qoJVmOJTI9/rAAAFtt11AAEwYkYa1wcfcdlYOmXwFkYwqTPa3exIgk7GmSssWY4khiK/4jHS6ctfdevhZG/Kz4nFItJIJlBoyblwyo7EItZvNHVM6U1HLrsLoJkiiahAnIKEzIVFh3NBoiPKety5lcid14WkhEG7cOq2gUZMqzRFpP3TSI55xchBQlVTc7rA0jyrNPrFU//uURG2Jkmoh0mtmTL5LpEpdaYeVyQyJTa3pI3kgESh1zLCntfBknyZ3A3JJbGYmB1OZGg5oJ3j1QA0aLRg8cL0E8asFdoxe/TWYy0Yn3qDWhzDA0tUrYY2ucjtGmFC7d6IyZd2rmpv2eZNZma5iaz3alQAAbbtoAAkRhQSYC5Gr1RoqKQAwzOJ1iMkmia/AnWbX0OAcSpNNErKyJVq0NCqZCYWZk1OMJaKSYUslmlWRg11pxhLY5cVHgoVGSogAAQ7LLdQAA+Jhw2YVIGn1BsZ2YgOIeiQ2XnIi5ndJNoDp+E0rMhmIA7GYQxJjo/OpParnoSzhryYZHMUHyfK7WqHLBwGora8uC891gucwfDQxmXI17UM2Y083NnjdQnAMTNwjcwq8SIxmNBLXLWhw8hSKLPkfkum6uo70ljEuBSlJGwlLXMxstuM7y15T4u2Zr/KysTEhuMIQ8MrH6OkypNsunPycw/IjzCBdNDjEw3FzMoAMRBtW4KAEzEDPuzrW//uURIaJEl4h0tN5ST5MZDpNbYh5iUxtNG7ww8klkSYJ3hh23moZBRplG96lGnxKbVWZT5Yq4p25hWbyUP/nY8baFgFeAACABDJE5DcYzzSvWCDAmgSeAZGZzDJgFymSgQasQk4kEIjV3I+sSIA3JaU2F9H/jkflFPnhXsBgYPVVVVWWW8aicqqlpS6a2HJAAAAAHDDlGYMqYskxJUFDru7PYsswygzM4vANZMnAgAh5mo6EgoNy+TcaJnr8wFG5BPzVCIAobiRS5YEFQHrKRcpXhIaBS4CkQU4CAzMW1PMIX+MOtTM610MrQ8KqXspCo5g5BDG7StYUNJqoJqsXZK0hhrTharTHpsopWNy9u/rRPiUh4YWRiekaLo6qYPxfqb8wSDcyBTEycpAxbq8QJ8aAkQYW2hguKQ4l4Dgqx15xJ2U2gmYoJqIxIDOSIkjX+jRytflHlH+jTjdfc2L1oLqlf/9tBhrPsboG3gAAwGwPTFCNLM29E8yVmujKoNrM//uURJ6N0lUPzRu8yKJKo7lze4MfCRBPNG7liNklEWaJ3ZivmIKwxl0TXAmBKIOUCcw4diIAGCAGZMAxhgFIJUmkJiE1CpAxY8ojbKG7xqXV6yludmtK83LaQMg/syQ8sgFsanFP5ZmQACrnLqAAM26CQQqwSYRHKpVuKaGkJpBxqBYV6IlneAItAivnph2EPkBhC6whQnnFHEqYQUxp0nwPLcAm4VA8MQotYykAXTNtabM40uAwyhIzBGF7MTwFQCjJGFcCqdtKJVAUOMcvNAOUVDg4wAQTFAFHtZy/HjgJyILnbdIo6B2RK6ZpmHBE6UyNXwroXIgMCJu988AAW1bmtCsoL8FMm9KVRUWRMMoQFuNgePBIMy0Mh8PU9ztn/yoNTKqD/H+eWi/l8IML1GWzWpB6T3T9kgfAAwEgcjELRAMc1Xg0inGjSmFBMm0AkwJRcTHyCIMEEgAwXQEzfMhkmNEj3mDHCIsYcGWTQzDgafSYCkHraXEngkcxLHAw//uUZLkNEtYaSxvcMXI6xEp6YeM5y0iJLG9oaUjsEKlphhjnwQS6O+hU99Nz0cJXNhFfvic/ugAAlKcgAAuCokCxwQ8N6AnVFExti1DSwlmqvyWSjUTsSt5xcGrtfzOISI4fmZ04m5kkRQ5p06LiIPowzwALpg4ACGRaf6YpkEJsdw9myuP2YPY2hgfCzmV0GQYL45RjGgVHd2GoJGqLn+yCAMYMQgeQGwEAFghMYEhC7JU8anbxvxINDwcHJrXqZM96bFa/b5n2GYrL7sVrpu28YyDoGHIXGrez0fJV4nUEwplIeC4tUwKFKhyhL+xg3ccRDr08mDubIIEAnkSJ9QhmTl28y9UBgAMCyBQTCYDC0yJQkgM3sQjj0ub8N6AS8wfhLTYyEoMJEFMDPXn8UZpp+ZmnHZSJqacZ2nGHqAoqDJSYWCkQmWWAoMOhBVBCwAKKNbhbrOtFtSGDoFg+Rz9erS2b1inu3RepYXNvpPr1gAjOU5AABBQICG9gGysA//uUZNGNktAiSpPaGlY8hCoaayMdy1h3Jm9oyUDfkOlphgzvFwEcn7FyiJT4LVMwR/KpELg3FC4pV00TUFAw1UMzRmVRxxUZil28zmIELLenama01pLX9cPrEYbITxr7M8Hv/HEfmezp0sxHmeiYKYK4rxsQAxAxBEyZwgDMNaMHDQxiLD3YINHjwxWSAaJASHCTgXcexAPxnMndNNbZm1pkzbOU4T7Q9J79uejUSlVq1ay33WtZdyw/nN301H8AAJtJyPQQigZtNjTNFHM0KHABGEWjKEjpU8HhaLwi4ZHyo6NT05Lpiqjs0199dFAQaH+TMzMzQS0YWbMGhIVNg0WcLQdAAwE0GYMGbMTDFNkB80hYoQMVMLIjFxwWQwFMLiMV6AsTA6BTswKYD6GLs0s7NikzorI0UiMrHjBikwQzARGYYEiw6WaAQE3EvazVsDBYFjTtS6OxmpGgk4BHmo5RK0qecZrOqyCPOjeoE7wAAWRAxfN1Tg2NLDdruNcF//uUZOqNE1EYRpP+2KBE5En6aeY7zKh3Gi9zCJkOEefppg1vcwkmgjunB4CYOzVTUEC5S6UlWFgl63tmnetwzZBARbSqUNgJlUoezaltqxqJVOjJQY7+uGg8YNoDqmNvhOJmrCFga+ePimHAHZZizQZgYL4IBGKCApJgzwlSYZgBNGgKsZpIxgUdHYGeIyAAi2YuDIyAQUJDCYCBALQcLpFuWGJ1NFe93aQNFTAqKmBKWeyo1eIpvjWxqs8aNx+389rq3pEYpxqdhZiecEdXOfYqcseOxg0azlqIwCdZTlMol9oZzE0QYeROTIvjbLaiyiVzWyyzk5q5qgmVHF4UQk35JpuVGAwBhDzFKmBN3lKU/0DyDdBfyMgQWEwekRDLCEbMP4BkycgQTtW0x8zMHNDkVUUHhGIggIjxVAS8CC6aica3WbOq70qu3C0FqHBJE4uLx82vTtf1WDHe5tsqDAAGAMAv5ggRuWZieD3GoJAsxigpioYqYH5mUFHG9yZm//uUZOyNk1ceRpP7MmJGw6ljcwM6TXyHGE/xKUEBDmcpp5lPlhGnqQfmMoMkIPmDYqGwY0mC4EIAy/CcSQSIqVzTWXMpeIDxgo25pWpsNtsvjKMrpDNi8RcWg/QOGCmDUZwAfhmCOFn8eHCctipZkjhcmCmjYYoAjZg9FDGNUBufzseEAYZCOqjRBCoDQeCxhYBNNlTHWDs4gsNBEwmiQrRmwjnWN2GLkSOogxN99BIYICB5GN9CPZh8SpSbByHiGXKFwplEoQKYKmG4GDuAspgXQiEYZgBUmQPg8dGBz5jv8mmkOn9DKtKl60IJRKeR3woVMm0F/0/yYXUT3J1U7/yKl3+dyugHAAMAbBHjACS3gwVszyNDGDiTMMQlIwgcFuMApE6jBSQMswGUPAMBBAaT6RgBLZao4AvM4BGjoZgoDHgtylcSFd8QfSGYH+N1hjsrG4RaadBnwPDbSAAYCeC0GCwIZRh8yrIaXgOEmbUi6RhbojYYAqFUGAOASJgN//uUROuN0s8bxwvbMlZZAviyf6ksSmBRHE9pKQlgjuJJ/aUgYo8IgJ00qFDDYBMNBw8QbzDwAQBxlVF7ohBztvfMWRaR6yGH00/5eFZ766mwm5HqCwwHwCQDCt8wqcxNNP9IbDP4gyQBE8BgEYv4YeyBmmClh9BhRoDyet5hIEIRQ6cZBRwvBd4IKRIETVdx813vDL3ilk/G6RA4Iupw+PSvRY1PLf1z9zlLtxGhhgPwHsZDuNhGX0nspsRp68a2MLBmcrAaZgogh8YPiBLmDnjNBh5YFyaKPZEjzCqMPgvMwmEFqwe3inojGn3hGMSFID0UDMy6feyONH5j2uPy5G+fX/zf+hvv1+vfWhoAAwOAHHMH9N/zK6hCs0iAvPM4FHGTBUwc0wBUT2MVjAGzY02DykBjJsnjEcEBAHJgKPRdlMphwUA5dLTn6aE39WAQNFXURVLaJfdDVuN0HhRYDP2I0ygABgswUGYkKoimkcIKJpryLmabSAyGJqis5UDk//uUROqN0qUXxpP7GlBUYviif4ZICzh5Fk/syUF3i6HF/hkgDCxQBIweUHWARFUaueGRjhjcOeT/DgKm5EywBVpdWrNBs7jWKyDBJAQ/GilT8xW/Sxv//XQQwTkC3MjIFUzQSVOY2bVYQP9b6w1GQNDGTV6OUcIYyGUiTOnBOMJgnEBCHBg4f8FzqZzaAkmVfKp552hQVBUSlUVh2K0szM8ws5d7W0YA9oTBU/Z0aeGByg4pkiBMSZxO9GGsfr0hprxNsZZ+GSGQSVkZzoexkOotmhODQYRQhJWC6DghTIOAXKwSkImosqYvMYvhT457KylVCKnTg+eEY+K6n9/9fX///76//6P1fpoIBUMJZEXDHCmv40ZWFZOLyjozRT2Tkx4IXCBoZQZV6C6GCykk5gmYI4a6dxn0aGIDcdpa4QQqGKgkIjQClOdl5sNNOqmKo/J15A3r6aXuefBjFtTqf8n7ONX1puyWr/9yfu/SsAAGBkAAYx5OhsrAAmysgOaP//uUROgN0rcXxZP9QdBSQoiCf2VICrxTEC/7YtFqCqHF/wywCXZnAjXmFmI4aloCRn9STRplzgAhohKzCFYSN5dHXGXFuqMnsqnfMpJ/jb7tDkpeZZ2rb0sQYQYCfGZKosBvnBrwcM7LeGNuNeRkvArCYKABpGWRgoRgqQ/6Yi0BsGgsvmGQZGE5Bmk6HGFoHrQaWIAbV5uklNNZ0gtlFRmZYKpiIuRVZ5YbIJqT/1a9/v+63/Yz/7vdsBSkbQMCg9MaVtNn+kPQbMPQWeNrz4MOyDOkwyApMDVTQ7VPMsNgLXtL7Ky0RmRkIx7l3xliKKwl+WPfN1tJ/QACBvVQjB2FVMhWZ81tVljsM8zNfyNUxwQiTChILNLcKwyWSE4UAcIQhJh7QEeMnafGGgi1+jvOQ2cYvaj8HR5nNuSFIBaTFneg0/v839S/93T/Yr/zH6dkTGDVBupjdqkGZb+0NGnLPqphRicaZdyGFmDqgrZijYJEYNkGimKIADZj8GQF//uUZOkMkx8aw5P8MkBGwviSe2k4DERrDi/0aQD9CKO13STdAUwQCk4GDQOEFPJ1CACgRWw6QMS3Pu5Wd+e9bxUetJCT1vR9f+2xpdZjos/69u//Shft7CQAAwQsBxMeAOXTE91qczwd7uNH1NYDEnQcQwIcWbMfjBTjY7Yj4YgTV0lQUnpgeGxkGPIOBpWVnQoCzfuBFA2BsUCQ6J1UdKdfcg4NB8JOYbYvZ/9P///////+gSqrIYHKBJmL9HZ5kPK7KY2msVGSylqhiPodwZEt6eWkwarUUeEDCcuPmUBZAsGqz4OGmkx5lTzUX15ZYxJ1ViVfS9FxZOz16+v+7f/u21//VWAMEPB9DGKlaQxkBZUMkwkyzYZCmMxyYIeMDGBs2nRRDEmZCMSAM05xHyaWCofNKmBXajqD6UTNnxcda7L4/EHggSF09FSXDB4DiYYCAEBAfCVc/9lf//7n81T3/rWBUDAuwWgxehA6MqMQYjBt2hk/ssDTpkGqMP4t//uURPEIgs8UxsvdYbhgIohgf6lEC5BLEE/1JwFEhmJZ/uyIczEwfDEaU+MUQJMx8QwwsGCxGcdPAkX2rO+VADKLve0QIAiYchOH6RRj0VdP+rTtu19H/XsHZ7///sGAgBvjGRTuoybcHQMGqhJDWVQcYwDkKVMBeHazDTQKIwD4VAMJpAZzu/4nVAEXjdqTEKz3fJA91mzs3ctrbd3jgOnkFidG9uO4M3pmxc5mE74wE8IwMWbTsjOZzswyEZ4+NSdKfDBoxZkEGJmSWBuYGi/Bk2g8mfzETLgweMTsBOQhaHKUtWOVMbl2z/aZwwsKBtYRW08zyGjDA30Udl9X6LN+mz91+76FK4wwqoNFM4PeETbvK/s0E/o/OW+KJjNDQbgyMa5TBkAjMY9Qw3JwRTgXXNxCMLpIzHLxUEIjBcGAwMF4ULUmVNWiunqGJRdnaWlxqcr5BM+AiKOxFlwuilX6BVH7QFu/OJXQ/FP6EtcZ6GvEoAAARkbiAAAAAZmG//uUROsNwuwQQ4v+4RBXAXiCf9wUCghrEE/saUFtBuHF/3CISxmnGfGXnZmvlSG3QvmEDTmIwKAg0iKMDcagZ2Y4lUBQLKCyRTUJRPP0xUTOmq1mJ4aXCKE19fR/7CssSmA5Bcpi2CU+YQEzVGVYRMhprJwiYSMI1GBaDNZhYABqaN2uf0k8ZTIoYiAEYHBmZ6iwgPC4BMSZkkUx+OrVhmGn2EcJQ+OVd9ENVih7h16XSSev3t/e3b/2NX1d7un//02AK/bQGBIDGNA1G0YlmJEcBVXTJUUQCa4QhA4tpQW0r2MlDJyNZYyqfpWSyl2oaC4KhLSV0tizJmK1PTcWJ2m9yjAMFtB2jGFigozcUpJMjHYuThO0uNEQUkwXkmzRYB7MF89ExFQXjK5s1gIAwWcsCDQ4Bg4LALipGq2J9Mpg5nUGxWGqSexw7zLtmqimlRf6UoTFvf8rIgIYOqDtGRBE9xqy4zyZNKxMnudjwcVxkphKFUmPkGqYZ6WpjNhE//uUZOkIk1MSQoP+4RBFYfj9dyw1TJRREs/1B0EACuT13CTsGEO5kwQOFZ9TADjlY0pRfk8tuX4zepiQkWECqiqBIzhyzp/8K//R/2f1zf/6DAJgbwxE03WMz4GLTL5lowxQdDzKFFpMGY6M1UAhTAmPNMXAEw7TSMiGREYGJpIYEEwkGBKZyApO1VRY7JGpNq70Mymgu3Ltyvhbt3rWITTRVEHJY1V77KBZTAIQpQxBtI4M9wRaTN9FaI5D8WzczMtMGcisyqAwzALTxMaYHY2piBwyOnBn0eNBkDQywlzL8tlUpuZ2AeDbdx4UEhsNqJVbd1ftutvbR2foNS1jHf9tX+hcWRUwUoHXMbHIkTDaEdw0o9crN83ig0FxcjCULDNq4A0wmCbTLqA0PZfjOAgQEJzpw+oOFzBAILg4oCoCkfU8k+FjsofGGoGkFPcxt0lPWtXrdIHC2P7BW2s1UmC7A1JjmIjKZBUsIGiYoZpg0RbwYo6GUGFrmnXgCGLV//uUROyPwq0SxQv+2KZUoZiCf9sUCvBRFA/7Ypl2hqIJ/2xQYgtjDSRwWHR0DPjIBYDfy66EtpK8xdrWb5g+0I0svHV2+j8hTzva3rcidrTqTvf3POaXp7PKgwYESDFmH7I/Zk8KQGa0ophHoRl6aSA0RgxBkm3cBAYQwxhlcAhnXJAVLTCQM4w8YaYiFjgMBggGACsJcdhSSCmcPNYhl+JLJ7FS7bqcqXqe5Y3nhyp+/1zP7Eh6P+MAYCqEOmHCLwJodaq2bVwnIGQfIQpk1wUUZ1KyfwEUZVM4fVDWYbBU3UwCCszQH4t68zkKLCyawgHWEXrrQPh6BQKOeHzgeW/1ey32f/q/b92z9rfyKg4AAwLcCcMW4GljGEhHo1ek+dPjt/gwch1TDQHYNbIDowYjVDFIBjMK8RpNwKRHRuNJGOeusgGL5riiSCi6GJsrzl0NwupRaqVa9/D6fPWsOX7Hg+Y4yhl+hgAAwNsC8MdiDADKQh4c2PE6PO/vTQxV//uUROqNwrMURIP+2KJYwaiSf7siC9BtEk/7YolXB6HF/pigERTDYDeM0gHwwjEQjHkB1OTngUemFnBwDqvBgbdKd3rOUUlcvlHO26k5T5cz1ypsLAugHyBb/3f+3+swHEFEMD3MRDGHyig0ZAmIO8oXkx2QaTB3JSNAwEIwKSqjCzBJGZpkzK3wlUUGi2YXBoc38bVfi6IMe+H7strSyKWN5XBEofWPSABkxDvH1jIYGKEDGBlKghks6/sbFEV7H3U/OboINRimiQGbwHgYMKVBiWhNoBwSA2CgChYd4mAvZi47KRXIMGBWAeRAwgJOTtqMRbYmjKODx86OPHzdM//9P7P9v//21RAMFAAXTIbxmUyWhYRN0TLjTTaRzkwnEIwMDuFFDFSgEoxpxA7JEAwYWgxvBwwgc6i5SYcfLNkoNrEWd5TZFJOpp0DOs3rmQ5Sbm5unz1fv9oAHMUKefSpWWGAADA4AJQx+ME3MLKXlTWwRqs0J0ChMB2DojLpx//uUROaP0soXxRP+yLJSwmiCf9sUCfxFGA/7QollCeIJ/yRoDqgsDHHRh9lyQlgMGxggGpjQNSc7J19xFtJ2qEw2AcByM0y8fJyNduXbe3NTBpJAPPjft/////////9I0GBhBC5gA54qZC4VlGp6CMxmjRIAYhEBLmAiCQwsK8GAtgTJhIoBIbLAmmgYWADIjWGA4CCoAKhCs7/sWS6R+UydKpFXub6BgKEghHzM7VEvQYNsVSmLnFUph/EHmRH1qeODRR/cAgnmwjobEwpZhNDtGL8BsYLoNJkrgaGFciTYgLHogxxm+oaHxcJh0Tk7cWT5s223cWWTZtsDiXYfL0/6utck/V///u/6P9QtqpAQVGZE8FummZlrxro4xgZpaXdmHxhL5gKokoYKAA3mGPHm846GUAlAIyn6MtQBVrLvF7l3RKDn5kSul3QEkTDiQNCZYsifs56tVSafOcaSIp3cr/9rqL13dn6/d/6P9gAA1X4AAXBhhBeEPNPNp80Z//uUROmNws4URAv90ZJY4oiCf6kqCvBjFE/saUFXCeKJ7SUQUTPgwCygVXOQmogAvnpNHRzJ6MipILMaSe0R2CQUSVN2iHeK/Wku3eKW/h1mygyrCYMiHmGGNL4BiRDaacCEewmRRJYBi/IMsYg7BRjDiSmFAtGYhgWwHhxbWMQRzUmoIETAQZvxEFsSdtUq1Vhl1QC7MVkri1H+yuym9Ws45U2eN7G0EhEJTjBQu5O7r+x1n4yftqX20//5On9PQcBgeADmMCQoaLSqRq/CnGfWkoYxwmpgXFJGKCBeYBoCkAumKF7MDBCWaKmja0R8hDJkARoBWVEJkiNIY+tk/JWQZ0s3KhkAAwTQCnMaXFKjKaj3I1f0viNfzS8yUBojBIQEMzcLswrCbzHaAsN0owdVCAKM8Ix4IR+VVbxXLMnXYi8UDuLAUjf6Ozkuq8rb5ZhalBEQaNN//YIAwb4BXMldL2DT5Fgo2v9E0MIZQqTGqAyA08rg3LOU0k9U7cGQ//uUZOeMkxEZRRP9SdA/AzlJceY7zdBbEE/7ZEESieKJ7STZ15dAUUYoPHeJo0QM2pXkazGItEZZLpTYl1a3La1+ludskjwSW/QM1q/X3SP//9n/1/v/ewwHgHvMG9RfzKQyIE2FZAdPDHQwyOhyyFAo0wQ5SIX0yfgHz94ha8j0G47rbNNUqZ0yluTYXofeGIfhyQSednqWmxrC4ZhI0GwWE3////////7BSFUwKoIrMNpRzzNKwZI1lhDnMZPObDD1g30w0ow4TOUzsDc70Bs2E4X+YmJHUlbLpDfaNTXbmdi3eBkFhYWCwGJiZqrk/fVd/2avH/4orqV27fqu/qogDA2QEEx3AkpMUaNXzZH1PQ0doo5MMBA7zA+wzAxi0DDMWMGN+CNMFUvGgrCoLmBwhPGH4III1jsRBLHAunxoTzuBajcXwBsHBZNweFnM/X//2//////6vW4Bgc4FCY9YdPmJcL6pqxKpUZkcZbmPIgi5pfER2eX5kJvhzIS5//uUROsNwqQSxRP+2KJZIkhxf7siClxHEi/7QoFYBmIJ/uyImf4JCgVaYHUymMW6eeQeCz7RVgo4yxW8fYvPWW/d//c5dXd/6v/0BIgpgcYPQYgYnrGMEqmBqvC5iagWIFGFRg/xgzicG0WB+YPJ/Rgqg+misBhouoeovDzlzEFw1IKe3GOTdaxlYzpwQJmQyOcOadk0JqYv/c7//T6v9qO6rf6nu9IacbjQJKMCQmMtLOAPfHIGVnIaDGF45EoQPXYXkkQ2xXycBVPF9Af7k3PcjeysKCpWAlfvV//Vt2Jtq3/6P1+uMBCEZDP7XHg0RWB/OEd56js3AmY0CoGvMCcRk8FwIjKDhuNXUQs0FhUDFgYSioaikGDheXTA6VLUaOeq0VzOv2z+deqDUxE6wmamwsxW8a1ahcaooKNLDDzxqyWlyGvS6mOYZrOl00PvTZadYKiszRSpHva8KgBa8wTAUzGxPfNfhQs32VQDUyUaMusHgwew+wM38ZWepSa///uURO2MgssRRAv9YaRMwThxf7oiC3g/EE/7RED/hOW13cCWuPDEPTECG/eQPHXc1GTbh81OLft7PT/f//f2f0M/o8Ytd2r0GHFCfZmz8emcrgsJmmibKpyApqSY2qJ+mCch+dBYehlEj7GwuBQdOUphQcGCzWZnWhgUAs+g1ECE37IBFqTZWHBz1hpKGGU6xorJa3jX8JMcVZPqqcid7Wsjr0LFE8gUWtiMzMKUF6S7XdD6AMAi1NhObMwGWg3WGiTMjeLMhwcgQkjmceCEZuuCkJ5F4cARUNAPj2fJZZQg8HFLKJZtrdHqr+SvFv/9H/3/91UwUcQJM5McxzZy7DQz0fkuN1qW5jJVQrgygWeTgXG0MoNds03QhDRa3BTMEiCc+D48FVJPkjc34dSocs2KHXGwCaEggKizSdQbVCMgsVUxhsWY/S5WRsc/eueJ9gbHN1au7Cnu6l+gqVVMXSENkosNFNesz/VrjEdW8Mh4RkwBh+zIxAYfofJPEcXE//uUZPqPg68QQYP+6RBHQbiFe0g0DWwrCA/7hEEChOHF32gQViCO6a9nJNl/YKgMCSZNnV/o2/7v/7vTd9X/+4w4UUyM5odRDa56rkz0TmJNRpenzHaBWwwZH+zZ7GmMQGHUw4w4Dq1bMpBQwsODa6JDgNnZSPh4AAsPJVE542BJDVP0NuodR2oCPCSYtiqzew0y4USnrpbAdVjC4Ef8XXl2lT8i0wIgSjFhQuFsJDMudpNBhT0yZwrzFGwTZ0RASiMYBNxPIoKaYgCObhu3aooqatOgA6Nvle5rf02/9XR/s/17Mv8YMHmEGjPSZ5Q6jZUCMtp/ZjHPJZwyVoZPMHCI8zE0wUgwaQhJMTEBGjv9Y+Y2MxVTw6gtmzCA0bWrZ4u57ORps7or7PNbROc2j/3vXWSqtd1fuuDefVqrACiakBe9LqX2TalHlpUQ4rZ0mWvIEVCrKABEiqph0NZsjAZlJuDGWHAoa+Z4xjoiiGAKXiYPYLANgIbgmIUUHIAz//uUZPOP0zYJwgP+4RBAgSiSd9kEDJQnCA/7hEEIBOIF7uiA1cHNSVyrH3Ck2BAat/0JTX/T21/3/0bC3+a/+j3mGdCmZn97jWZ4XDSGUX+iZoq0QOZVSDXmNLReYaRDxkSHVmy0CmZQp5rgWv6JsUaIDrw4oY9tzP+55XDxQwhCVBx1DQbedLkh6Zhz1DFhxzgITfUV5Y8AiI26mc1lwXSpN7jbkcyt49jTcdEzxCMZQOSA98oCDQDTBuJ7MQ1HkxXkTT5BWTZYNDApbzGwGh1Q0sYmTNgJCOw4FKYpi7S7z1LLe77Wfd/M/1bGf47Z/qZ9FTBOQ2sy5a0rMuh1KzaSOzA3yNvIMdrE2jAWlJNKAVIyhFQDWICtIeYbWD4FLhotYiEBKAu0k838pw5fyyzx1nhSicsk+kXHFRQ2L3LqUhzk9d1ZZrSZxjjCFsFlDFpGsFE3/XXSpfWprnLHzHIqKrGmAYEgYbdTsaGi4hlbzIHACGQYPov5gnFsmQsB//uUZPqPg4AuwYP7EjBGYSiGd9oEDjg1Bg/7hEEBA+Nl7uBMkCARtwBotQ9QQlio5+b12CK/XB2Jh1VN3AfFf+hCv+v+3t/RT//9f6zB6QhAzqBgIMrChTDhPcZE4qBK/MxCCbjFEeHNnAT4xeHoDGIDtPTNTsBMxMRPJbiIDVDC3ThrvbOVNdw1ZIhoRuCbDSFl23Quvuc22sWRbUhbyrVUfaVYduzxg539bk3dYsccMaXL3vQK8MDUIIwF1XDWHSMMUqXM3RjNjLLA+MFcmky8gOgvLKK4KlAqLAA9FnLtiNCoZQ7UKHcMFwCXc1Ejf260aEf19l3/9PZ//q/U/owJ0KAMQZTqDLWQhI1ZRvANXBBSjA6QikwKAOHDC5kwu7Q2pCkyGLQIHFWMDHA5lDUUfYSGCoOMzmrVDU1olDmniP2eQ6vr7+1Nav+yuL79jNlmj/qMErDPDCf2u40sVB6NqEazjWkyjYwlUShMA4lkzJgMQIpYZGYMpm0IEOJh//uUZPQP04MPwYP+4RBEYUhxd9oEDUw5CA/7ZEEfCGHF7QzYIOeQPEwrR4wDO6LFlx7n1rq1fT2t7GWlhN2/de99+24U9Vb0IVWjcOq7UdxUd1VcYcQX5qWtonP1X6fIPwJ4GKDAJBsGpyfkAqYXoACsCN/aCDKHIAj36q5Tk70BgmIwQaLBuQe/Zt9Ngr/4v7/1f/qZuX+77vkyGDZARplp5OKaVlAFm15siJrcwIgZAWC8GfsDn6gkmZtSB+bm9nGcFjioE512TNBBAaDBG9IzDNil2WsHSOtNqaLrZhT9b6vSyiT02KNfUxpj391o9CU1mZVjBchHkwzJy7Mcgfojf6npQ1/9EVMJqEbDBKAV0xzYCJNB7JPwSADnVBwvGBwLmWYfF+4xXVmHbNRfx33l2afIMOPFmgih5MKw43QbC8XSojUhnpQ9B+8lc00fph2kaU2CuKref3vfcRUpYuPkGmFEyiIj6whNZsUs1HmdDC3SnMY0MUwCQdDK0AJA//uURO6I4rITQ4v9GbBXgUhQf9siCVgpFs93REFbhKFF/uiIxpGiCoFT3B6GXZ/JDXeaHhGAXtlWOYmvubYP7f/Q30/7P/Z//1hAgBiAoYsahARFHX9q7J2/cnWbNM9sGX0CYZggweUZT0DzmCRj/JhNIJAZL3Ri8EBQTnBjADgqrE7q6xmouw5c+vzyhe7VIPYGtUI6msiLfd+yLG/Z1MnpEE0LHzP3/NHaGvtr/+p/tM3tuu/23zc9uyv/3+I5m/VP/IwYBgHhVGA2mMaTzYpuDv/mauugZLIdxggxB5oFpjyMENBjMALBbIDBwV1t5U1+6IAJbR1n54BJHtaFG9m7s/+5Hq/V//sV4cwPMSSMLfh8DWqFV85p5QsMl/X/THNwyMwGUgDcUFlMB5k0yYgpDCoc1EJKpcYU5tyh+wone/6wxyHCoeS6OcsVM4yjiiVGLLE9Ck2Go/M2o1MDhIDsXxCjfNOdD7nP22RcmcZuUwBioIpwvABlHSeG1Q5c//uUZPSMw3oQwxP9MbBDYSiSc9kED3BFBk/xaIkUhOHF7uyAa3yt5l2BwGCkQsZVgD5gsy7QCRGmbkg4dPYdgsQvov+pbIms/2r+l////+n/9bLmFRBBJnQYtsZmtMaHXamG5jp6+EY2aGiGC0rwa0QcphpHcmiEBwYWmw05wYCTaoeIg3Ak+7kJAg+dbShJcEB5dKmAZKJ3YKIxZw1blpFLnKnxqa6Y0XVuPC60BmiZixlW1bjTGvSopeBjAsZbagQA4mCuxGamropu1tim/6kYYYA1ZilihxoLoM+A6MYDQRVo2RQWgX+SmywxpV1uFlbl//3fS38X1/nbI7+r7P/tAC/5gPAZCYEC/NmefuW50iQYIZ94oImQsBKRgMwrQBhtEyodA+oIE7c1AVmYADnFHDe2sGjU7wq+YeyKoeecftFULxpBBdOsheNfQFkYodGj7A7j5vUqxFkw65acY6QxrEyz9zI0AAAm5aYIAPMSYXMy2iFTSaLoNQAGkxqw//uUZOiM0zULQov+2RA8gShxd9oEDdAnCi/7hEEFhOGF7uiAHzGvgNVBsCkH/gu41uCEe5Zv5jGjZXStkrVdb3fV9e3+9X/u/Rb+//rIAwXcCVM6wKYTQDg2M9cso2NvZU9jAJRVsxYIDDEIA5MNpo4yvAszHHQO/zETw2SDQEPjHH7hVhBeHSbDwswQn7Do4wcQTfLKrEhZasSXF2OuxdY3pUiOUbQZJPYTike56PaTZ5B7gWA2LUMAYDoJxh/KqGcLD4b0ZSpuXmQmCSLAYIV8Y7heYt4fENqjcymoKaiv7mrVC3Et8tFnikZM/p/3/+7p/W7/0/9mjoUwqUT8MfMtejaRawg+pBXdOPwLfTIlwCgysYajIlEsMQuAAyVxIzm50NCiseDQeLiYHMVg9O2UUowamXYYFIuhFojsESDBIQ6jzmirz55z9IvPrc5rCiGGHLrRIFmCoaHoz1VLDTq2tc18x0MSogDAEjDemSjGrlHOdwJg26lOjN6DRMA4//uUZO0Igy4Jwyv92YBBgTipe5ggDWwpCC/7ZEEHhOHF7uSAx4xxwSwrnTKJxYdGhkIaNB7jE7fH0tGqIZWwIBl0/66O+qyO9Tf//+7ZTi2ujZ/qhkwZIJwM83NODL6KQE7VtpCOCXHTTCCxEww9GxjU5CIMNdOw0VwFTP0RNLhIw8OTQRqDAS/1WKvbaSeePDTVBq5KZF0K3HTrFPGLG0IPWC6GPCeKOQhw/Yt3MAdi1WRcsssonFb1NGmHgNErRacQPMIIMExy07Tc0MiOp8bY0iV3zIJEzMEaeOpw1FM4tYMtpHpzckEc5v47Y5cjT2Mlxlz/N/v9PW7o/+Z9zhPT/Vt/xdWTjCbxQEwslxPOUIaLjlcpls1jAaeMUoCwTDPh1YxisBLMIFBxQETBGAogPxgHYBAYAGAFGBCAOKjrjQhRIYMiI8wuE0B9wdXIyMmhqVHXFVn16cXoUsoedSZF5WTuoStAcc+UwNCTbKnvCjgHFFsRXzSmAHglDH0L//uUZPCM82wKQYP+4RBHgUhhd9oEDfgpCC/7hEEKBSGB7uiA/Neteo42jhzEbVAMZsgwxGCNDKnBJMFUE0wKAHTAPAQQ2X2BgGr9jsoEubKRy920YA60++vWn+j+v/+yv//0J7EaDTZq/Q+vtQRNozN/DIc2WoyjLe/NluPZTJQQ1EweEFgMTaG0zAoQOcxUsnLMISADTAfQCAeACwcD2v8YGmAqyMJoDLIYCnluuMmMgfDiQ6A5UccEBQ0HGhMQMPnSIiUwPrVxQUHODJ8G1pUQXFhND1gpcsGkVMnzYTEbTdYjLONiYaGf/+oZ//wk4AGAwrEHsMXpNszFTUY8zyMsMMZUNwTGlQ/sxasKyFAJQw24DqMAAAwzBLwoEaBMDARgAyVGAFgDrxGANgCj5BaQJ0FiSUzH0GvcU9vVNMMfUcxNTF/cVv1L08f1DP3xB33ccL1P+rRe1/PV6yl9Cj7aaxPETxpd1H1Xcdddy1XVXVPWN3uCjwEEXHTMHgZf//uUZOqAA5QJwg18IABIwUhxrwgAEUgvChn1gAJLKeEHPoAA//oFVTAGwkMz8hwYMLljwTc5LBszXpiUMb1D4DCwwZ4x9EB1MFxFoTCXgJsHA9oWAIzANgEkwCwCUAwA8iM7KsoWJpxW7iEZyrRBfTTppPzDTS79GhW7Q+lVd8Ct9DGLaxz2dnqJlIlbHKdj0iDCbKANppkg1lD5zWDXMMQwPswKAdTL5AUMF8LIwQgNyID2aVWSnl+OMrFOgXjvrVYzr7fZZ////2fo/7um7qAAlVc0YfwShs55FGY7IlBnbqvoYY4ZrmGqA3xjAaJ/OIZktJZuADxhORalBbAxCBGBLfKWycfh69PIZxnZ6Wepyp/H/VX2bE3+xCv997LPQgwIEMz1rszdRNDQxSmNMQr0xpQOzBhDNMjUA4zeDVBM3NLGHyIqD8M5kYn7JTHKGMwWgL5Me/XyzOhR8c1TiXPM4JT8DDkg2EwHikTYHC5MQ4BEzgANTak0SZDFwg5U//uUZKwI00IMQgd9IABAQUhw7wgACgQnFY9/oEC7BKJF32QUdGgqBZ5ugDisUxR3e2iXGuvU2+pd/apP6bWURs50V8dK9mZ1C3SplRxaiAC4HZiVJFmT0+iaqjZ5uOiEGIOJkYDuOdLg8Y1AJhzZKRYQuUMEby+pZ307U+iYIder9v/8z/////Vs6TA7QnsyzR6HMr0gSTS8cZg2d5MlMezBRzGvUzNmMD0wok1DMsDdPVEzigUw8tEDUiPAkndKRHRMKhcJB5ItQQVcydUhZ4AalUbdlZeT7Bb0s3/LqvSyZTzWe2hDU/3RjDB0HDZrnDB2huNW1143LhdBJcEwAiLzIBAuNGcp6A0Yto3ItjLP+PG1M31QlHyliv+O/q+zr/6f2/0/s6YAABl6qkwNcEkMWMNfDFI0rgwuBaJM4BEcDBpQgogvk4nDAwzuAw5Fs2lMaipeHMEv5X1H8kptP1+hCBzHXM0f+62zq+tf/pTVp/oXwBg2OhlH1xn5mxml//uUZMYM0tIJQwv+2RA8wUhxe7ogC+wnCA/7ZED1hKHF32QQM1uazpVJhvh1mAGQ2Yh4DQBpkRozDEoWOeTE+OD5hjKlLmETWy//6v9n/////+sQq6u4wHkJEMShPjjIKTCMxpplENBmBQjFkgJsk2s0rEQy5rY3HDky9YnEAkMccuzaK34ZvVbHubpeWlw/mh6lJpYKa9kvFAohaS83N36I3XQpBAzWysc48jXvdQ6cSAzcaTTZg0BZoCsp4xNZx79hzpOBo6ExgaoYJAYq0p8FgooFkrOJ3Buv3Il21izXa/9mrd+/S++P/+j0f+z7kdMCDFoyEM0B1YfOyCO3zWy9s84b081MpVI5TE5Sb0wEsE4MH9BLDE2wDwLA0JgEYDCYCKAomAggRhgDgAMgMjrVBKbgCrJZpIIDBAHK2MIQ2E0GTtZLsky9y0XEI9bElDQHuStR9wY6Ghs6s4JmTFD5oaKXIQMYkLta0Pl0ABoSZh2INmRbBAZpjWJkJtMm//uUZNoIgl8JxUv90RA7IPiBd9oEC9QnEy/3REEBBKMp3IjQCSCuYY5WpiVBYGDkAeYHgEBgPALjwGhaQIANpafVgZaWHV10gH+rfs1f/3aev//3dns//QaSEGvmxvs6hkDLpoZh4DkGH3pO5+th2YYkIIvmFIgYJgfZAiYG+AyGCxAgJgJoKYYEYAsqOGBXgBjfGBlACTvkaoy8bj2rCc61DiQoMBXBK96sm0wtbr0nmi3FLGvehr7HMqElTFtUdMlyLEJuS/E74gUCMu9Fb//9Jf//pMEUBXzIiydIxs1JHMiSJUzGQEeUzCYvCMUOBCTAigOcxKcL/MDIAXjCVQ8IwBECuCADNoiAZvAgAFX+dQXHgy7vHyOJRjKs3hrp6R+rnml+9Rw3WG5aIeepe6tJ5ZKTmFIFY3rqJbu7iK441VGSahO+Y675euutBnY3eXX2TiKqVNmUkcTAgRJtAIkLg4wuf//yh///iJUy/CHgORsfqza91WMxyU+ANMIk//uUZPUAA9wKwY19IABG4ThxrwgAD6wvDBn2AAJaKmDDPoAAlT7LVtkxsIQHMDmBrTIpRgsw4AGOMPRBfDCigFIwGsBvT6MAkAalymAdAOokABhtUGBTIB0XDCy4bBEXMknABUAxw9QViRo5CQqBrbCbUn0PQKuQtxNZYFHhas6HySWZODblHgyfYIzQ44kPyp02otSYf//pE3//JAACAAAGM2FGdKURZlVDyHtCiYZgHXB6Y0lmvkCsYAoi5luh6mE0GsY3JdQ0OejpDhgSgCtdMCwBBXYboR4Y6riWs5rjm5Whk3cI7T3EVP3Uz1PXLRDLKLD3pS/cXMwjTTL1jJj55jmXa7425Sfa2nu7uI+blop4S+iFD7RIyLxMKE0gu0UhZhL//nj3/+gBj+YBECSGC1KmRt+AvgaqE+8GMfsuhiIYZYYTWEPmKKAWZg7QliYRMA6GBbAHRgJQAuYA4AfmAigIyIy6pt0RAOhqHsUbF++pbHq9YcWKGPRWtKTt//uUZLeABF8MwoZ9AACPSYhFzyAADHQnDD3xAADUBOJXvBAEplbCWxHT75fUzsPdWvdTqAgABgdhkGQOAkYaaFBndLMGieYAYxosRhpgVmOMAUYIwA5gfgLGAcAgX5Y6HAIxmz2uvuWtACABhDgTiZhSSrmOqrUBuDLrWZfIqpmMkgfpg4k/m0sGsYqQ8ok6icrpGRCN0PUEYJfqGJacKrIyvfY8lUbHIb1afT2i1rOLWWNIIb6em5q9qpLatzaq1bd5fACQD8wKkhTE/dNMwVqE1viHzJIBAMOhNBZyCmg2kafTawUNBwMWMX03KImRv7ltVcxgBgLqYOciLmDDpIZouRpEZTySAGEZgrgCEg9qAQy4ZA2WCEAMQQlQxv270prQVfxnU6dPXt06/oXOavcrX7ftWj/9en/kHliAUDnahsZdSEhmPMXGueDEYOIeZhwThy4A6oAcKwcADN3bX1R9+gbVX+y3u////+n///v1/+kwU0AOMwEHsDGdGIA5//uUZIuI0tQJwqv+2RAuQPiBe7kgCWApFM/3BEDZBKIFz3QIetR7NlMNGzIKAX4w9h+jZgAkMPZRIw6geTNcyBQLLrmcRoTAeET0B027UYwOCrvfRRNaO3fYmS+9VZZ2l/T3nfXv+7VV7ofNpN8AYGINBhGIaGjKbMZ1Ky5qjEmGN6AoYWpMccAWKvwxcF5gsDigsB083bm1zj1LQ9tvv1f/1//+7SDEAmCqhhRgsi/kaE+RFG+nF/RqkYmsYKSFxmCAYMaLwP5gFHgGNOCCas6AkaV57gEJt8ek4OaW2U0aGsnDecZ++s45VzH7v27+z6qq/19td70IMRC4/GZjOCeTNYBYY08UfTCqEEMgcY/8JTBRdAwIAIBIhG1wunTfyhQssk5cKwiuE//+jqUweYBsM0hBMzW7nd05+8sHNpmH0DGXQEYwoUxDSUD7MKcnQzcQFDQOgDWYgGDDU1nU3yUS3RimfivaE38ijdUtmUtvf++rUfsO1I+lDaxncux1//uUZLSM0rUKQoP+4RA4wPiBe7ogCkAlDk/7REDUhKHFz3AIe/j4uSAAGCIEqYXZxZmMLvGxSgyZfCQRjkBOmOvEcuF5lXgtQGTEU0fXdehpX00Pc44n//////////1CijJgwQcOYUesCmQvLfxt3wbMaJSY5mC+BhBk92xxQRZlm75ziJBmXoLXGCCnjKL3qYwY90UdXelbll7Fsdpu3baW2IbjPwjNqaz8J1WuUBSyd4YGMxf51u0iCmMgYgM2KRoTfcPwMNlQMxgwuTDeSjKILWah+omONV6HiYRnyYUWZrcd+aEjxA3/07//3f+Q/s//9v61MCcC0DNzEJM5d1StOUxNaDWAmNgynoCOMMCHFjA2wzMwfsVxMIDATTAPABwwEkAmC4BeYD4ANIWMAaZbEzAYOj7BhSKj1BCViiHMXS4ercuccNdWRUhj8VDLyy2n6n8LtV6vkxikrWsag9mVVIaowrBRjEsR2M35x85jUYTNbXMMMsgAxFC3DDVC//uUZNcM8q0KQoP+2RA3wPiCe5kgCzQlDk/3REDyBKHB7uSCpMD0DwwNADxgAESBHXWWvxw5WHj7/anTq3Uao2jRRr/7+/t/T/+5Pp+7WYEvAPnCsIt5gV9s+dsAlXG2jcdBqUNOKYVqLhmIAA/BlMAiYYjkBFmO4C3piwgH+YCWBCqLmA1ARQYAFmAWAS6OBZQM0DGzXKU0YfR9rXqdxvibh1X1VbLY61dsK1T+O5bbpiqmbuT0TEPpszw+Irmf6n/4a72c8xue25m99XTvuJfK3Vbt0y1e99S96LlAyVWGIeSLCIFAVOf/6B3//NGFohQBjxA4CYyYUBGTKEghhw6XgaRcLymJwgRZgJIIYYT4F+GBUgTJhyQQ6YBOA0mACgDjgmAdADLLzARQAB3SVqsYVRFrospK8fNK8UvxmPea7zG//szf3itaJf5mYX5ldmS7ZrJ7GM+I26uzS3z3u67NW5Xd5106l8jt35bfa3XjctrztrN57aqvle/ncspn//uUZPMAA3YKQgV9IABF4UhgrwwAFAFPBhn1gAKBLaDDPmAAyLZQwQhMDhL//gZn/+kmATU1VGIKaAb98UhsdfKH8PEAYAk/hoqncGTyZ8ZDYUJhODOmNUCKYPAUZgFAOokgIIZg0/GZ0ami8Y1Yxi0ex/7fTze9vT6f9TGye3d//+8wMwuDIbI/ND9CE1ER0TOzHSML4NkwFRkDGlAjMBcGIOAXHANSgAZnSanN8qjHP////+3///////ZQYI0GEmLCudZljbfKcRgzRmeJJXZjswR8YBp3hqBgQGDANEZkgOJ27QZ+AAc9TJM38mqW+jQpFXt4u11MsYgsu5euz+16V6TNVXYY+n2Ldd6Nmn0dqeCEKzR6tzM+J1NVYhM1UAgTGhAHMrxoF/cIFQsZ2XItUDO7+tTH51iTPiq//////Rp+1dVmZmMBTA/TF6ij0x4AGTNM5RUjMAyNAkCHTE5xD4cGjDCkjP4RzDUkYGBJDtt/KuVu+5Pc2ldz6fTs//uUZKaI0oAJxTd4QAA54TiA7wgACqQpCg/7REDMhKIF33AIGf2WWU9vdZdo1CX/Xb9q/RqMCcDYxUkBDNdcuNjc9Q13SgTAZEOApEBqJGRekVsZqprOSgOkeuVuo7Nv3///Wn+r3f/////330GD/hehkPDY4aeO21G4sQB5rbg5qYoaAoGOuGUa6IgRhgpUGL2FeZghAIDVeGjyWcItthpK0P3qJHHop7qUWx7dG/Tnr+/oTT1f/fiiWeqv2MVQgwNIA0nuYxd22TagTjNOo7gyVggQaA+JKLmBus6MQFbhXHgFvuNc87Z7SSWf3dVf+9S9VX/9VKowMARQM6ha7DKUq7U3snKKOMTHlTCJRFAwJQIHMkrBSjA/RM4xGsApNd1UxcHDBg3M7GpeL7VZBFdYvVjMPDM+kWN55VMdDg5rJ+NppW1b6RsUaRXTLtHi0WUlhA0ppAOIaBTrjTyzVT87BtOskpjWzKWSwuABBNdV0YsHZvfdnoyyH++cG7s5//uUZMsM0loJxJP9wSA4YThwe7ogCkgnCg/7ZEDdBKHF32gQmZYygUhwVYxUnJjAKqvx3llz0lqXqO0eLso/2PTXZ/6mdttH/e+p32/9axGFyCHJlk7KybdciYGaIUzJsqxogZBkCcmD0bGbNIdBhqiVmdSCsehLmCCAWFDYjttIPljdlGAu24GkKFBDAjGBdACGMSmhilyxwSKRJyx+6nbKXV2B2t5Uv14j0WcXILRQ9dSblugYCIMpisG1mmOlga+y75heJRGM2E4ZTwp/8PBeEFum82VhRFC6j/lRzyQVTVrMkk7Hrveru9UZ6LrP93/r/t//9NUBzBFQoUybxYFNiTPFDE/ZuY0Vc+DMQ1CTzC/P9NIcJAwdT+TH/ByMnKwE8DQGDxtEukuyqTjh6mS18XS3Qm5mrYmpUxeMtlW1No9PMXOxRKhXh9pwU73rLZ40ZZEZxh02YrkgbhTAYwsF5p4uaGTotOZAwbRgAESmOSBgFqg2UWbGtJSNCTm///uUZPINg6MYQQP8GjA+oPi5c7kEDLwlCC/7ZEEJBKHF7mSAuAftASVOcu//fTR0b5pbtK1bop2UXp//9l/o5L2sD/5hGYbkZNslJGKeuyphp0/CZHslMGJ8gthh7pEmaKBWYMyVZhzA8HCXY9EAwfMYQHRjUzAYYiQeHwKVWXG46HD4xzWknz25q3vTHqaeQMFlJsJERa3UK3mz08YhClBly3kd5JcetCiYxoeGdTDAoA4MU1Nw00GWDLoe/NF9QwxEBBTCqSzQ8IgBoL0mKZkzhiSessx7dypYo65t4FKjxcolg77WC8z0Iqrqp/9S7aa7NPXad/1qMEwTYJaMlzdgDUmIB4wGCyAMT4W3TF8gzQw5F/TITAHMSY/UzGAXBySNeAjBA44AzVgl9JEZ3fPzqbARGLTCGR7oLhEnmKCOZUNbTUHhiN7qybmuY6lm5K25kVR4QQenaUKKEpkg8tPT9AAAAAsUbbbTBgFmU6Am55bGSvUHSC5maYGmF4WY//uUZPII0wcJwov+2RBFIShgd9kEDaQlCq/7ZEElBOGF7uiAIBQhsLHHDIs22+44lSpngeHTLe+/i6//1L/b3bCOn/+v2dH6TCCgvkzJtZDM7/JEzJxcHc0F9fyMQYD9zDkZbMSYIExcAGDQaA5MxwQGcmTih0paXqa7LmkWMv/PC9mNIrAlU4JniZDa5yFCFzCzHyaTihCefGGDp2/GFC5Mkw80UMYVspJPaxc6YICh3FjyWuCtz9SAOvurwsBCYHhSpi2HymCEiCerjQY4kEYAJWZBAUQgK6oFow0rLLyzj7btE61J/WM9KNrrt9/q//+zv9qXf/qx9NVpqqMAGB9TEVFN0yDBKlMr2bDjOBCncxA0CnMo+dOWAFMYKQNmBbMenAFAg0HKhNQ2b8eoxaUS9K3m3SJwHz5JCKbHNpbPgdZhxEyk8fXaNDITtebkBlyHCxQSwZkVnUuE9mbFjgukXkVIfYt54mA+Mc4jowC4CjBtg8NpIWoHKamGUBm5//uUZPGMg1EMwgv+2RBBIPjNd5ggDig1Bg/7ZEECg+Ll7uBIwOBWkJ2NrYodbEpre780URcTYMWsXs6GvPKOfc3u6Lau3kOr/+07f6vb7FWtCmqu/MC1AgDFhB/YxT0+cMr3QuTLNw4IwX0GeMNovOJgiMC5AMLQ5CVg7lgAHpdS7lN5NncfWZd3Yld7iX3Xm5Ydck3t8XV697pN95QaOpQvkrNrmKel6DUMCsIAKn/GguZuCG9zUdKDMNsNUwMVM4AAIYFEIAI1oHqvmTCoyIfJkrWd+kuxXX7f+3X6v319t37P0f6FAAAJqerMDlCiDCMUV4yb0uPNK4TkjO2gU4xTQCJMHnnA+EGSs0GzYVnEnBikLhjcj36s9lNhz1k9ikRl4Ui5qXaARjwxO3tLlGHhxlzDLXUzb67WNsqcpdg98a90ZbO7x5BndPHRWVUOBdMZcOU0gjaTEuWcPTtqNhw0CpZk34gSGGGDGEmCxFdlvL7oy6YPWeY7Vb2ZVq0t//uUZPCIw2wJw5P92RBEgShge7kgCwAnFS/3JED2A+HF7uCIalNv9/67vs9HuYJ9V3WS7jDkApk0dk3ZOdASfzuaZr83U0vvMYPD7TBrAmcyBAB4MFlBVjEbwHA5PcMwBjCTU0FoYXBGa+AEXmpcQeEpAy6nK3ihuSCer3ayzbf5nIZirnFft2zP2/X0/4hr/5647Zst8av2e9wc7+d5WKv7XzKq7W8ndbB0gTFVTRgNA6iIskw41BjJ7RvNUfHM3i2MKAxBXEABsa4Asld/GC2uuuc0eq27DjvY/md6UqQlte5936Ni//T1blY127aYAE0tVGBRBmphXi0CZOOuXG17KBx8DXWGg6JsYYoBBqQAxGHOVCZBYHQ/lAzMoEC81bsDTkav6w3ks5yTHTblnSPpf8rX1aM77pxwB+dE0z1v1OjWz3tHaguK3Tv7wBjlra2Nwe5lv+Ihi138l+nTTretsxlE44QSc04WRzV6YgMYVZQxrhAzA4DcMo4B8yyI//uUZPqIgywJxEv90RBDgSiCe7oSDuxDBA/saIkShCJl7uRIjAJ6MTa8NB5F+phpsRrS7S6ur1s29Wi5nd/2D031fFvu/7f6RgMJnB8jNAgJw1U14lOBbUETL3U+0xBYMcMNAi42AgRjA3TLMDEJoK/wc3GEBxq5mza1YgO2EnMh1rHgJpTHlQEagoLBRBoqRKlmkxdZKM4+lrlPzGxbhdJliugVUjsvFEwum+hjBNehQ9h4M1TMVAPjAFMSMzoBg0Nz4ToK/TTUSxCbxsEAII0KxBDYqObgDObooMCiUOf35X72eXT4q3/01//9n/mr/fQqMC3EBjCB4po1qQ4qOQ6OQjBcmB4xcoPVMEuEnjFrgG0wRYSNMLQAkDq8xrMYUmd2ImvMTMAhSG4hY+57fKbEIsbGOaVRJNQ5CjKn7wRIrEc2UM2rQ+FWn2NMnSiVFUJi4bwFcis6LKHHAWj05Bwog0TwuQBiQGnpeEZELLBshKaGpuZsYm4bQE2h8wIi//uUZPYIg5QKQzP+0KJBwShgd9oEDXAnCC/7ZED5g+JZ7uRIIxCwLEgWNB91EKb2saGu5qWXP4ulgVb/3f/MK+z1xvbWhNzf/T1L/3iEYPwCTmamjoRjjLIAcvmKamZlKpJjIYLqYEuJ+ERIYanNOT6IYQIAY0gKrCPJwsyLzkcClnMgaVK3MGbctV6uuP0bMf7Xz/j2p71C+t/7uu058v6l/e+UXfM+fbtBlwtL0A5ite0nS9O6ae70rK0fT260ZbjjaJLFAOYbzJg/TZwCvJwMVYKXgEUAKrSqBJZJaMehhA9wq6vZqTRzCbOWW5a7j3jFf7P9G9n6Lv//PVUwRQNVMLjiWTFLJL857QanNR/REjEmwswwAlljKOBDMVwyQzDAYz1Qk0kLFCAy1NUGdmJPVfGjmhseWc06IBhNBZU4P6LBrjR2Glw6mEy5xLVOe+cLAFAttbvXFzCGsgYkhi8IMa6m+JndKjIBKpkklBszwQs3+ro+ojo/tCwxBJkw//uUZPSMg5USQYP6QiBDAShhc9wCDmBJBi/0Zsj4A6O1zuwMAWQwmBGGxdwtMmFE4Cs4JiulDizWakFkHq6Bf07EV//f+v/8jYn20fzYBFTrJgSYGiY5oKdmHhHUhsG5A4Z2YPAmJdAXhiVnQBCcxmt4xyEM5CCetu55mPnO369oN3EFmWUqAYOunGrTs/S+oM5EPii1HdW6USzC3TKNPopNOvCWKOzLZGhD09SQCOm2jBsNDJKYDtGEDy1OTwZUAM9RiWSGSwWFelBkcSY9E0Gi9J832twsQfZQtdX6ew59dXRhL/Z791id/9vrMJ7EejHCInY3AUuyO2mUzTd/xpAwVcP/MEdkwywQyzA/UoMmEGs8lCHqICgJzwYTANrbdaLP7w4uvbQLk/i3D3K+aipWb/vdfteO6/moPt//+XDNdou5ptNG//V914bd40Ha/6sYe5oR8ml/PV4/fPtstjAw6Oq5gymFxzU4B+PndNMhi6BDCGkYIlQRUrCgExoE//uUZO8Ig18JwYP+2RA/wSiWdwI0C/wnDy/3JED+BCKl3mCAoEXve193Vy73MVz6pgs1n28UtR0f/pO/2/3pN/9kcgXzBMAagyrcrINMKYVTfMUlw1VYbEMU7AMTDlQ4M28NcwpR2CJXY0atFocUCgKau5/0lnCsM44w/BFa4v6vESPj+dfqLwk/uLedZHy/+Z05XiAbtCB/Ju3eIHP1JlHc93ccre8f88i22//zm9XM8prWtoAYJQQ5iNmPmaUt8bUYhhlcIdGNEEyYOvB8ENhS8jMEHI0VE1ht63je60SrVL5I6MGCmX3f3I30/ov/b/d9f6G/9CoAJKKqYOIGpmKWpn5lNipUatckqmeJlu5gkAXYYmS8fqiWZauOcXh0ZggcEYYAGdUpAco3BQfWUUHFtLIOPYFTVxfUgiTJVGEoVLXKY9MYTYL4aa6JHc5qF5XQAKUSL3LcWkx1RaiXs2gCKVpjC4JN/b8zRw+zR4E9MT6vMvxJMKSgFqHaeJBD//uUZPeMw44LQQP+2RJAgThxc90ADjQpCC/7ZEkJBSGF7mSAEmLZtQJqjqqN8beZrbSxvq3UOR+wj+OSvsT+r7vo0N7kbruuowQ0BzMauKcTO2x5EzSFA4MfKKNDDygX8wdY48pHsxbmUyhCoLEmDgCVRMLgIZLKr1OK3+r6vxBBY0BNawKGo42KMeLvQBBqSVb3BiksXENJgVccEzjqXq7hz4bQqlhN7SjHorWRoEyNAUaggDAhBoMN4qAybEZzXfKKOL+4NSSAMGxwFwTJdAi0C801Iy+fUHRiUB4daiMqAECiqP091Tada9/+593ur9RQZQS/b/9ReiWJYxxi8jPCxANwI1s7Vu+zLHq5M8UokxbQPzM/AtMDsbcMDlDAcAEAOicFgB2Xxfsyhc8vvf5koQWlqHMdqSXfNNILPPr04vpdajtTf0Gbza7v11He4U7Xx1YEGE+CNhjZB6MZH0bimVdJ75kKpeWYyKHBmEFgIZhQ4EaYBOFjkwLIYBgA//uUZPIMg0UJQrP90RBBYOiWc90ADbhPDk/0RQEXA+GF7uRIQFryUAKMAGAJ0w3HuUoaD6jS7VSKWEa6WC2PaswLjjVKkInnona0A2KvnVDReVFkEvQlTkI7E/vIlWKtrFzaI4sA/xtyyNMuPaTWbykkyvFJ4OUigYDJYRHwwwQBIMisEwTEhAcExncTlMPoBYjAAgE4MABzASAD1AIYF2AIQ6OCaGpQaEslkw8aWcXNzCkC0tebnkG7x+hNybjZrki8wVhSnolR1labUD1tQq4Yzdr5IOHXUkUujOKztT7lbgqtpsw6ghRug0refPr6RieNj7jY1Vo3RM7rNTyS6CDXnz72oQgf01dKHH1GKIaq0MRRRPsk130aqxaF0k7/////////9SDv/////////9SgAogAIpQoiaYa+XHbxr2xnzge2aviHymIZwWBxKS/4Y4uASGHNABxiUwP+YgYBPmDkBc5h3wBUYCyAgKxmBRAGKChWBNwMeeNmoIaaOMO//uURPEAAtcVw5V4QABo4ShBr4gAGgYE/hn1gALqL6AbPmAAOKwmtZ5K6yBjY52p0W7a9li0d+xXciaX4vmMcmSdJxUlHBCBAkayrC81zrskoVwst+gWQQfG0+AJA2G5gWdqyZrSpkrZoLohhzsac6TAkk1wnpkJVqdJtL3Z0vqBl761yyz9pY3//iE9//qElTItWak+9BgpMw2TADStCXI1jVQLNPvUUTB/hl4wqwAEMRmCbDCQATUxqEQaMIyA3jAmgDCKGA4gJ7MDACwGZdKhgHOPuo9HO4i53qIlHjYqIFkIpS2NFWVEIRWD4xZ4wc7kOdZhcimQ5bnJQUUWGXE3UxXY9CI5DiTi58iGMeQgiyJOqFc9iVKWejkjCkGucxXYgqdyWV2OawoxHe6ujIpF/////QQd//lRKaG0l51oAwGCansaYo0JiMJcGXWcWY1RCxg+gBmZ8J2YGYM5legekgQAOAJgYu1DjD70DNAdpq5QPXV1FCFVziFhrNmE//uUZHeABVVwQQZ8oACHqygwzwwADgApCl3xgAEEhGILuhAA0+VzB+ovIpBmWEMbDQvSOe9Feu5HEi/mlT4pwjBiJ7J+RnKnNselNJM2NxXxf5FLGpQ2N/4DNgwYPEwFR//x7//8usVVlMGCD2TGqjr8yGQFTMmxW7DM0SewwlsBXMCFCBzDBQPggCUjA4gHshAOTAWABUuoYAcALu6/tjIU4dM1h6hRsDBafecfM7Awavic2sMSOxQfLN203uBxxEVlGMJIAQteF1ElFZB2cvqbERwVysWZVIDAcDDLRtDfvpjf3iDudQTBwZgsdZmeIQkJ6C7oiwCsRi3a2RFDqiWmJRA+2tm6v1+LL7r/+jFF3ImWX////WptTBuQf4yMBKMNLPWpjHmoBM01AYlMRZAYjBQxB8wj0EkMmesNHyHMUB3MQgLL9ExpItQLIoJQPKAThAqkX6Qo82NogbH1aeXCY3Lkf7feGfOpy/lSI/2uZn6+TeTfMydvNvlpG7V8//uUZDOMg/dcQQv9GbJIYPhle7kSELUnAA/0Z0jyA6KlvvAAr6ZLmeh3zVUY/89c71bMQl0rhLawqDskAHiDAJBUMIMlwxD0XzK2TFPDo0NZBLMAlFMawGJM3+BCJQRRsUshRp4s+VxKI5Ao5Q89d7PsrTWX49WpVHpO/1L66v3qb39KTAhw8QyCliyMOod5zHOpq42JUSJME1DDjAdhU8wTUEiMqcIG3CMCyQAxgAAGTD4NnRmpudotWJ+tc4Al0iIq8q01JARhmhohn5itaRxS2emeS7i3pbdJUBZ/tPIzNDzhmTX64iniV+9OXZjrmVwmxkrg0tcshZBfD5fsGvB0EXL4udKM3TAe1VTZhwec/TnARqGsdTHEa+ZpFo4sQUEBQFqQUUU9aoXLiqyWmpGK3yTbvUyn9Po1d3/q+d1f9CEdNCEVACABg/wSqZFOYAmnMFuxmaLeeaFQP6GKBAepmd6JnwThiGY5xuIhgkQIcNhYAIw2B9mlHqCqYzML//uUZByIw70ZwSv9GUI6gOiCd5sQDslhBi/0RQkBBKGJvuAQwTnOg6cvhXOA485CGEqI8Pzqkbq//WsxpNn103+8l1e8jy8V27nu/0QC+GP+bHd33tTx9Dne+RRCd/ZlfvzhKsoVApMYG5N54ANGMxMmeo1eOgrsm6ACNpEGt3VDuBnC10W6aEE7NF6NPfrvr9WrfiDlKfq/dr/+pkymAfg95iaSB6ZtqR/GZoLSxlSZIiYRSCemHd5nDQiGBstmlIfGKAxEQUsuFiLl9Bh0VdAylYjugsHYZ9XRr3Td09yKkzIxEXd2Z22t3cjsf0Z3Vn/NlO/nIjR2Yuh3d5Xk+sy012d/Wr1I7oV2GaQodoZzzp9CQABj5UfMpGXPrmFHuGglGmdgmGBB0gpwAYomiDFLd42mHcKzzNjVXpOBJq2QnKo21019v9f+nq20e3sT3bfsSmWMEPBYjFfBYAwxM7lMp9RcTEgiQgwsECkME3nOPApMVorMXA0IhDGgtQ4g//uUZBYMw08Swgv9GUA5oOhxd5gSDmBVBC/0ZMkJBCFBvuAQwDn9mt0NpLKU+dI+qKBF5gTE4mYYp2qlS6s8lN59BtbIu0gdTaxguTcsRkbXHnn3libqnSJ8vaXm1I3sqinIhQDDE2ADY6YDSPGzw/aMjFcwshSbYAzTNEATHZVKRGI2VvToltVdikI/1eltVX/6aPR3+lW3196UAMAmBjDDqUagyBhC+M0UStDCLC90w1IGSMdH3PJBFMpnlNegYMSA2UtRMMMgJbwWJBjMSrY8IQRgj/PyQWTMkO4G3OsbMRLds++eu/Z94D7+R7//ll5D5J7W5zZZd9431T7Vs96df9vrJ/+jp/cBano+tCSd2bm55qG83OHOi8mZYFmBIykU2kPESwQB2et9froa8aOeu+YDOxnVZvYEv6Urfezeru03EpLt56GGVr/bie+7uidMIYIEz/zRzOBGtOSmes0YW0jD0EpMYBxOBhHMZAiHolO/RC1UwZfIrOrmzJWZ//uUZBeMwtIJwovdyRA0IOhxd5gSC4ApCC90ZMCyA6HFrvAAKrofPvgmxNSqjU1vs581qc5jgKOeKNyRS4YOlWPUfSyg1XZsZzPeRY5x6rBAoB5A2ZpZN5rTL5/EoGQyIAgKLaFRRC4Qk7ZhwDzkZrErjSv7af9Xp9X//0/7O/+6tqmgIYGgjZikW3mvJOydekJBtbKpmO2B2YhgYceBqYNKAZgBGgEUbZGSgJLyjMLYMEwifS+hqzHsb5cCISSsexts+tDWKRRwMK3LUIY4CvcIidjnPU5+1taEuRJUPUwCpz/DDDGSjTh9j4g2AVlAgkCLKo2nFFJHUlIN3oCDuyoUahLt3v3f/v/2UQAzPVRMTj/UIMilXw16yuTOxFcMI0Hwwo8AeKDbZNMLjDzyxCFzCm2aaFHVZJLRer1JT1r0sVWgnXPU6NheijN2JtXprV7f2XpAAACqjjaIIKqwgajdUU1+LP6rMCBJBw1JFpsLl9fNq0K3CPQ1NrNGv6+j//uUZDoIgkoHRLOe2BAygPjtb0sTCwAhCA9zBEDGA6GBzTCI/v/3ej//06zB/EuBsSBrHq+nb4WIbPgNJk3AmisAO1BkyLADToFLvq/Y0ETnqqxV7LgiXa8/tCiHRi3pVtbc+ZZWtaK7D6kdoik7VsIvc5lrbQhQqRFXSiULdVjErQ+1HA0GNDyk3OsS43tFDPYCEO4DzRwG3gvGdViR9LP/t+m44i1fQn//b/pySbf1+ohZ+xUCDIQET2s5DeGMROl0Ks1JRkzD1C5MTR42YFzF4cNZgAEBQaBzMy71UBhh4SMpME6C8NaGXLixuKOc6q8rrE78W3v7UGbaC232abr6G2SLjkX3fJ8ADAmYDZ5h/JHV0+YziZjYPClgSgtxU80hzlBOMtYvigcqh9w1Kv9urb6v/f//6PvWQBggB1mAMyCY8bHhweDBnzVpGmotGFqADRiGFJVg4+kryYT8kyZD+6YPOeJSLzYiF5jTnmNA8OyyEUR3XOYei19Gur0T//uUZGUM0p0Hwgu+4BAxQPhxc0YkCpQlCC93AoDPA+GBrhggCndwHSkl3k+Rd7HvYw/StCo73s3DUTyRoMsw4yEIyQ0MZbAuFqcKofhB5czH/3/mXzj3OkP3+r0U+309C9nt/0+pWpUAABmlmkxGBjnEnNx7DPM1qN77cy0KiU1JNjpdV0xVq8sd+tM9uTSsJLKizKbOvd93Tkv9EUF8w+XfpZt0RRZP/6SAHQAFlsCrQddF501uAnMKqQsVidI1QpesgCdIPYGS1qy8fJCpsiyh2v071/9P/0+//9KP9/vbcwDFQKccbfIgfoU+IZWMeBRAEoPT5gr4lQTicaXsuzCDhhtoeMNHLLCWePD2mQFRbYvYt+vS/bo/M/Yy5xtOUnswxNuv2aXuqK6pi01GXXQRhg79uMdEBk2KFYczOtLXdBubvtJJZym566mfS2x/93R7pH8Z+hj/v9Xf/1IABR++utSBp1sGibUfbwBpJ8g4rgm4jOS9HpRSf6MbZLSV//uUZIwI0g4HRMud4AA2gOhhc28SCTAfDC7vRADPg6GFzbxIODGTNBsWcHK1OSKdLrxju2pDdaAMbI3dHd0+ds9nLzz9LqOBgEGtZiV+dGKHJF48skrRXKSjcUxhkeswkUagZb7+91H5D1a/J/f+77P/fo8rstp3AQYHB+Yg3ia4cSezYSe9wxnchDsgH9gQ0ErrOoeyf8GDoOAUKBYqHWNE6TIOZJAo9RpJkeeoVUy1K0nlLKkMtyaBY43SZTaLgAXWRd9gk8pi76HN9S2JeExCk2DY9isF1lKyURH5GLwPB2ypmMeyxNGhXXT67P/Z7+//+/0b+ulHVckwFBM1OfEyMpo/b7g5eXUzgAEKnwLLhluCBUlAYzPONaGQWhoDkKskMREy1crSM5C9joxSc9FMlqyjOc0jJXdz+uRTBJbRc5nmfL8PXczvmZnqcJMGxasRltIRnWrEp8hT9OP4xkvmoBp9GuxtpQkv7tAxMc9oc9cfRA8UuYiiEbw+lLbx//uUZL8IwiYHRUuZYRAxYMhxbywgCrQdCC7zQgC1AuHFrCSARCLlzd239juliXd6f/Io2dWn7PX3tpq9CuMMGRAMZLKO7zcPSuPONzEMHQjIAMMiwAMARUGgfZrBT+sVlAwE9q9xFRmXoRFOTYWKG/1yVwyUaG5mqK46XXOmSKbamTnFzgcH5Ez2m89dEiTJu0Gbsg9cmO0qCIWLzsdbCPJqYIg5NIZyDJMzNqFKx6doyEaP7WmRkzAkGxAw6A1XM80c6a0eK41ML1ybS2AQ0EiAInnhFjKZ0MJfm0LRQLHNKilzx2bKPSNPoCTHmUmCO+6wjtstZvXIbG2K83UhdSAJGsYMcguJ2n9G9BAUGZH4fqROKz3hisnoagmomLcZzn2iBMkNEjqpBbEp1PjMXa8izrHpKlM0R6ll0bbKK5E+kY5SKQ5F0PVzYpZ+UbWwpoRvEQ7QTFk1P8jK8NmfNUThQHR8HTgdxqMAhkRnMQ0OGsC1kfZSiVVvB4PEjY88//uUZPCPw3lVwIO7GTIwILhxaykSD5WI/g6YbQkSA6EBrKxIgLWBTStLExV3ek+G3htzWs778oLrFP32121dG7aLa35NowVbYh6rkorCMrKgwFCqyZk2GlVpvyQgXKkEqiv4BgSIJB9QsRn1Vxkz240LMjL8kOLC5EpKZctJW39655chnkXJTbdTlJqfKcmEbM8jNU9pprpmslP03OK5eeicntgsabX1unCUvSZbntXS5ocM9VBhZEQw+RAqDrMiyg8Co5w9cOPCSkkWFUswMgJxZKhQohVljrENzaXyLSqDwqLiB2NfcslRlbmOAipx73GhemPFiyUUWqkRlLukbdwHIfse5yvloOhFJ2EKspvv7dKz+IBjMR+YLrtxKWI3ZUj/3TWWuofqrvtmsjLMrqbjvUZDrArov/so8lVvTOr6N4478eHTOwXisAHKrCtoNEXGdYFBkIONQOFqbHMbVKtg/mF4FHc3XL32r0RdC6ViBh/1Njpq2bUm/74w3X9N//uUZO4I81hZwAOZGTI+QKhAY1gADE1NBs3gYokgg2DBhgyAd7qcX3Alf+Fa8zD+ecU4FTc4NcF+9qX/Mippe9YC+f+lAAatfRaIRNOh1UEFgqXEpgyXRrXWjJc7AZLrPXZMY66pUqtVSAjVSUqpBhXrszUTD/qs1X/bX1h0mP2bUul9I/qxVjdJm/gYVRJFON6qf69WeXVIMKKHSjbEzWH/8Y1LNhWIqUEEQWBoAo6tqqTPSZxMDBSawCb2ZtYBUBbVSh1WagJawCL9Yx+ucY71Ywo1EjCrp1q+CuIoi/iIqd/U8NfbJEZUt/1hpUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZPIMYpcZwZMvGKJWY+gBYMMsTIFs8kwwZckRFB5UkY0yVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uUZFGP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV" ; </script> </body> </html> diff --git a/artwork/boot-animation-source/render.py b/artwork/boot-animation-source/render.py index ee85ea06a..66a32dbcd 100644 --- a/artwork/boot-animation-source/render.py +++ b/artwork/boot-animation-source/render.py @@ -18,12 +18,12 @@ W = int(sys.argv[1]) if len(sys.argv) > 1 else 1280 H = int(sys.argv[2]) if len(sys.argv) > 2 else 720 FPS = int(sys.argv[3]) if len(sys.argv) > 3 else 30 OUT = sys.argv[4] if len(sys.argv) > 4 else f"droiddeck-boot-{W}x{H}.webm" -THEME = sys.argv[5] if len(sys.argv) > 5 else "light" # light | dark -ONLY = [int(x) for x in os.environ.get("FRAMES", "").split(",") if x] # debug: render some frames only +THEME = sys.argv[5] if len(sys.argv) > 5 else "light" +ONLY = [int(x) for x in os.environ.get("FRAMES", "").split(",") if x] html = Path(__file__).with_name("droiddeck-boot.html").resolve() tmp = Path(os.environ["FRAMEDIR"]) if os.environ.get("FRAMEDIR") else Path(tempfile.mkdtemp(prefix="ddframes_")) -tmp.mkdir(parents=True, exist_ok=True) # FRAMEDIR=... makes a long render resumable +tmp.mkdir(parents=True, exist_ok=True) t0 = time.time() with sync_playwright() as p: browser = p.chromium.launch() @@ -45,16 +45,16 @@ if ONLY: print("frames left in", tmp); sys.exit(0) COLOR = ["-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709", "-color_range", "tv"] -SOUND = Path(__file__).with_name("droiddeck-boot-sound.wav") # made by sound.py; muxed in if present +SOUND = Path(__file__).with_name("droiddeck-boot-sound.wav") cmd = ["ffmpeg", "-y", "-loglevel", "error", "-framerate", str(FPS), "-i", str(tmp / "f%05d.png")] if SOUND.exists(): cmd += ["-i", str(SOUND), "-map", "0:v", "-map", "1:a"] cmd += ["-c:a", "aac", "-b:a", "192k"] if OUT.lower().endswith(".mp4") else ["-c:a", "libopus", "-b:a", "160k"] cmd += ["-vf", "scale=out_color_matrix=bt709:out_range=tv:flags=accurate_rnd+full_chroma_int,format=yuv420p"] -if OUT.lower().endswith(".mp4"): # H.264 for sharing, previews and anything that won't play webm +if OUT.lower().endswith(".mp4"): cmd += ["-c:v", "libx264", "-preset", "slow", "-crf", "15", "-profile:v", "high", "-tune", "animation", "-g", str(FPS * 2), "-movflags", "+faststart"] -else: # VP8 keeps software decoding inexpensive during Steam startup +else: cmd += ["-c:v", "libvpx", "-b:v", "2M", "-crf", "10", "-deadline", "good", "-cpu-used", "2", "-g", str(FPS * 2)] cmd += COLOR + ([] if SOUND.exists() else ["-an"]) + [OUT] diff --git a/artwork/boot-animation-source/sound.py b/artwork/boot-animation-source/sound.py index f0f7eb1b8..6919dfbc9 100644 --- a/artwork/boot-animation-source/sound.py +++ b/artwork/boot-animation-source/sound.py @@ -22,15 +22,13 @@ FPS = 60 ev = json.load(open(sys.argv[1] if len(sys.argv) > 1 else 'events.json')) OUT = sys.argv[2] if len(sys.argv) > 2 else 'droiddeck-boot-sound.wav' FRAMES = round(ev['DUR'] * FPS) + 1 -N = int(np.ceil(FRAMES / FPS * SR)) # exactly the video length +N = int(np.ceil(FRAMES / FPS * SR)) rng = np.random.default_rng(20260925) -# note frequencies def hz(n): return 440.0 * 2 ** ((n - 69) / 12) D2 = 440.0 * 2 ** ((38 - 69) / 12) D3, A3, D4, E4, Fs4, A4, B4, Cs5, D5, E5, Fs5, A5, D6 = [hz(n) for n in (50, 57, 62, 64, 66, 69, 71, 73, 74, 76, 78, 81, 86)] -# ------------------------------------------------------------------ building blocks def tt(dur): return np.arange(int(dur * SR)) / SR def edge(x, a_ms=1.0, r_ms=4.0): @@ -70,7 +68,6 @@ def put(t0, st, gain=1.0): if i >= N: return j = min(N, i + st.shape[1]); MIX[:, i:j] += gain * st[:, :j - i] -# ------------------------------------------------------------------ instruments def wood(amp, f0=520, size=1.0, thump=0.0, dur=.35): """Hard wood / toy-block clack. thump adds body for heavy landings.""" r = [1, 1.93, 2.81, 4.13, 5.62] @@ -142,24 +139,19 @@ def bell(f, amp, dur=1.6): y = np.sin(2 * np.pi * f * t + 1.1 * np.exp(-t / .3) * np.sin(2 * np.pi * 3.5 * f * t)) * np.exp(-t / .7) return edge(y * amp, a_ms=2, r_ms=40) -# ------------------------------------------------------------------ tracks from the engine tr = {k: np.array(v) for k, v in ev['tr'].items()} TT = tr['t']; dT = TT[1] - TT[0] def at(name, t): return np.interp(t, TT, tr[name]) def speed(xk, yk): return np.hypot(np.gradient(tr[xk], dT), np.gradient(tr[yk], dT)) -X2PAN = 1 / 330 # world units to stereo pan +X2PAN = 1 / 330 drops = ev['drops']; T_L = ev['T_L']; LOCK = ev['LOCK']; HOP = ev['HOP'] events_log = [] def log(t, what): events_log.append((round(t, 3), what)) -# ------------------------------------------------------------------ cue sheet -# Kept deliberately sparse: one sound per story beat, with silence between them. tsamp = np.arange(N) / SR -# 1. the stack: each piece lands on a muted note of the final chord (D, A, A-echo, F#); -# the lock then plays them all together ai = drops['archDrop']['impacts'] put(ai[0]['t'], pan(tock(D3, .40, decay=.5, sub=.8), .1)); log(ai[0]['t'], 'legs land: D') @@ -171,21 +163,18 @@ for k, imp in enumerate(bi[:2]): di = drops['domeDrop']['impacts'] put(di[0]['t'], pan(tock(Fs4, .28, decay=.45, sub=.45), -.05)); log(di[0]['t'], 'bowl lands: F#') -# 2-3. the balancing act and the jump play in silence: the stack and the lock carry it -# 4. lock: one snap, then a four-note chord that rings out put(LOCK, pan(tock(D3, .24, decay=.28, sub=.8), 0)); log(LOCK, 'lock: soft landing in the stack voice') for f, a, p, off in ((D3, .24, -.1, 0), (A3, .28, .2, .02), (D4, .30, -.2, .04), (Fs4, .26, .15, .06)): put(LOCK + .018 + off, pan(epiano(f, a, decay=1.3 if f < 300 else 1.1), p)) log(LOCK + .018, 'D chord') -# ------------------------------------------------------------------ room, master def room_ir(dur=1.1, rt60=.75): t = tt(dur); tau = rt60 / 6.9; irs = [] for ch in range(2): n = rng.standard_normal(len(t)) ir = lp(n, 7000) * np.exp(-t / (tau * .6)) * .5 + lp(n, 2200) * np.exp(-t / tau) - ir[:int(.011 * SR)] = 0 # pre-delay + ir[:int(.011 * SR)] = 0 for d_ms, g in ((13, .5), (19, .35), (27, .3), (38, .2)): ir[int((d_ms + ch * 1.3) * SR / 1000)] += g irs.append(ir / np.sqrt(np.sum(ir ** 2))) @@ -196,13 +185,12 @@ out = MIX + .14 * wet out = hp(out, 28) fade = np.ones(N); nf = int(.45 * SR); fade[-nf:] = np.cos(np.linspace(0, np.pi / 2, nf)) ** 2 out *= fade -# gentle peak control: soft-knee compress only the loudest transients, then normalise pk = np.max(np.abs(out)); x = out / pk knee = .5; ax = np.abs(x); over = ax > knee x[over] = np.sign(x[over]) * (knee + (1 - knee) * np.tanh((ax[over] - knee) / (1 - knee))) L0, L1 = int((LOCK + .06) * SR), int((LOCK + .76) * SR) body_rms = np.sqrt(np.mean(x[:, L0:L1].mean(0) ** 2)) -out = x * (10 ** (-14.9 / 20) / body_rms) # chord body held at the level the team liked (-14.9 dB RMS) +out = x * (10 ** (-14.9 / 20) / body_rms) assert np.max(np.abs(out)) < 10 ** (-1 / 20), 'would clip' import wave diff --git a/docs/agent-control.md b/docs/agent-control.md index da7e78098..c7d16c2f9 100644 --- a/docs/agent-control.md +++ b/docs/agent-control.md @@ -75,49 +75,49 @@ artifact or file error, 7 debugging tools disabled. ## Commands ```sh -tools/droiddeckctl state # schema 2, below -tools/droiddeckctl start steam --wait # --ui desktop, --url steam://..., --timeout -tools/droiddeckctl start steam --wait --reuse # keep a Steam session that is already up +tools/droiddeckctl state +tools/droiddeckctl start steam --wait +tools/droiddeckctl start steam --wait --reuse tools/droiddeckctl start desktop tools/droiddeckctl run /usr/bin/foo -- arg1 arg2 -tools/droiddeckctl wait ready|idle|failed # --timeout (default 90 s) +tools/droiddeckctl wait ready|idle|failed tools/droiddeckctl stop tools/droiddeckctl resume -tools/droiddeckctl launch 489830 --wait # Steam game by app id; --wait = until it has focus -tools/droiddeckctl wait focused 489830 # gamescope focuses it AND Steam lists it focusable -tools/droiddeckctl wait game # any app but the Steam client (769) has focus -tools/droiddeckctl quit 489830 # Steam's TerminateApp, then SIGTERM/SIGKILL -tools/droiddeckctl focus # focus properties and every Xwayland server's windows +tools/droiddeckctl launch 489830 --wait +tools/droiddeckctl wait focused 489830 +tools/droiddeckctl wait game +tools/droiddeckctl quit 489830 +tools/droiddeckctl focus -tools/droiddeckctl input key ENTER # names (ESC, F1, UP, A...) or evdev codes; --mod CTRL -tools/droiddeckctl input text 'hello world' # as US-layout keys; --commit for text-input -tools/droiddeckctl input tap 640 360 # session output pixels; --space fraction for 0..1 +tools/droiddeckctl input key ENTER +tools/droiddeckctl input text 'hello world' +tools/droiddeckctl input tap 640 360 tools/droiddeckctl input swipe 100 600 100 100 --duration 400 -tools/droiddeckctl input pad a # a b x y lb rb start select guide qam up down left right +tools/droiddeckctl input pad a tools/droiddeckctl input stick left 0 -1 --hold 500 tools/droiddeckctl input trigger right 1.0 -tools/droiddeckctl ui dump # tagged, labelled and clickable nodes; --all for every node -tools/droiddeckctl ui click rail-steam # by test tag -tools/droiddeckctl ui click --match "Play" # by text or description; --index for the nth match +tools/droiddeckctl ui dump +tools/droiddeckctl ui click rail-steam +tools/droiddeckctl ui click --match "Play" tools/droiddeckctl ui set-text some-field --text abc -tools/droiddeckctl guest -- xprop -root GAMESCOPE_FOCUSED_APP # --timeout --env K=V --cwd --stdin --check +tools/droiddeckctl guest -- xprop -root GAMESCOPE_FOCUSED_APP tools/droiddeckctl cdp targets -tools/droiddeckctl cdp eval 'SteamClient.Apps.GetMyRunningApps?.()' # --target, --no-await -tools/droiddeckctl env set DXVK_HUD=fps # next session only; --persistent until cleared +tools/droiddeckctl cdp eval 'SteamClient.Apps.GetMyRunningApps?.()' +tools/droiddeckctl env set DXVK_HUD=fps tools/droiddeckctl env list tools/droiddeckctl env clear [next|persistent|all] -tools/droiddeckctl override gamescope ./gamescope # first on the next session's PATH, removed after it +tools/droiddeckctl override gamescope ./gamescope tools/droiddeckctl prefs get session -tools/droiddeckctl prefs set session some_key true --type bool # returns the previous value +tools/droiddeckctl prefs set session some_key true --type bool tools/droiddeckctl logs latest ./session-artifacts tools/droiddeckctl screenshot ./screen.png --display 4 tools/droiddeckctl record ./clip.mp4 --seconds 8 --display 0 -tools/droiddeckctl displays # logical ids, names, sizes, capture ids -tools/droiddeckctl wake # wake the screen, dismiss the keyguard +tools/droiddeckctl displays +tools/droiddeckctl wake ``` A sleeping device suspends the session and hides the app's windows from `ui dump`; `wake` first. diff --git a/docs/development/stores.md b/docs/development/stores.md index 4ce7d1aab..79b1e2b73 100644 --- a/docs/development/stores.md +++ b/docs/development/stores.md @@ -39,11 +39,11 @@ volumes. A successful run removes the cache. ```json { "version": 1, - "store": "epic", // gog | epic | amazon - "id": "Samorost3", // the store's own id: GOG product id, Epic app name, Amazon product id + "store": "epic", + "id": "Samorost3", "title": "Samorost 3", - "exe": "Samorost3.exe", // relative to the folder, forward slashes; what the icon is read from - "launcher": ".droiddeck-launch.bat",// optional; what the shortcut runs instead of exe + "exe": "Samorost3.exe", + "launcher": ".droiddeck-launch.bat", "args": ["-EpicPortal", "-epicusername=\"Name\"", "..."], "env": {"FUEL_DIR": "C:\\ProgramData\\Amazon Games Services\\Legacy"}, "installVersion": "1.4.0", diff --git a/gradle.properties b/gradle.properties index 1eda82b0c..3289cc4c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,13 +1,9 @@ org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 -# Reuse task outputs, skip unchanged configuration, and run independent tasks in parallel. org.gradle.caching=true org.gradle.configuration-cache=true org.gradle.parallel=true android.useAndroidX=true android.nonTransitiveRClass=true -# The NDK the compositor is built with. CI passes the one the runner has installed -# (-PndkVersion=…); this is the fallback for a local build. ndkVersion=27.3.13750724 -# Enabled parallel sync for Gradle 9.4+ org.gradle.tooling.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5f0709380..b29e48e44 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,3 @@ -#Fri Jun 07 19:24:10 CST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/gradlew b/gradlew index 4f906e0c8..5292cabd8 100755 --- a/gradlew +++ b/gradlew @@ -16,16 +16,7 @@ # limitations under the License. # -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link PRG="$0" -# Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` @@ -43,10 +34,8 @@ cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { @@ -60,7 +49,6 @@ die () { exit 1 } -# OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false @@ -83,10 +71,8 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar -# Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" @@ -105,7 +91,6 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi -# Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then @@ -121,19 +106,16 @@ if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then fi fi -# For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` - # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do @@ -141,17 +123,15 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + CHECK2=`echo "$arg"|egrep -c "^-"` - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" @@ -172,14 +152,12 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then esac fi -# Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f93..cb765c36a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -15,13 +15,7 @@ @rem @if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## -@rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 @@ -29,13 +23,10 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% -@rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" -@rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe @@ -65,21 +56,16 @@ echo location of your Java installation. goto fail :execute -@rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar -@rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end -@rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 diff --git a/tools/aaudio-sink/build.sh b/tools/aaudio-sink/build.sh index 0f0f3a87b..eed2a2abc 100755 --- a/tools/aaudio-sink/build.sh +++ b/tools/aaudio-sink/build.sh @@ -1,9 +1,4 @@ #!/usr/bin/env bash -# Compiles module-aaudio-sink.so against the upstream PulseAudio 13.0 headers and the daemon's own -# libraries the app ships (app/src/main/jniLibs/arm64-v8a), with the Android NDK. -# NDK=<ndk root> tools/aaudio-sink/build.sh <pulseaudio-13.0 source dir> <output dir> -# Produces module-aaudio-sink.so in the output dir (the fallback sink; the DirectAudio sinks come -# from the pinned DirectAudio release, see tools/directaudio/fetch.sh). set -euo pipefail PA_SRC=$1 OUTDIR=$2 @@ -40,14 +35,11 @@ sed -e 's/@PA_MAJOR@/13/g' -e 's/@PA_MINOR@/0/g' -e 's/@PA_API_VERSION@/12/g' -e cp "$HERE/config.h" "$HERE/ltdl.h" "$INC/" build_one() { local src=$1 OUT=$2 extra=$3 - # PulseAudio 13's own headers (pulsecore/atomic.h) predate clang's int-conversion error; that - # one is downgraded for them, the modules themselves compile clean. "$CC" -O2 -shared -fPIC -Wall -Wno-unused-parameter -Wno-error=int-conversion -Wno-visibility -DHAVE_CONFIG_H \ -I"$INC" -I"$PA_SRC/src" -I"$HERE" \ -o "$OUT" "$HERE/$src" \ -L"$LIBS" -l:libpulsecore-13.0.so -l:libpulsecommon-13.0.so -l:libpulse.so $extra "$TOOLCHAIN/llvm-strip" --strip-unneeded "$OUT" - # What the daemon looks for, and nothing linked that a device would not have. for sym in pa__init pa__done pa__get_author pa__get_description pa__get_usage pa__get_version pa__load_once; do "$TOOLCHAIN/llvm-nm" -D --defined-only "$OUT" | grep -q " $sym$" || { echo "ERROR: $OUT does not export $sym"; exit 1; } done diff --git a/tools/aaudio-sink/config.h b/tools/aaudio-sink/config.h index 4615ca53f..0c7e1d0e4 100644 --- a/tools/aaudio-sink/config.h +++ b/tools/aaudio-sink/config.h @@ -1,6 +1,3 @@ -/* The handful of build-time facts PulseAudio 13's headers read, for compiling one module against - * the upstream source tree without running its configure. The daemon and libraries the app ships - * were built for the same target (Android arm64, bionic), so these match them. */ #ifndef DROIDDECK_PA_CONFIG_H #define DROIDDECK_PA_CONFIG_H #define PACKAGE "pulseaudio" diff --git a/tools/aaudio-sink/ltdl.h b/tools/aaudio-sink/ltdl.h index 84b3985c0..c183a4672 100644 --- a/tools/aaudio-sink/ltdl.h +++ b/tools/aaudio-sink/ltdl.h @@ -1,5 +1,3 @@ -/* pulsecore/module.h keeps a libltdl handle in pa_module; the module never touches it, so the - * handle type is all that is needed here. */ #ifndef DROIDDECK_LTDL_STUB_H #define DROIDDECK_LTDL_STUB_H typedef void *lt_dlhandle; diff --git a/tools/aaudio-sink/module-aaudio-sink.c b/tools/aaudio-sink/module-aaudio-sink.c index 3ad32896d..619bd2673 100644 --- a/tools/aaudio-sink/module-aaudio-sink.c +++ b/tools/aaudio-sink/module-aaudio-sink.c @@ -1,15 +1,4 @@ /* - * module-aaudio-sink: a PulseAudio 13 sink on Android's AAudio, for the DroidDeck app. - * - * The daemon's IO thread pulls rendered audio from the sink one burst at a time and hands it to - * a blocking AAudio output stream; the stream's own buffer paces the thread, and the latency the - * sink reports comes from the stream's playback timestamp. With adaptive=1 the buffer grows by a - * burst after each underrun, up to a cap, so a device that cannot keep a small buffer fed settles - * on one it can. - * - * Module arguments keep the names the app has always passed (performance_mode, adaptive, volume, - * buffer_frames, max_buffer_frames) so the daemon's config line did not have to change. - * * SPDX-License-Identifier: LGPL-2.1-or-later */ #ifdef HAVE_CONFIG_H @@ -58,18 +47,11 @@ PA_MODULE_USAGE( "buffer_frames=<initial buffer size in frames, 0 = two bursts> " "max_buffer_frames=<largest the buffer may grow to, 0 = the stream's capacity>"); -/* The name the app's daemon config addresses (set-default-sink). */ #define DEFAULT_SINK_NAME "AAudioSink" -/* Underruns are polled every so many writes, not every write. */ #define XRUN_CHECK_EVERY 8 #define FALLBACK_BURST_FRAMES 192 -/* The buffer starts at whichever is larger: this many bursts, or this much time. The daemon runs - * under proot, where every system call is traced, and a buffer of two bursts (8 ms) left the - * client's menu sounds choppy on device; 30 ms is inaudible for a menu and games have their own - * path. adaptive=1 still grows it further after real underruns. */ #define MIN_BUFFER_BURSTS 2 #define MIN_BUFFER_USEC (30 * PA_USEC_PER_MSEC) -/* Android's own audio threads run at nice -16 (THREAD_PRIORITY_AUDIO); an app may ask for it. */ #define AUDIO_NICE (-16) #define STATS_EVERY_USEC (30 * PA_USEC_PER_SEC) @@ -109,7 +91,6 @@ struct userdata { int32_t max_buffer_frames; int32_t requested_buffer_frames; bool adaptive; - /* rate= was given: ask the device for exactly that instead of its own rate. */ bool rate_requested; int32_t xruns_seen; unsigned writes_since_xrun_check; @@ -127,8 +108,6 @@ static int32_t usec_to_frames(const struct userdata *u, pa_usec_t usec) { return (int32_t) ((uint64_t) usec * u->ss.rate / PA_USEC_PER_SEC); } -/* Each write hands the device half the buffer, whole bursts, at least one: fewer, larger - * trips through proot's tracing than a burst at a time, with half the buffer of slack left. */ static void set_chunk(struct userdata *u) { int32_t frames = u->buffer_frames / 2; frames -= frames % u->burst_frames; @@ -166,7 +145,6 @@ static void apply_buffer_size(struct userdata *u, int32_t frames) { set_chunk(u); } -/* Opens the stream for u->ss; on return u->ss holds what the device actually gave. */ static int open_stream(struct userdata *u) { AAudioStreamBuilder *builder = NULL; aaudio_result_t r; @@ -180,9 +158,6 @@ static int open_stream(struct userdata *u) { AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_SHARED); AAudioStreamBuilder_setPerformanceMode(builder, u->performance_mode); AAudioStreamBuilder_setFormat(builder, u->ss.format == PA_SAMPLE_FLOAT32NE ? AAUDIO_FORMAT_PCM_FLOAT : AAUDIO_FORMAT_PCM_I16); - /* The device's own rate unless one was asked for: at any other rate Android falls back to a - * legacy stream with 20 ms bursts and room for three of them, and the daemon under proot - * cannot keep that fed. The sink takes whatever rate comes back; PulseAudio resamples clients. */ AAudioStreamBuilder_setSampleRate(builder, u->rate_requested ? (int32_t) u->ss.rate : AAUDIO_UNSPECIFIED); AAudioStreamBuilder_setChannelCount(builder, (int32_t) u->ss.channels); @@ -240,7 +215,6 @@ static void stop_stream(struct userdata *u) { u->started = false; } -/* How much audio the device still holds: frames written less frames it reports played. */ static pa_usec_t stream_latency(struct userdata *u) { int64_t position = 0, when_ns = 0; @@ -267,7 +241,6 @@ static void check_underruns(struct userdata *u) { u->writes_since_xrun_check = 0; xruns = AAudioStream_getXRunCount(u->stream); - /* A line every so often while underruns keep coming, so a session log shows the count. */ if (xruns != u->xruns_reported && pa_rtclock_now() - u->stats_at >= STATS_EVERY_USEC) { pa_log("aaudio-sink: %d underrun(s) so far, buffer %d frames", xruns, u->buffer_frames); u->xruns_reported = xruns; @@ -285,7 +258,6 @@ static void check_underruns(struct userdata *u) { pa_log("aaudio-sink: underrun %d: buffer now %d frames (%u ms)", xruns, u->buffer_frames, (unsigned) (frames_to_usec(u, u->buffer_frames) / PA_USEC_PER_MSEC)); } -/* One burst from the sink into the device. Returns <0 when the stream is gone for good. */ static int write_one_burst(struct userdata *u) { const void *data; aaudio_result_t written = 0; @@ -315,8 +287,6 @@ static int write_one_burst(struct userdata *u) { return 0; } - /* The device went away (headphones, a route change). Reopen once with the same settings; if - * the new stream differs in rate or format there is nothing sane to do but give up. */ pa_log("aaudio-sink: AAudioStream_write: %s; reopening the stream", AAudio_convertResultToText(written)); { pa_sample_spec before = u->ss; @@ -346,8 +316,6 @@ static int sink_process_msg(pa_msgobject *o, int code, void *data, int64_t offse return pa_sink_process_msg(o, code, data, offset, chunk); } -/* Runs in the IO thread: an opened sink means a running stream and a busy loop; a suspended - * one means a stopped stream and a thread asleep until a message arrives. */ static int sink_set_state_in_io_thread_cb(pa_sink *s, pa_sink_state_t new_state, pa_suspend_cause_t new_suspend_cause) { struct userdata *u = s->userdata; @@ -368,8 +336,6 @@ static void thread_func(void *userdata) { pa_log_debug("IO thread starting"); pa_thread_mq_install(&u->thread_mq); - /* Ahead of everything else in the app, as Android's own audio threads are; realtime on top - * of that where the kernel allows (it does not, for an app, but it costs nothing to ask). */ if (setpriority(PRIO_PROCESS, (id_t) syscall(SYS_gettid), AUDIO_NICE) < 0) pa_log_debug("aaudio-sink: could not raise the IO thread's priority (%s)", strerror(errno)); if (u->core->realtime_scheduling) @@ -379,7 +345,6 @@ static void thread_func(void *userdata) { int ret; if (PA_SINK_IS_OPENED(u->sink->thread_info.state) && u->started) { - /* Nothing written can be taken back from the device, so a rewind is just acknowledged. */ if (u->sink->thread_info.rewind_requested) pa_sink_process_rewind(u->sink, 0); if (write_one_burst(u) < 0) @@ -426,7 +391,6 @@ int pa__init(pa_module *m) { pa_log("Invalid sample format specification or channel map"); goto fail; } - /* AAudio speaks 16-bit or float; anything else is rendered as 16-bit. */ if (ss.format != PA_SAMPLE_FLOAT32NE) ss.format = PA_SAMPLE_S16NE; diff --git a/tools/adb_device.py b/tools/adb_device.py index 5e2990399..9b113c5d0 100755 --- a/tools/adb_device.py +++ b/tools/adb_device.py @@ -45,7 +45,6 @@ def _identity(adb: str, serial: str) -> str | None: def _rank(serial: str) -> tuple[int, str]: - # Prefer a stable mDNS transport over a network address with an ephemeral port. if "_adb-tls-connect._tcp" in serial: return (0, serial) if not re.search(r":\d+$", serial): diff --git a/tools/build_local.sh b/tools/build_local.sh index 0487104ed..2695ef084 100755 --- a/tools/build_local.sh +++ b/tools/build_local.sh @@ -47,7 +47,6 @@ fi ndk_version=${DROIDDECK_NDK_VERSION:-} if [[ -z "${ndk_version}" ]]; then - # Only complete NDKs: an interrupted sdkmanager install leaves a directory without source.properties. ndk_path=$(find "${sdk_dir}/ndk" -mindepth 2 -maxdepth 2 -name source.properties -print | xargs -n1 dirname | sort -V | tail -1) ndk_version=${ndk_path##*/} fi @@ -73,7 +72,6 @@ trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM -# A wedged daemon used to hang indefinitely and then trigger an unnecessary image rebuild. python3 "${repo_root}/tools/docker_preflight.py" "${image_name}" rm -rf -- "${linuxfs_dir}" mkdir -p "${linuxfs_dir}" @@ -84,7 +82,6 @@ docker run --rm --platform linux/amd64 \ set -euo pipefail d=app/src/main/assets/linuxfs mkdir -p "$d" - # Not install -m: the VirtioFS share sometimes refuses its chmod (EPERM). The APK keeps no modes. put() { mkdir -p "$(dirname "$2")" && cat "$1" > "$2"; } aarch64-linux-gnu-g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \ -pthread -std=c++17 -static-libstdc++ -static-libgcc -Wl,--exclude-libs,ALL \ @@ -131,7 +128,6 @@ docker run --rm --platform linux/amd64 \ chown -R '"$(id -u):$(id -g)"' app/src/main/assets/linuxfs ' -# Cardboard mode's SteamVR driver and Vulkan layer (tools/vr, docs/cardboard-vr.md). DROIDDECK_BUILD_IMAGE="${image_name}" "${repo_root}/tools/vr/build.sh" "${linuxfs_dir}/usr/local/lib/droiddeck-vr" github_repo=${DROIDDECK_GITHUB_REPOSITORY:-} @@ -150,12 +146,8 @@ if [[ -z "${github_repo}" ]]; then github_repo=${github_repo%.git} fi -# Pinned downloads are kept between builds, named by their checksum, so a rebuild fetches nothing -# it already has. A cached file is checked again before use; a bad one is fetched anew. cache_dir=${DROIDDECK_BUILD_CACHE:-"${HOME}/.cache/droiddeck-build"} mkdir -p "${cache_dir}" -# cached <sha256> <name> <command...>: prints the cached path, running the command (which writes -# to "$out") only when the cache has no file with that checksum. cached() { local sha=$1 name=$2 out shift 2 @@ -182,7 +174,6 @@ fi . "${repo_root}/tools/linuxfs/uruntime.env" uruntime_binary=$(cached "${URUNTIME_SHA256}" "${URUNTIME_ASSET}" \ bash -c 'curl -fsSL --retry 3 -o "$out" "$0"' "https://github.com/VHSgunzo/uruntime/releases/download/${URUNTIME_VERSION}/${URUNTIME_ASSET}") -# macOS's install has no -D. mkdir -p "${linuxfs_dir}/usr/local/lib/droiddeck" "${linuxfs_dir}/usr/local/share/licenses/uruntime" install -m644 "${uruntime_binary}" "${linuxfs_dir}/usr/local/lib/droiddeck/uruntime" install -m644 "${repo_root}/tools/linuxfs/licenses/uruntime-LICENSE" "${linuxfs_dir}/usr/local/share/licenses/uruntime/LICENSE" @@ -244,7 +235,6 @@ install -m644 "${mango_pkgs}/usr/bin/mangoapp" "${mango_dir}/mangoapp" for library in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1; do cp -L "${mango_pkgs}/usr/lib/${library}" "${mango_dir}/${library}" done -# Ours, not the package's: GPU memory without tracefs (tools/mangoapp/libtracefs-shim.c). docker run --rm --platform linux/amd64 --user "$(id -u):$(id -g)" -v "${repo_root}:/src" -w /src "${image_name}" \ aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wl,-soname,libtracefs.so.1 \ -o app/src/main/assets/linuxfs/usr/local/lib/mangoapp/libtracefs.so.1 tools/mangoapp/libtracefs-shim.c @@ -270,8 +260,6 @@ fi sink_output="${staging_dir}/sink-out" "${repo_root}/tools/directaudio/fetch.sh" "${repo_root}" "${sink_output}" "${repo_root}/tools/aaudio-sink/build.sh" "${pa_source}" "${sink_output}" -# proot is rebuilt only when its sources (source.env, the patches, the build script) changed since -# the libraries in jniLibs were built. proot_out="${repo_root}/app/src/main/jniLibs/arm64-v8a" proot_sources=$(cd "${repo_root}/tools/proot" && find . -type f ! -name '*.pyc' | LC_ALL=C sort | xargs shasum -a 256 | shasum -a 256 | cut -d' ' -f1) proot_inputs=$(printf '%s\n' "${proot_sources}" "${ndk_version}" | shasum -a 256 | cut -d' ' -f1) @@ -301,10 +289,6 @@ tar -cf - -C "${bundle_dir}" . | zstd -19 -T0 -c > "${staging_dir}/pulseaudio.tz mkdir -p "${repo_root}/app/build/prepared-assets" mv "${staging_dir}/pulseaudio.tzst" "${repo_root}/app/build/prepared-assets/pulseaudio.tzst" -# The GOG / Epic / Amazon download engines are Rust (app/src/main/rust/stores); Gradle's -# buildRustStores task cross-compiles them with cargo-ndk, which needs the Android target and the -# cargo-ndk binary installed once. DROIDDECK_SKIP_RUST=1 builds an apk without the engines (the -# stores then report themselves unavailable) for a machine without a Rust toolchain. gradle_rust_arg="" if [[ "${DROIDDECK_SKIP_RUST:-0}" == 1 ]]; then gradle_rust_arg="-PskipRust=true" @@ -385,10 +369,6 @@ grep -qi 'CN=Android, OU=Android, O=Android' <<<"${signature_output}" || { exit 1 } -# With DroidDeck's own key at hand, sign as CI's main builds are (tools/release/sign-apk.sh), so -# the apk installs over a release instead of being refused as a different signer. The key's -# location and password live in an untracked .signing.env (RELEASE_KEYSTORE, RELEASE_STORE_PASSWORD, -# RELEASE_KEY_ALIAS), looked for in this checkout and then in the main one, which worktrees share. signing_env=${DROIDDECK_SIGNING_ENV:-} if [[ -z "${signing_env}" ]]; then for candidate in "${repo_root}/.signing.env" \ @@ -400,7 +380,6 @@ if [[ -n "${signing_env}" ]]; then echo "Signing with DroidDeck's key (${signing_env})" ( set -a - # shellcheck disable=SC1090 cd "$(dirname "${signing_env}")" . "${signing_env}" set +a diff --git a/tools/cardboard/test_cardboard_params.c b/tools/cardboard/test_cardboard_params.c index 360f08857..4eb35ffda 100644 --- a/tools/cardboard/test_cardboard_params.c +++ b/tools/cardboard/test_cardboard_params.c @@ -1,5 +1,3 @@ -/* Host test for the cardboard pass's pure helpers (waylandcomp/src/cardboard.c, built with - * CARDBOARD_HOST_TEST): clamping and the push-constant layout cardboard.frag reads. */ #include "cardboard.h" #include <math.h> #include <stdio.h> @@ -28,20 +26,15 @@ int main(void) { const float pose[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; float pc[24]; cardboard_push_constants(&q, pose, 1920, 1080, 2400, 1080, pc); - /* rot rows with the scalars in w */ CHECK(near(pc[0], 1) && near(pc[1], 2) && near(pc[2], 3) && near(pc[3], 0.032f)); CHECK(near(pc[4], 4) && near(pc[5], 5) && near(pc[6], 6) && near(pc[7], 3.0f)); CHECK(near(pc[8], 7) && near(pc[9], 8) && near(pc[10], 9) && near(pc[11], 2.0f)); - /* lens: k1, k2, tan(fov/2), chroma */ CHECK(near(pc[12], 0.34f) && near(pc[13], 0.55f) && near(pc[14], 1.0f) && near(pc[15], 0.006f)); - /* view: inset, source aspect, mode, eye viewport aspect (half the panel) */ CHECK(near(pc[16], 0.05f) && near(pc[17], 1920.0f / 1080.0f) && near(pc[18], 0.0f) && near(pc[19], 1200.0f / 1080.0f)); - /* A side-by-side source: each eye's aspect is half the frame's. */ q.source = CARDBOARD_SOURCE_SBS; cardboard_push_constants(&q, pose, 3840, 1080, 2400, 1080, pc); CHECK(near(pc[17], 1920.0f / 1080.0f) && near(pc[18], 1.0f)); - /* Unknown sizes fall back instead of dividing by zero. */ cardboard_push_constants(&q, pose, 0, 0, 0, 0, pc); CHECK(isfinite(pc[17]) && isfinite(pc[19])); diff --git a/tools/comet/droiddeck.patch b/tools/comet/droiddeck.patch index f65fe9efa..6dbb60ae9 100644 --- a/tools/comet/droiddeck.patch +++ b/tools/comet/droiddeck.patch @@ -119,7 +119,7 @@ index 0c539f1..50fdd21 100644 .unwrap_or_else(|| String::from("en-US")); } + -+/// A launcher may restrict Galaxy to explicitly enabled SDK clients in its private policy file. ++ +pub fn client_allowed(client_id: &str) -> bool { + let Ok(path) = std::env::var("COMET_ALLOWED_CLIENTS") else { return true; }; + std::fs::read(path).ok().and_then(|data| serde_json::from_slice::<Vec<String>>(&data).ok()) diff --git a/tools/desktop-kde/build-kwin.sh b/tools/desktop-kde/build-kwin.sh index f86f28e4a..ae6ac4943 100644 --- a/tools/desktop-kde/build-kwin.sh +++ b/tools/desktop-kde/build-kwin.sh @@ -1,6 +1,4 @@ #!/bin/bash -# Build the pinned KWin executable with the nested-scale patch on native aarch64 Linux. -# Its dependencies come from signature-checked Arch Linux ARM packages in a disposable chroot. set -euo pipefail out=$(realpath -m "${1:?out dir}") here=$(cd "$(dirname "$0")" && pwd) @@ -20,7 +18,6 @@ for repo in core extra alarm; do done touch "$work/runtime.txt" python3 "$here/closure.py" "$work/db" "$work/runtime.txt" "$here/kwin-build-seeds.txt" > "$work/packages.txt" -# Refuse a repository whose KWin headers/libraries have moved beyond the pinned source. grep -Eq '^extra/kwin-6\.7\.5-[0-9]+-aarch64\.pkg\.tar\.' "$work/packages.txt" export work mirror builder here download() { @@ -56,8 +53,6 @@ sudo cp "$work/root/build/out/bin/kwin_wayland" "$out/kwin_wayland" sudo cp "$work/root/build/out/bin/libkwin.so.$version" "$out/libkwin.so.$version" sudo chown "$(id -u):$(id -g)" "$out/kwin_wayland" "$out/libkwin.so.$version" strip --strip-unneeded "$out/kwin_wayland" "$out/libkwin.so.$version" -# The backend lives in libkwin, not the small launcher. Catch accidentally shipping only the -# executable, which boots successfully but still loads the distribution's unpatched backend. grep -aq DROIDDECK_NESTED_SCALE "$out/libkwin.so.$version" { echo "KWin $version source sha256 $source_sha" diff --git a/tools/desktop-kde/build.sh b/tools/desktop-kde/build.sh index b64c00482..524a76f46 100755 --- a/tools/desktop-kde/build.sh +++ b/tools/desktop-kde/build.sh @@ -1,24 +1,4 @@ #!/bin/bash -# Builds the KDE Plasma desktop package for the Linux runtime: the closure of seeds.txt over Arch -# Linux ARM's aarch64 repositories, minus what the runtime image already carries, plus overlay/, -# as a zstd tarball that extracts over the rootfs (DesktopCatalog's "tar" kind). -# -# tools/desktop-kde/build.sh <out dir> -# -# The runtime's own packages (runtime-packages.txt, "name version" from its pacman db) are not -# shipped again, except where something in the closure asks for a version of one the runtime does -# not have (a versioned dependency on it, or on a library it provides): then the repository's -# build comes along and replaces it. Nothing is deleted on extraction, so a library whose soname -# moved keeps its old file for what the runtime linked against it. -# -# Every package is fetched over HTTPS and must carry a valid signature by the Arch Linux ARM Build -# System key (archlinuxarm-builder.gpg; primary fingerprint pinned below, as archlinuxarm.org -# publishes it and archlinuxarm-keyring trusts it) and match the sha256 the repository database -# gives for it; anything else stops the build. The repository databases themselves are not signed -# by Arch Linux ARM, so they decide only which signed packages are taken. desktop-kde.packages.txt -# lists every package taken, with its sha256: what a release was built from. -# -# Needs curl, gpgv, tar, xz, zstd, python3. set -euo pipefail out=${1:?out dir} here=$(cd "$(dirname "$0")" && pwd) @@ -27,7 +7,6 @@ case $mirror in https://*) ;; *) echo "ALARM_MIRROR must be https" >&2; exit 1 ; builder=68B3537F39A313B3E574D06777193F152BDBE6A6 fetch() { curl -fsSL --proto '=https' --proto-redir '=https' --retry 6 --retry-delay 5 --retry-all-errors -o "$1" "$2"; } work=$(mktemp -d); mkdir -p "$out" "$work/db" "$work/root" -# PKG_CACHE keeps the downloaded packages between builds (a local rebuild after an overlay change). pkgs=${PKG_CACHE:-$work/pkgs}; mkdir -p "$pkgs" trap 'rm -rf "$work"' EXIT @@ -56,24 +35,19 @@ done < "$work/pkglist.txt" cp -a "$here/overlay/." "$work/root/" -# Same-version KWin, built by build-kwin.sh with the nested desktop scale patch. if [ -n "${KDE_KWIN_OVERRIDE:-}" ]; then install -m 755 "$KDE_KWIN_OVERRIDE/kwin_wayland" "$work/root/usr/bin/kwin_wayland" install -m 755 "$KDE_KWIN_OVERRIDE/libkwin.so.6.7.5" "$work/root/usr/lib/libkwin.so.6.7.5" fi -# Weight that no desktop on a phone reads. rm -rf "$work/root/usr/share/doc" "$work/root/usr/share/man" "$work/root/usr/share/info" \ "$work/root/usr/share/gtk-doc" "$work/root/usr/include" "$work/root/usr/lib/pkgconfig" \ "$work/root/usr/lib/cmake" "$work/root/usr/share/help" -# English only, on purpose: the desktop session runs in C.UTF-8 and nothing hands the app's language -# to KDE, so other translations would be weight no one sees. They come back with localizing the -# desktop itself (LANGUAGE from the app's language at session start). find "$work/root/usr/share/locale" -mindepth 1 -maxdepth 1 -type d ! -name 'en*' -exec rm -rf {} + 2>/dev/null || true find "$work/root/usr/lib" -name '*.a' -delete chmod -R u+rwX "$work/root" -sed 's#^[a-z]*/##' "$work/pkglist.txt" > "$out/desktop-kde.packages.txt" # "file sha256", each verified above +sed 's#^[a-z]*/##' "$work/pkglist.txt" > "$out/desktop-kde.packages.txt" tar -C "$work/root" --zstd -cf "$out/desktop-kde.tar.zst" . sha256sum "$out/desktop-kde.tar.zst" | awk '{print $1}' > "$out/desktop-kde.sha256" ls -l "$out/desktop-kde.tar.zst" diff --git a/tools/desktop-kde/closure.py b/tools/desktop-kde/closure.py index b0475cf6e..f078a5068 100755 --- a/tools/desktop-kde/closure.py +++ b/tools/desktop-kde/closure.py @@ -15,7 +15,6 @@ REPOS = ("core", "extra", "alarm") def vercmp_part(a, b): - # rpmvercmp, as pacman compares version strings. if a == b: return 0 sa = re.findall(r"\d+|[a-zA-Z]+", a) @@ -104,14 +103,11 @@ def main(): def resolve(dep): """The package to take for a dependency string, and whether the runtime's copy serves.""" name, op, want = parse_dep(dep) - # A runtime package that is the dependency or provides it, at a version that serves. if name in runtime and satisfies(runtime[name], op, want): return name, True candidates = providers.get(name, []) for real, _ in candidates: if real in runtime: - # A provided name (a soname, mostly): the runtime's build serves as long as it is - # the repository's own, or nothing asks for a particular version. if not op or runtime[real] == pkgs[real]["version"]: return real, True if satisfies_provider(real, name, op, want): diff --git a/tools/desktop-kde/kwin-build-seeds.txt b/tools/desktop-kde/kwin-build-seeds.txt index b544ad4b0..c0cfe51be 100644 --- a/tools/desktop-kde/kwin-build-seeds.txt +++ b/tools/desktop-kde/kwin-build-seeds.txt @@ -1,4 +1,3 @@ -# Native build tools and KWin's Arch makedepends; dependencies are resolved and signature-checked. kwin base-devel cmake diff --git a/tools/desktop-kde/lxqt-applications.txt b/tools/desktop-kde/lxqt-applications.txt index 812715567..6351c7764 100644 --- a/tools/desktop-kde/lxqt-applications.txt +++ b/tools/desktop-kde/lxqt-applications.txt @@ -1,5 +1,3 @@ -# Application packages from steamdeck-desktop-r1's desktop.packages.txt. -# Keep dependencies and shared apps/icons (Firefox, mpv, Breeze, xdg-utils and portals). labwc lxqt-panel lxqt-config diff --git a/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/actions/steamdeck-gaming-return.svg b/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/actions/steamdeck-gaming-return.svg index 5cff1ce9d..96d426607 100644 --- a/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/actions/steamdeck-gaming-return.svg +++ b/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/actions/steamdeck-gaming-return.svg @@ -1,4 +1,3 @@ -<!-- Created with Inkscape (http://www.inkscape.org/) --> <svg id="svg2" height="64" style="" viewBox="0 0 64 64" width="64" xmlns="http://www.w3.org/2000/svg" sodipodi:docname="deck_icon.svg" inkscape:version="1.2-dev (d2dd25d878, 2022-02-01, custom)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"> <path id="path42220" d="m 32 4 v 10 a 18 18 0 0 1 18 18 a 18 18 0 0 1 -18 18 v 10 a 28 28 0 0 0 28 -28 a 28 28 0 0 0 -28 -28 z" style="fill:#fff;fill-rule:evenodd"/> <circle id="path43518" cx="32" cy="32" r="14" style="fill:#1a9fff;fill-rule:evenodd"/> diff --git a/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/places/distributor-logo-steamdeck.svg b/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/places/distributor-logo-steamdeck.svg index 082485bf1..b0ac005cb 100644 --- a/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/places/distributor-logo-steamdeck.svg +++ b/tools/desktop-kde/overlay/usr/share/icons/hicolor/scalable/places/distributor-logo-steamdeck.svg @@ -1,4 +1,3 @@ -<!-- Created with Inkscape (http://www.inkscape.org/) --> <svg id="svg2" height="64" style="" viewBox="0 0 64 64" width="64" xmlns="http://www.w3.org/2000/svg" sodipodi:docname="start.svg" inkscape:version="1.2-dev (d2dd25d878, 2022-02-01, custom)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"> <style type="text/css" id="current-color-scheme"> .ColorScheme-Text { diff --git a/tools/desktop-kde/overlay/usr/share/plasma/look-and-feel/com.valve.vapor.desktop/contents/layouts/org.kde.plasma.desktop-layout.js b/tools/desktop-kde/overlay/usr/share/plasma/look-and-feel/com.valve.vapor.desktop/contents/layouts/org.kde.plasma.desktop-layout.js index 485bf7468..e27c370d3 100644 --- a/tools/desktop-kde/overlay/usr/share/plasma/look-and-feel/com.valve.vapor.desktop/contents/layouts/org.kde.plasma.desktop-layout.js +++ b/tools/desktop-kde/overlay/usr/share/plasma/look-and-feel/com.valve.vapor.desktop/contents/layouts/org.kde.plasma.desktop-layout.js @@ -1,5 +1,3 @@ -// The first desktop: DroidDeck's wallpaper, and SteamOS's Plasma's default bottom panel with the Steam Deck -// launcher icon, Steam, the file manager, the browser and Konsole pinned to the task manager. var desktops = desktopsForActivity(currentActivity()); for (var i = 0; i < desktops.length; i++) { desktops[i].wallpaperPlugin = "org.kde.image"; @@ -36,7 +34,6 @@ if (trayId) { trayContainment.currentConfigGroup = ["General"]; trayContainment.writeConfig("scaleIconsToFit", true); } -// The runtime has no locale of its own (C.UTF-8), whose short date reads "9 10 2026". var clock = panel.addWidget("org.kde.plasma.digitalclock"); clock.currentConfigGroup = ["Appearance"]; clock.writeConfig("dateFormat", "isoDate"); diff --git a/tools/desktop-kde/overlay/usr/share/wallpapers/DroidDeck/contents/images/1920x1080.svg b/tools/desktop-kde/overlay/usr/share/wallpapers/DroidDeck/contents/images/1920x1080.svg index 4a3d4eeca..229397123 100644 --- a/tools/desktop-kde/overlay/usr/share/wallpapers/DroidDeck/contents/images/1920x1080.svg +++ b/tools/desktop-kde/overlay/usr/share/wallpapers/DroidDeck/contents/images/1920x1080.svg @@ -1,5 +1,4 @@ <svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080"> - <!-- DroidDeck's desktop wallpaper: the mark (artwork/droiddeck-mark.svg) on deep navy. --> <defs> <radialGradient id="bg" cx="960" cy="540" r="1150" gradientUnits="userSpaceOnUse"> <stop offset="0" stop-color="#14213a"/> diff --git a/tools/desktop-kde/patches/0001-respect-nested-desktop-scale.patch b/tools/desktop-kde/patches/0001-respect-nested-desktop-scale.patch index af8c1bcfb..7c8767ff8 100644 --- a/tools/desktop-kde/patches/0001-respect-nested-desktop-scale.patch +++ b/tools/desktop-kde/patches/0001-respect-nested-desktop-scale.patch @@ -28,7 +28,7 @@ - // intentionally ignored, as it would get overwritten - // with the fractional scale protocol anyways - // next.scale = props->scale.value_or(m_state.scale); -+ // DroidDeck owns the outer window size; Display Settings owns the inner UI scale. ++ + if (qEnvironmentVariableIsSet("DROIDDECK_NESTED_SCALE")) { + next.scale = props->scale.value_or(m_state.scale); + next.scaleSetting = props->scaleSetting.value_or(m_state.scaleSetting); @@ -53,7 +53,7 @@ void init(const QSize &pixelSize, qreal scale, bool fullscreen); -+ // The outer window scale, independent of the desktop UI scale in DroidDeck. ++ + qreal presentationScale() const; bool isReady() const; KWayland::Client::Surface *surface() const; diff --git a/tools/desktop-kde/removal-manifests.py b/tools/desktop-kde/removal-manifests.py index 86ed74e69..9c909dc84 100644 --- a/tools/desktop-kde/removal-manifests.py +++ b/tools/desktop-kde/removal-manifests.py @@ -68,7 +68,6 @@ def main(): cache, out, legacy_cache = map(pathlib.Path, sys.argv[5:8]) here = pathlib.Path(__file__).resolve().parent seeds = {line.strip() for line in (here / 'seeds.txt').read_text().splitlines() if line.strip() and not line.startswith('#')} - SHARED_SEEDS - # Published source package hashes; the caller places this beside the verified cache. packages = dict(line.split() for line in (cache / 'desktop-kde.packages.txt').read_text().splitlines()) selected = set() found = set() @@ -86,8 +85,6 @@ def main(): selected.update(str(p.relative_to(here / 'overlay')) for p in (here / 'overlay').rglob('*') if p.is_file()) inventories = [inventory(p, pin[1]) for p, pin in zip(archives, PINS)] out.mkdir(parents=True, exist_ok=True) - # The old runtime's pacman database omits some gamescope dependencies (e.g. libseat). - # Own only the LXQt application packages, never their dependency closure. legacy_seeds = {line.strip() for line in (here / 'lxqt-applications.txt').read_text().splitlines() if line.strip() and not line.startswith('#')} legacy_selected = set() diff --git a/tools/desktop-kde/seeds.txt b/tools/desktop-kde/seeds.txt index 816285de5..ed61bd8ee 100644 --- a/tools/desktop-kde/seeds.txt +++ b/tools/desktop-kde/seeds.txt @@ -1,6 +1,3 @@ -# The desktop: KDE Plasma, as SteamOS's desktop mode has it - KWin nested as a client of the app's -# compositor, Plasma's shell on it, and the apps SteamOS puts on its desktop. One package name per -# line; build.sh takes the closure over Arch Linux ARM at build time, minus what the runtime has. kwin plasma-workspace plasma-desktop diff --git a/tools/directaudio/fetch.sh b/tools/directaudio/fetch.sh index f2f21c196..b7ce2dbcb 100755 --- a/tools/directaudio/fetch.sh +++ b/tools/directaudio/fetch.sh @@ -1,21 +1,7 @@ #!/usr/bin/env bash -# Fetches the DirectAudio pieces DroidDeck ships from the pinned release in tools/directaudio/release.env -# and puts them where the build reads them. Four zips, checksummed: -# directaudio-linux-wine11.zip the game driver for Valve Proton 11 / Experimental / GE-Proton -# directaudio-linux-wine11-systhread.zip the game driver for Proton-CachyOS (system-thread mmdevapi) -# directaudio-linux-relay.zip the Android-side helper (speaker + microphone for games) -# directaudio-linux-sink.zip the PulseAudio sinks; the app ships module-directaudio-native-sink -# -# tools/directaudio/fetch.sh <repo root> <dir to receive the sink modules> -# -# Driver sets -> app/src/main/assets/directaudio/<set>/ (generated, ignored by git) -# Helper -> app/src/main/jniLibs/arm64-v8a/libdirectaudiorelay.so (generated, ignored by git) -# Sink -> <sink dir>/module-directaudio-native-sink.so for the audio bundle, next to the -# module-aaudio-sink.so tools/aaudio-sink builds (the fallback). set -euo pipefail repo_root=$1 sink_out=$2 -# shellcheck source=release.env . "${repo_root}/tools/directaudio/release.env" for v in DIRECTAUDIO_REPO DIRECTAUDIO_TAG DIRECTAUDIO_WINE11_SHA256 DIRECTAUDIO_SYSTHREAD_SHA256 DIRECTAUDIO_RELAY_SHA256 DIRECTAUDIO_SINK_SHA256; do [[ -n "${!v:-}" && "${!v}" != TBD ]] || { echo "tools/directaudio/release.env: ${v} is not set" >&2; exit 1; } @@ -44,7 +30,6 @@ for set in linux-wine11 linux-wine11-systhread; do done rm -f "${assets}/${set}/README.md" done -# Each set says which interface it was built for; the name must not lie. grep -q '^abi wine11$' "${assets}/linux-wine11/version.txt" grep -q '^abi systhread$' "${assets}/linux-wine11-systhread/version.txt" diff --git a/tools/directaudio/release.env b/tools/directaudio/release.env index 1e30f83f1..5e052f0bb 100644 --- a/tools/directaudio/release.env +++ b/tools/directaudio/release.env @@ -1,6 +1,3 @@ -# The DirectAudio release this build ships, pinned: nothing compiled for audio lives in this repo. -# Source and release notes: https://github.com/The412Banner/directaudio -# Bump the tag and the four checksums together (sha256 of each zip, from the release's SHA256SUMS.txt). DIRECTAUDIO_REPO=The412Banner/directaudio DIRECTAUDIO_TAG=directaudio-linux-v1.1.0 DIRECTAUDIO_WINE11_SHA256=835e6cab072f5148893c43379e60506483aeb3f1444e777b430c295cbeb472c9 diff --git a/tools/droiddeck-scenario b/tools/droiddeck-scenario index 2a37eebfb..b4590b02f 100755 --- a/tools/droiddeck-scenario +++ b/tools/droiddeck-scenario @@ -155,7 +155,7 @@ class Runner: ): try: found[name] = action() - except Exception as exc: # evidence is best effort + except Exception as exc: found[name] = {"ok": False, "error": str(exc)} write_json(folder / "evidence.json", found) return {k: v.get("ok", True) if isinstance(v, dict) else True for k, v in found.items()} diff --git a/tools/droiddeckctl b/tools/droiddeckctl index d7456a81a..7c9f284b4 100755 --- a/tools/droiddeckctl +++ b/tools/droiddeckctl @@ -15,7 +15,7 @@ import time from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) -from adb_device import DeviceResolutionError, resolve_serial # noqa: E402 +from adb_device import DeviceResolutionError, resolve_serial AUTHORITY = "content://com.droiddeck.launcher.agent" @@ -215,7 +215,6 @@ def add_serial(response: dict, serial: str) -> dict: return response -# --- displays ------------------------------------------------------------------------------------ def displays(adb: str, serial: str) -> list[dict]: """Logical displays with the SurfaceFlinger (physical) id screencap and screenrecord want.""" @@ -285,7 +284,6 @@ def record(adb: str, serial: str, path: str, seconds: float, display: int | None return {"ok": True, "command": "record", "path": str(destination), "seconds": seconds, "display": display} -# --- commands ------------------------------------------------------------------------------------ def start_session(adb: str, serial: str, request: dict) -> dict: if request.get("mode") == "run": @@ -341,8 +339,6 @@ def execute(args: argparse.Namespace, adb: str, serial: str) -> dict: raise ControlError("Steam URL must start with steam://", "INVALID_STEAM_URL") request["steamUrl"] = args.url if args.reuse: - # A scenario's per-run "before": keep a session of this mode that is already up, so only - # a run after one that ended (a crash, the low-memory killer) starts a new one. current = get_state(adb, serial)["session"] wanted = {"steam": "steam", "desktop": "lxqt"}[args.mode] if current.get("running") and current.get("mode") == wanted and current.get("phase") not in {"STOPPING", "FAILED"}: @@ -508,7 +504,6 @@ def execute(args: argparse.Namespace, adb: str, serial: str) -> dict: return add_serial(record(adb, serial, args.path, args.seconds, args.display), serial) if command == "wake": - # A sleeping device stops the session and hides the app's windows from ui dump. run_adb(adb, serial, "shell", "input", "keyevent", "KEYCODE_WAKEUP") run_adb(adb, serial, "shell", "wm", "dismiss-keyguard") power = run_adb(adb, serial, "shell", "dumpsys", "power").stdout.decode(errors="replace") @@ -549,7 +544,6 @@ def build_parser() -> argparse.ArgumentParser: stop = commands.add_parser("stop", help="stop the session and wait for IDLE") stop.add_argument("--timeout", type=float, default=90) - # Collecting the client's logs and redacting them took about two minutes after a Skyrim session. stop.add_argument("--artifact-timeout", type=float, default=300) commands.add_parser("resume", help="resume a suspended session") @@ -711,7 +705,7 @@ def main(argv: list[str] | None = None) -> int: except (ControlError, DeviceResolutionError) as exc: error = {"code": getattr(exc, "code", "DEVICE_RESOLUTION_FAILED"), "message": str(exc)} code = exit_code(exc) - except Exception as exc: # the contract is JSON on stdout, even for a bug in this tool + except Exception as exc: error = {"code": "INTERNAL_ERROR", "message": f"{type(exc).__name__}: {exc}"} code = 2 response = {"ok": False, "error": error} diff --git a/tools/gamescope/build-in-arch.sh b/tools/gamescope/build-in-arch.sh index 2078592aa..7cc19fd00 100755 --- a/tools/gamescope/build-in-arch.sh +++ b/tools/gamescope/build-in-arch.sh @@ -1,26 +1,15 @@ #!/usr/bin/bash -# Runs INSIDE an Arch Linux ARM container (menci/archlinuxarm:base-devel) on an arm64 runner. -# Builds Arch's own gamescope package of the runtime's version with the patches in ./patches -# added, then keeps only the gamescope binary, checks its library needs against the runtime's -# list, and packs it as gamescope.tzst (usr/local/bin/gamescope) for the apk to stage. set -euxo pipefail VERSION=${GAMESCOPE_VERSION:-3.16.29} WORK=/work cd "$WORK" -# pacman's download sandbox (Landlock + the alpm user) cannot be set up inside the runner's -# container; the packages still come signed from Arch Linux ARM's mirrors. grep -q '^DisableSandbox' /etc/pacman.conf || sed -i 's/^\[options\]/[options]\nDisableSandbox/' /etc/pacman.conf -# The geo-redirecting mirror answered 500s and connection resets mid-transaction; name a few -# concrete mirrors ahead of it so pacman has somewhere to fall over to. { for m in https://ca.us.mirror.archlinuxarm.org https://fl.us.mirror.archlinuxarm.org https://de3.mirror.archlinuxarm.org https://nl.mirror.archlinuxarm.org; do echo "Server = $m/\$arch/\$repo"; done; cat /etc/pacman.d/mirrorlist; } > /etc/pacman.d/mirrorlist.new mv /etc/pacman.d/mirrorlist.new /etc/pacman.d/mirrorlist pacman -Syu --noconfirm --needed git sudo zstd binutils -# makepkg refuses root; a builder user with passwordless sudo installs the dependencies. id builder >/dev/null 2>&1 || useradd -m builder echo 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder rm -rf pkg && mkdir pkg && cp -r tools/gamescope/patches pkg/ && chown -R builder pkg -# Arch's packaging for exactly this version: the tag is <pkgver>-<pkgrel>; the first rel is tried -# first, later ones after it. sudo -u builder bash -c " set -euxo pipefail cd pkg @@ -31,13 +20,11 @@ test -f arch/PKGBUILD cd arch grep -q '^pkgver=${VERSION}$' PKGBUILD cp ../patches/*.patch . -# Add our patches to the source list and apply them after Arch's own prepare() steps. PATCHES=\$(ls *.patch | sort) { echo; echo 'source+=('; for p in \$PATCHES; do echo \" \$p\"; done; echo ')'; for p in \$PATCHES; do echo \"sha256sums+=('SKIP')\"; done; } >> PKGBUILD cat >> PKGBUILD <<'PREP' _droiddeck_prepare() { - # Arch's own prepare() leaves the shell inside the checkout; start from a known place. cd \"\$srcdir/gamescope\" for p in \$(ls \"\$srcdir\"/*.patch | sort); do echo \"applying \$(basename \"\$p\")\" @@ -51,7 +38,7 @@ else prepare() { _droiddeck_prepare; } fi PREP -makepkg -A -s --noconfirm --skipchecksums --skippgpcheck # -A: the PKGBUILD lists x86_64 only; Arch Linux ARM builds the same file +makepkg -A -s --noconfirm --skipchecksums --skippgpcheck ls -l *.pkg.tar.* " cd "$WORK" @@ -61,7 +48,6 @@ tar --use-compress-program=unzstd -xf "$PKG" -C out --strip-components=2 usr/bin mv out/gamescope out/usr/local/bin/gamescope chmod 755 out/usr/local/bin/gamescope strip --strip-unneeded out/usr/local/bin/gamescope || true -# Every NEEDED library must be one the runtime ships, or the binary would not load there. NEEDED=$(readelf -d out/usr/local/bin/gamescope | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p') echo "NEEDED: $NEEDED" MISSING="" diff --git a/tools/gamescope/patches/0019-steamcompmgr-arm64-virtual-white.patch b/tools/gamescope/patches/0019-steamcompmgr-arm64-virtual-white.patch index 62914a426..b98e4523d 100644 --- a/tools/gamescope/patches/0019-steamcompmgr-arm64-virtual-white.patch +++ b/tools/gamescope/patches/0019-steamcompmgr-arm64-virtual-white.patch @@ -22,9 +22,9 @@ index e54194b..4131467 100644 g_ColorMgmt.pending.outputVirtualWhite.x = santitize_float( bit_cast<float>( user_vec[0] ) ); g_ColorMgmt.pending.outputVirtualWhite.y = santitize_float( bit_cast<float>( user_vec[1] ) ); +#if defined(__aarch64__) -+ // The ARM64 Steam client packs the pair as one 64-bit element, so only x arrives; -+ // Steam derives the pair from the CIE daylight locus, so y follows from x. -+ // Drop once the client sends both floats (y arrives non-zero). ++ ++ ++ + float x = g_ColorMgmt.pending.outputVirtualWhite.x; + if ( x >= 0.25f && x <= 0.55f && g_ColorMgmt.pending.outputVirtualWhite.y <= 0.01f ) + g_ColorMgmt.pending.outputVirtualWhite.y = -3.0f * x * x + 2.87f * x - 0.275f; diff --git a/tools/gamescope/patches/0020-color-p3-red-is-wide-gamut.patch b/tools/gamescope/patches/0020-color-p3-red-is-wide-gamut.patch index 047ea2d37..aa183697d 100644 --- a/tools/gamescope/patches/0020-color-p3-red-is-wide-gamut.patch +++ b/tools/gamescope/patches/0020-color-p3-red-is-wide-gamut.patch @@ -18,7 +18,7 @@ index 64430a5..f4dcd98 100644 { - // Use red as a sentinal for a wide-gamut display - return ( nativeDisplayOutput.primaries.r.x > 0.650f && nativeDisplayOutput.primaries.r.y < 0.320f ); -+ // Use red as a sentinal for a wide-gamut display; DCI-P3 red sits exactly on the threshold ++ + return ( nativeDisplayOutput.primaries.r.x > 0.650f && nativeDisplayOutput.primaries.r.y <= 0.320f ); } diff --git a/tools/gamescope/patches/0100-realtime-queue-and-gamepad-cursor.patch b/tools/gamescope/patches/0100-realtime-queue-and-gamepad-cursor.patch index 0e390b099..75a355d4e 100644 --- a/tools/gamescope/patches/0100-realtime-queue-and-gamepad-cursor.patch +++ b/tools/gamescope/patches/0100-realtime-queue-and-gamepad-cursor.patch @@ -6,10 +6,10 @@ index 54e4efc..024af02 100644 } float queuePriorities = 1.0f; -+ // Without CAP_SYS_NICE gamescope never asks for realtime-priority queues. Under proot on -+ // Android there is no capability to have; the fallback below covers a driver that refuses. KGSL -+ // Turnip accepts but ignores it (one submit-queue priority, contexts created alike). Opt in with -+ // GAMESCOPE_FORCE_VULKAN_REALTIME=1. (Armada's --force-vulkan-realtime, as an environment switch.) ++ ++ ++ ++ + const char *pszForceRealtime = getenv( "GAMESCOPE_FORCE_VULKAN_REALTIME" ); + const bool useRealtimeVulkanQueue = gamescope::Process::HasCapSysNice() || ( pszForceRealtime && atoi( pszForceRealtime ) != 0 ); + if ( useRealtimeVulkanQueue && !gamescope::Process::HasCapSysNice() ) @@ -67,8 +67,8 @@ index 10990b8..08afbc4 100644 } - else + m_bConstrained = false; -+ // Nothing to draw (the gamepad UI, most games): the position only feeds focus and zoom, and -+ // wlserver's copy is what stock gamescope uses for those. No round trip to X. ++ ++ + if ( m_imageEmpty ) { - m_bConstrained = false; @@ -80,12 +80,12 @@ index 10990b8..08afbc4 100644 } wlserver_unlock(); + -+ // The gamepad-driven cursor is moved through XTest, which moves the X pointer and nothing -+ // else: wlserver's copy of the position never changes, so the sprite sat frozen while the -+ // pointer moved. Ask X where the pointer is. (Armada 0001, on 3.16.29.) That is a blocking -+ // round trip to Xwayland on the paint thread, which waits while Xwayland is busy drawing the -+ // client's interface: once per vblank only while the sprite is shown, and every 50 ms while -+ // it is hidden for inactivity - enough to bring it back within a few frames of a move. ++ ++ ++ ++ ++ ++ + if ( wlserver.bCursorHidden ) + { + const uint64_t now = get_time_in_nanos(); diff --git a/tools/gamescope/patches/0110-wayland-backend-touch.patch b/tools/gamescope/patches/0110-wayland-backend-touch.patch index 5a270e749..e85bf5d2a 100644 --- a/tools/gamescope/patches/0110-wayland-backend-touch.patch +++ b/tools/gamescope/patches/0110-wayland-backend-touch.patch @@ -6,8 +6,8 @@ index d74268b..bf72267 100644 void Wayland_Keyboard_RepeatInfo( wl_keyboard *pKeyboard, int32_t nRate, int32_t nDelay ); static const wl_keyboard_listener s_KeyboardListener; -+ // Touch from the host compositor (DroidDeck): each finger goes to wlserver's touch path, -+ // the one a Steam Deck's touchscreen drives, instead of being dropped. ++ ++ + void Wayland_Touch_Down( wl_touch *pTouch, uint32_t uSerial, uint32_t uTime, wl_surface *pSurface, int32_t nId, wl_fixed_t fX, wl_fixed_t fY ); + void Wayland_Touch_Up( wl_touch *pTouch, uint32_t uSerial, uint32_t uTime, int32_t nId ); + void Wayland_Touch_Motion( wl_touch *pTouch, uint32_t uTime, int32_t nId, wl_fixed_t fX, wl_fixed_t fY ); @@ -58,7 +58,7 @@ index d74268b..bf72267 100644 + } + } + -+ // Touch ++ + + bool CWaylandInputThread::TouchToOutput( wl_surface *pSurface, wl_fixed_t fX, wl_fixed_t fY, double *pflX, double *pflY ) + { @@ -75,7 +75,7 @@ index d74268b..bf72267 100644 + *pflY = ( wl_fixed_to_double( fY ) * uScale / 120.0 + oState->nDestY ) / g_nOutputHeight; + return true; + } -+ // Touch ids are offset by one: the nested pointer already moves wlserver's touch 0. ++ + void CWaylandInputThread::Wayland_Touch_Down( wl_touch *pTouch, uint32_t uSerial, uint32_t uTime, wl_surface *pSurface, int32_t nId, wl_fixed_t fX, wl_fixed_t fY ) + { + double flX, flY; diff --git a/tools/gamescope/patches/0111-wayland-pointer-warps-in-passthrough.patch b/tools/gamescope/patches/0111-wayland-pointer-warps-in-passthrough.patch index 466d0a781..c9027a309 100644 --- a/tools/gamescope/patches/0111-wayland-pointer-warps-in-passthrough.patch +++ b/tools/gamescope/patches/0111-wayland-pointer-warps-in-passthrough.patch @@ -7,9 +7,9 @@ index bf72267..c400978 100644 wlserver_lock(); - wlserver_touchmotion( flX, flY, 0, ++m_uFakeTimestamp ); -+ // Always move the real pointer too: in Passthrough (what Steam's Big Picture asks for) a -+ // touch motion for an id that is not down moves nothing, so hover never reached the -+ // client and a click landed wherever the pointer had last been. ++ ++ ++ + wlserver_touchmotion( flX, flY, 0, ++m_uFakeTimestamp, true ); wlserver_unlock(); } diff --git a/tools/gamescope/patches/0112-restore-iconified-game-on-resume.patch b/tools/gamescope/patches/0112-restore-iconified-game-on-resume.patch index 2eee045e4..9e24b7287 100644 --- a/tools/gamescope/patches/0112-restore-iconified-game-on-resume.patch +++ b/tools/gamescope/patches/0112-restore-iconified-game-on-resume.patch @@ -28,11 +28,11 @@ index 08afbc4..372a82c 100644 if ( keyboardFocusWindow && ctx->currentKeyboardFocusWindow && find_win( ctx, ctx->currentKeyboardFocusWindow ) == keyboardFocusWin ) keyboardFocusWindow = ctx->currentKeyboardFocusWindow; -+ // An overlay that takes input (the Steam menu) moves input away without changing the focus window, and a -+ // fullscreen game iconifies itself when it loses input. The focus window never changed, so it is never taken -+ // out of iconic above, and wine will not activate a window it believes iconic: the game stays minimized -+ // (its 160x24 caption in the corner) after Resume. Put it back to NormalState before input returns to it, -+ // then hand focus over again so wine sees the restore first and the activation after it. ++ ++ ++ ++ ++ + if ( inputFocus == ctx->focus.focusWindow && ctx->focus.focusWindow->iconifyRequested ) + { + ctx->focus.focusWindow->iconifyRequested = false; @@ -59,8 +59,8 @@ index 08afbc4..372a82c 100644 } + if ( ev->atom == ctx->atoms.gamescopeRestoreFocusWindow ) + { -+ // Anything outside (the session's watcher) can ask for the same restore: NormalState on the focus -+ // window and focus handed to it again. Harmless on a window that is not iconic. ++ ++ + if ( ctx->focus.focusWindow && get_prop( ctx, ctx->root, ctx->atoms.gamescopeRestoreFocusWindow, 0 ) ) + { + xwm_log.infof( "restore requested for focus window 0x%lx", ctx->focus.focusWindow->xwayland().id ); @@ -89,7 +89,7 @@ index 66022d7..af93a23 100644 bool isFullscreen = false; bool isSysTrayIcon = false; -+ // The client asked to be iconified (WM_CHANGE_STATE) and has not been put back to NormalState since. ++ + bool iconifyRequested = false; bool sizeHintsSpecified = false; bool skipTaskbar = false; diff --git a/tools/gamescope/patches/0114-take-override-redirect-from-mapnotify.patch b/tools/gamescope/patches/0114-take-override-redirect-from-mapnotify.patch index f2ea4a16f..492ec1116 100644 --- a/tools/gamescope/patches/0114-take-override-redirect-from-mapnotify.patch +++ b/tools/gamescope/patches/0114-take-override-redirect-from-mapnotify.patch @@ -26,7 +26,7 @@ index f00b62c..cb3b9c6 100644 if (w && w->xwayland().id == ev.xmap.window) + { -+ // Wine flips override-redirect while unmapped and the flip itself sends no event. ++ + w->xwayland().a.override_redirect = ev.xmap.override_redirect; map_win(ctx, ev.xmap.window, ev.xmap.serial); + } diff --git a/tools/linuxfs/check-preloads.sh b/tools/linuxfs/check-preloads.sh index 6e9e1dcff..78021c440 100644 --- a/tools/linuxfs/check-preloads.sh +++ b/tools/linuxfs/check-preloads.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -# Shared native ABI checks for CI and local builds. set -euo pipefail D=${1:?asset directory} NEED=$("${READELF:-readelf}" -d "$D/libfakeinput.so" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p') @@ -13,9 +12,6 @@ FAKE=$(syms "$D/libfakeinput.so") for sym in open openat ioctl read close poll ppoll select stat fstat access scandir; do grep -qx "$sym" <<<"$FAKE" || { echo "::error::libfakeinput.so does not export $sym"; exit 1; } done -# The static C++ runtime stays private: every session process preloads this library, -# and an exported personality routine takes over other programs' unwinding (shadPS4 -# aborted in pthread_exit). if grep -qxE '__gxx_personality_v0|__cxa_throw|_Unwind_Resume' <<<"$FAKE"; then echo "::error::libfakeinput.so exports the C++ runtime"; exit 1 fi @@ -23,7 +19,6 @@ SESSION=$(syms "$D/libblsession.so") for sym in socket bind getsockname setsockopt statfs statvfs syscall shm_open shm_unlink mmap mmap64 munmap drmPrimeFDToHandle drmCloseBufferHandle; do grep -qx "$sym" <<<"$SESSION" || { echo "::error::libblsession.so does not export $sym"; exit 1; } done -# Zink's GEM_CLOSE is caught at ioctl(); an exported drmIoctl made NMS replay old frames. if grep -qx drmIoctl <<<"$SESSION"; then echo "::error::libblsession.so exports drmIoctl"; exit 1 fi diff --git a/tools/linuxfs/clipboard/clipboard.c b/tools/linuxfs/clipboard/clipboard.c index cd91d65d1..61b89ca6e 100644 --- a/tools/linuxfs/clipboard/clipboard.c +++ b/tools/linuxfs/clipboard/clipboard.c @@ -1,7 +1,3 @@ -/* Android text -> Gamescope's Xwayland selection. Gamescope's Wayland backend does not - * relay the outer compositor's clipboard. libX11 is loaded from the guest at runtime; - * the small stable Xlib ABI below keeps this helper independent of cross X11 headers. - * The private host file is atomically replaced, never put in session logs. */ #define _GNU_SOURCE #include <sys/inotify.h> #include <sys/stat.h> @@ -45,12 +41,9 @@ API(int, XConnectionNumber, (Display *)); API(int, XCloseDisplay, (Display *)); API(void *, XSetErrorHandler, (int (*)(Display *, void *))); -/* Stay below X11's normal maximum property request. Never paste a truncated command. */ #define MAX_TEXT (64 * 1024) static int ignore_x_error(Display *d, void *event) { (void)d; (void)event; return 0; } -/* The host renames complete files into place. The inode distinguishes copying the same - * text again after a guest program has taken ownership of the clipboard. */ static int refresh(Display *d, Window w, Atom clipboard, const char *path, char **text, size_t *len, ino_t *inode) { int fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); @@ -98,7 +91,7 @@ int main(int argc, char **argv) { LOAD(XSetErrorHandler); Display *d = XOpenDisplay(NULL); if (!d) { fprintf(stderr, "== clipboard: cannot open Xwayland\n"); return 1; } - XSetErrorHandler(ignore_x_error); /* A paste requester can close its window at any time. */ + XSetErrorHandler(ignore_x_error); Window w = XCreateSimpleWindow(d, XDefaultRootWindow(d), 0, 0, 1, 1, 0, 0, 0); Atom clipboard = XInternAtom(d, "CLIPBOARD", 0); Atom targets = XInternAtom(d, "TARGETS", 0), utf8 = XInternAtom(d, "UTF8_STRING", 0); @@ -117,7 +110,7 @@ int main(int argc, char **argv) { for (;;) { while (XPending(d)) { Event e; XNextEvent(d, &e); - if (e.type != 30) continue; /* SelectionRequest */ + if (e.type != 30) continue; SelectionRequest *r = &e.request; Atom property = r->property ? r->property : r->target; Event reply = {0}; @@ -126,14 +119,13 @@ int main(int argc, char **argv) { size_t ascii; for (ascii = 0; ascii < len && (unsigned char)text[ascii] < 128; ascii++) {} if (r->target == targets) { Atom supported[] = {targets, utf8, plain, text_atom, string}; - XChangeProperty(d, r->requestor, property, 4 /* XA_ATOM */, 32, 0, + XChangeProperty(d, r->requestor, property, 4 , 32, 0, (const unsigned char *)supported, ascii == len ? 5 : 4); reply.selection.property = property; } else if (r->target == utf8 || r->target == plain || r->target == text_atom) { XChangeProperty(d, r->requestor, property, utf8, 8, 0, (const unsigned char *)text, (int)len); reply.selection.property = property; } else if (r->target == string) { - /* STRING is Latin-1. Offer it only for ASCII rather than corrupt UTF-8. */ if (ascii == len) { XChangeProperty(d, r->requestor, property, string, 8, 0, (const unsigned char *)text, (int)len); reply.selection.property = property; diff --git a/tools/linuxfs/debian-image.env b/tools/linuxfs/debian-image.env index 5532bfe16..8d6902e38 100644 --- a/tools/linuxfs/debian-image.env +++ b/tools/linuxfs/debian-image.env @@ -1,8 +1,3 @@ -# The Debian 11 image the x86 preloads are compiled in (glibc 2.31, what Steam's Linux runtime -# has). Published once by .github/workflows/ci-debian-image.yml to a release on this repo; the -# build downloads it from there instead of pulling from a public registry mirror, whose -# anonymous rate limit the shared runners keep hitting. Debian 11 is frozen upstream, so a -# refresh is only ever a new -rN release with the three lines below updated. DEBIAN_IMAGE_TAG=ci-debian-bullseye-r1 DEBIAN_IMAGE_ASSET=debian-bullseye.tar.zst DEBIAN_IMAGE_SHA256=5ac13c6b0f2e3ab3e8acc8cadcd39ee453a51a72870645d592635e24bcb85459 diff --git a/tools/linuxfs/desktop/droiddeck-desktop b/tools/linuxfs/desktop/droiddeck-desktop index eb0d10cc6..15771a35b 100644 --- a/tools/linuxfs/desktop/droiddeck-desktop +++ b/tools/linuxfs/desktop/droiddeck-desktop @@ -1,90 +1,39 @@ #!/bin/bash -# The desktop session: KDE Plasma, set up as SteamOS's desktop mode - KWin nested as a client of the -# app's compositor, Plasma's shell on it, and Xwayland for the X11 programs (the Steam client, most -# emulators). Run by droiddeck-session in place of gamescope for mode "lxqt" (the mode's name from -# the LXQt desktop this replaced; it is what the app and its preferences use). -# Staged by the app over the desktop package's copy on every launch, so a fix here reaches an -# installed desktop without re-downloading the package. set -u export XDG_SESSION_TYPE=wayland XDG_CURRENT_DESKTOP=KDE XDG_SESSION_DESKTOP=KDE DESKTOP_SESSION=plasma export KDE_FULL_SESSION=true KDE_SESSION_VERSION=6 KDE_APPLICATIONS_AS_SCOPE=0 -# Qt tries Wayland and falls back to X11 (Xwayland): an AppImage that bundles only Qt's xcb -# plugin - melonDS - exits at once on a plain "wayland" ("Could not find the Qt platform plugin"). export QT_QPA_PLATFORM="wayland;xcb" GDK_BACKEND=wayland,x11 SDL_VIDEODRIVER=wayland,x11 unset QT_QPA_PLATFORMTHEME -# KDE's programs log through Qt, which writes to the journal when stderr is not a terminal; there is -# no journal here, so their messages go to the desktop log with everything else. export QT_FORCE_STDERR_LOGGING=1 -# SteamOS's /etc/profile.d/kde.sh. export MOZ_ENABLE_WAYLAND=1 MOZ_USE_XINPUT2=1 -# Konsole's profile starts the login shell, which it finds here. export SHELL=/bin/bash -# Firefox's child processes sandbox themselves with seccomp and namespaces, neither of which -# behaves under proot: the content process ends up unable to read the font files its parent can, -# reports "no fonts" and dies - the tab crash, with the chrome around it still drawing text. -# The prefs for this are only advisory and a profile can override them; the environment is not. export MOZ_DISABLE_CONTENT_SANDBOX=1 MOZ_DISABLE_GMP_SANDBOX=1 MOZ_DISABLE_RDD_SANDBOX=1 export MOZ_DISABLE_SOCKET_PROCESS_SANDBOX=1 MOZ_DISABLE_UTILITY_SANDBOX=1 -# Firefox seals the size of the shared memory it hands a tab; on Android 16 (Galaxy S25/S26, -# RedMagic) adding the seal fails, Firefox only warns, and the tab refuses the unsealed memory -# ("Shared memory PlatformHandle is not safe to map") and dies on signal 11 - every page. Firefox's -# own switch skips the seals. (#280; the line went missing in the fontconfig move of #252.) export MOZ_SHM_NO_SEALS=1 -# KWin composites in software (QPainter): its GPU path allocates through gbm on a DRM render node, -# and the KGSL stand-in is not one. Its windows get shm buffers only, so GL programs on it draw with -# Mesa's software rasterizer, Plasma's own Qt Quick draws with Qt's software renderer (plasmashell -# crashes on llvmpipe here), and a Vulkan program cannot draw at all: games and emulators from the -# menu run through droiddeck-gpu (their own gamescope on the app's compositor), and Steam's desktop -# client uses software CEF on KDE. The session's own GL choice (zink on Turnip) -# is put back by droiddeck-gpu. export KWIN_COMPOSE=Q QT_QUICK_BACKEND=software LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe -# mpv refuses that software GL and then fails its X11 fallback (an assertion): allowed it, it plays. [ -e /etc/mpv/mpv.conf ] || { mkdir -p /etc/mpv && echo gpu-sw=yes >/etc/mpv/mpv.conf; } 2>/dev/null export DROIDDECK_NESTED_SCALE=1 export DROIDDECK_GLTHREAD=${mesa_glthread:-} unset MESA_LOADER_DRIVER_OVERRIDE LIBGL_KOPPER_DRI2 mesa_glthread -# The desktop's own log, beside the session's, so KWin and the programs on it can be read apart -# from the session that started them. The session log keeps the milestones either way. if [ -n "${BL_DEBUG_DIR:-}" ]; then mkdir -p "$BL_DEBUG_DIR" 2>/dev/null || true exec >>"$BL_DEBUG_DIR/desktop.log" 2>&1 echo "== droiddeck-desktop $(date -u '+%Y-%m-%d %H:%M:%S UTC') ${BL_WIDTH:-1280}x${BL_HEIGHT:-720}" fi -# The runtime directory outlives a session. KDE's daemons are single-instance programs that leave a -# lock and a socket there; finding last session's, they decide they are already running and exit -# without a word. Only the app's own compositor socket (wayland-0) is kept. find "$XDG_RUNTIME_DIR" -mindepth 1 -maxdepth 1 ! -name 'wayland-0*' ! -name 'keymap.xkb' -exec rm -rf {} + 2>/dev/null chmod 700 "$XDG_RUNTIME_DIR" 2>/dev/null -# KWin will not start Xwayland when its socket directory is not sticky, as a shared /tmp would be, -# and Plasma's session manager cannot listen at all without the ICE directory, which only root may -# create for it. mkdir -p /tmp/.X11-unix /tmp/.ICE-unix && chmod 1777 /tmp/.X11-unix /tmp/.ICE-unix 2>/dev/null -# Fontconfig defaults and Android language fallbacks are set up by droiddeck-session before this -# script or any desktop program starts. -# Steam opens on KDE; Steam Big Picture explicitly hands off to the gaming session. export BL_DESKTOP=1 ln -sf /usr/local/bin/droiddeck-steam-launch /usr/local/bin/steamdeck-steam 2>/dev/null ln -sf /usr/local/bin/droiddeck-steam-shim /usr/local/bin/steam 2>/dev/null python3 /usr/local/bin/droiddeck-desktop-games 2>&1 | sed 's/^/[games] /' -# Player 1 on the controller in every emulator that has no profile of its own yet - now, and again -# every half minute: an emulator started for the first time writes its own config, which is then -# given the controller for its next start without waiting for the next desktop. python3 /usr/local/bin/droiddeck-pad-defaults 2>&1 | sed 's/^/[pads] /' ( while sleep 30; do python3 /usr/local/bin/droiddeck-pad-defaults 2>&1 | sed 's/^/[pads] /'; done ) & -# Games and emulators from the menu run through droiddeck-gpu; BL_DESKTOP_GPU=on/off overrides, -# and each entry keeps "Run in a desktop window" either way. python3 /usr/local/bin/droiddeck-desktop-gpu 2>&1 | sed 's/^/[gpu] /' -# The windowed action still opens melonDS at the size/placement the LXQt desktop used. /usr/local/bin/droiddeck-desktop-window-rules 2>&1 | sed 's/^/[rules] /' -# Windows programs, Linux programs, AppImages and scripts open with droiddeck-open (Steam's Proton, -# FEX for x86); an exec the kernel refuses is handed to it as well (libblsession's binfmt.c). An -# AppImage run directly cannot mount itself under proot, so it unpacks itself to run. python3 /usr/local/bin/droiddeck-open --install 2>&1 | sed 's/^/[open] /' export APPIMAGE_EXTRACT_AND_RUN=1 -# A click runs an executable script or program without KIO asking first; Dolphin's settings can still ask. [ -e /etc/xdg/kiorc ] || printf '[Executable scripts]\nbehaviourOnLaunch=execute\n' >/etc/xdg/kiorc 2>/dev/null -# SteamOS's desktop icons: Return to Gaming Mode and Steam, put there once - what the user moves -# or deletes stays that way. desktop_dir=$(xdg-user-dir DESKTOP 2>/dev/null); [ -n "$desktop_dir" ] && [ "$desktop_dir" != "$HOME" ] || desktop_dir=$HOME/Desktop if [ ! -e "$HOME/.config/droiddeck-desktop-icons" ]; then mkdir -p "$desktop_dir" "$HOME/.config" @@ -93,23 +42,16 @@ if [ ! -e "$HOME/.config/droiddeck-desktop-icons" ]; then chmod +x "$desktop_dir"/*.desktop 2>/dev/null touch "$HOME/.config/droiddeck-desktop-icons" fi -# A Return icon still there is kept current with the package's entry (what it runs has changed). [ -f "$desktop_dir/Return.desktop" ] && cp -f /usr/share/applications/droiddeck-return-gaming.desktop "$desktop_dir/Return.desktop" 2>/dev/null -# Plasma's caches of the menu, the icons and the MIME types, which a package install never ran. update-mime-database /usr/share/mime >/dev/null 2>&1 & [ -f /usr/share/icons/hicolor/icon-theme.cache ] || gtk-update-icon-cache -q -t /usr/share/icons/hicolor 2>/dev/null & [ -f /usr/share/glib-2.0/schemas/gschemas.compiled ] || glib-compile-schemas /usr/share/glib-2.0/schemas 2>/dev/null -# In debug runs Firefox (started from the panel or the menu, so it inherits this) writes its -# own network log: name lookups, connects, HTTP transactions, the link/offline decisions. if [ -f "$HOME/.droiddeck-desktop-debug" ]; then mkdir -p "$HOME/.cache/droiddeck" export MOZ_LOG="nsHttp:3,nsSocketTransport:3,nsHostResolver:4,NetworkLinkService:5,NetworkConnectivityService:5,nsIOService:5" export MOZ_LOG_FILE="$HOME/.cache/droiddeck/firefox-net.log" export QT_LOGGING_RULES="kwin_*.debug=true" fi -# Remote runs, for driving the desktop from a debugger: with ~/.droiddeck-remote-run present (only -# the app or root can create it), each script dropped into ~/.cache/droiddeck/run/ is run once, -# inside the session with its environment, its output tagged into the desktop log. if [ -f "$HOME/.droiddeck-remote-run" ]; then mkdir -p "$HOME/.cache/droiddeck/run" ( @@ -122,18 +64,10 @@ if [ -f "$HOME/.droiddeck-remote-run" ]; then done ) & fi -# Plasma as its own login starts it: startplasma-wayland sets up the session's environment and runs -# plasma_session, which starts KWin (through kwin_wayland_wrapper, ours first on PATH) as a client of -# the app's compositor with its own Xwayland, then Plasma's daemons, shell and autostart entries on -# it. The session bus is ours: Plasma, the file manager and the portal all expect one. echo "== droiddeck-desktop: Plasma on $WAYLAND_DISPLAY" dbus-run-session -- startplasma-wayland rc=$? echo "== droiddeck-desktop: Plasma ended ($rc)" -# Plasma logged out - Log Out, or Return to Gaming Mode, which logs out the same way, so open windows -# are asked to close (and to save) first. As on SteamOS, leaving the desktop goes to Gaming Mode: the -# app is asked for a Steam session, and it ends this one to start it. A desktop the app stops never -# gets here; a Plasma that failed does not count as a logout. if [ "$rc" -eq 0 ]; then /usr/local/bin/droiddeck-steam-launch bigpicture sleep 30 diff --git a/tools/linuxfs/desktop/droiddeck-desktop-gpu b/tools/linuxfs/desktop/droiddeck-desktop-gpu index af9e11209..b1cabbff0 100644 --- a/tools/linuxfs/desktop/droiddeck-desktop-gpu +++ b/tools/linuxfs/desktop/droiddeck-desktop-gpu @@ -1,24 +1,13 @@ #!/usr/bin/python3 -# The desktop's games and emulators, started on the GPU. Every menu entry whose categories say -# Game or Emulator gets a copy of its own in ~/.local/share/applications (which wins over -# /usr/share/applications) whose Exec runs it through droiddeck-gpu: full screen on the app's -# compositor, where Vulkan and OpenGL work. The entry keeps the old way as a second action, "Run in -# a desktop window". Run at every desktop start; copies whose original is gone, or all of them when -# the switch is off, are removed. -# -# BL_DESKTOP_GPU=on (default: the desktop composites in software) | off import os import re HOME = os.environ.get("HOME", "/root") SYSTEM_DIR = "/usr/share/applications" -# The user's own AppImages (the front end's Add AppImage). LOCAL_DIR = "/usr/local/share/applications" -# Flathub's games, as Flatpak exports them; the copies here win over these as they do over the system's. FLATPAK_DIR = os.path.join(HOME, ".local/share/flatpak/exports/share/applications") USER_DIR = os.path.join(HOME, ".local/share/applications") MARK = "X-DroidDeck-GPU=1" -# The client and its games already hand themselves to the app (droiddeck-steam-launch). SKIP = re.compile(r"^(steam|droiddeck-game-|droiddeck-steam|steamdeck-)", re.I) @@ -76,7 +65,6 @@ def wrap(text): out_group.append("Actions=" + ("droiddeck-window;" + actions if actions else "droiddeck-window;")) out_group.append(MARK) window = ["", "[Desktop Action droiddeck-window]", "Name=Run in a desktop window", "Exec=" + exec_line] - # The entry's own actions (new window, a different mode...) run on the GPU as well. wrapped_rest = [] for l in rest: if l.startswith("Exec=") and not l[5:].startswith("droiddeck-gpu "): @@ -105,7 +93,6 @@ def main(): if not name.endswith(".desktop") or SKIP.match(name): continue target = os.path.join(USER_DIR, name) - # A copy the user made or edited themselves is theirs. if os.path.exists(target) and not ours(target): continue try: diff --git a/tools/linuxfs/desktop/droiddeck-gpu b/tools/linuxfs/desktop/droiddeck-gpu index e68f55178..40a346085 100644 --- a/tools/linuxfs/desktop/droiddeck-gpu +++ b/tools/linuxfs/desktop/droiddeck-gpu @@ -1,10 +1,4 @@ #!/bin/bash -# Runs a program from the desktop on the GPU: in a gamescope of its own that draws straight into -# the app's compositor (wayland-0), not inside the desktop's KWin. KWin composites in software, and -# gives its windows no DMA-BUFs - a Vulkan program there fails at its swapchain -# (VK_ERROR_SURFACE_LOST, RPCS3's error). Here it gets the same path as a program started from the -# app's rail: full screen over the desktop until it exits, then the desktop is back. -# droiddeck-gpu <program> [args...] set -u [ $# -gt 0 ] || { echo "usage: droiddeck-gpu <program> [args...]" >&2; exit 64; } if [ -n "${BL_DEBUG_DIR:-}" ]; then @@ -20,16 +14,11 @@ elif [ "${BL_REFRESH:-0}" -gt 0 ]; then fi [ "${BL_HDR:-0}" = 1 ] && args+=(--hdr-enabled) echo "== droiddeck-gpu $(date -u +%H:%M:%S): $* (gamescope ${width}x${height} on wayland-0)" -# gamescope is a client of the app's compositor, beside the desktop's KWin; the program inside it reaches it -# through gamescope's own Xwayland, as in a Steam session. export WAYLAND_DISPLAY=wayland-0 export GDK_BACKEND=x11 QT_QPA_PLATFORM=xcb SDL_VIDEODRIVER=x11 -# GL on the GPU again (zink on Turnip, as the session sets it): the desktop itself draws GL in software. -# Qt Quick back on its GPU renderer and KWin's software compositing not passed on, too. unset LIBGL_ALWAYS_SOFTWARE QT_QUICK_BACKEND KWIN_COMPOSE export MESA_LOADER_DRIVER_OVERRIDE=zink GALLIUM_DRIVER=zink LIBGL_KOPPER_DRI2=true [ -n "${DROIDDECK_GLTHREAD:-}" ] && export mesa_glthread=$DROIDDECK_GLTHREAD -# AppImages cannot mount under proot; the menu entries already say so, a hand-typed one may not. export APPIMAGE_EXTRACT_AND_RUN=1 gamescope "${args[@]}" -- /usr/local/bin/droiddeck-gpu-window "$@" rc=$? diff --git a/tools/linuxfs/desktop/firefox-droiddeck.js b/tools/linuxfs/desktop/firefox-droiddeck.js index ad6b1e40f..053d7d03a 100644 --- a/tools/linuxfs/desktop/firefox-droiddeck.js +++ b/tools/linuxfs/desktop/firefox-droiddeck.js @@ -1,17 +1,9 @@ -// Firefox defaults for the DroidDeck desktop, staged by the app into -// /usr/lib/firefox/defaults/pref/. A user's own prefs.js still wins. -// Firefox judges "online" by dumping the routing table over netlink, which Android denies to -// apps; seeing no default route it switches itself to Work Offline and every page fails while -// the network is fine. Let it just try. pref("network.manage-offline-status", false); pref("browser.offline", false); -// No user namespaces on Android kernels and a ptrace'd process tree: the process sandboxes -// have nothing to set up and only add failure modes. pref("security.sandbox.content.level", 0); pref("media.cubeb.sandbox", false); pref("security.sandbox.socket.process.level", 0); -// Handheld-sized chrome, and nothing phoning home on first run. pref("browser.shell.checkDefaultBrowser", false); pref("datareporting.policy.dataSubmissionEnabled", false); pref("browser.aboutwelcome.enabled", false); diff --git a/tools/linuxfs/desktop/kwin_wayland_wrapper b/tools/linuxfs/desktop/kwin_wayland_wrapper index 020c3d9c4..a34e7455a 100755 --- a/tools/linuxfs/desktop/kwin_wayland_wrapper +++ b/tools/linuxfs/desktop/kwin_wayland_wrapper @@ -1,10 +1,4 @@ #!/bin/bash -# KWin for the desktop. Plasma's session (plasma_session) starts KWin through kwin_wayland_wrapper as -# found on PATH; this one, first on it, has KWin draw into the app's compositor (wayland-0) and serve -# its own clients on wayland-1 - by default it takes wayland-0 and finds the app's lock on it - with -# its nested output at the app's surface size (left alone, a 1024x768 window the desktop is scaled -# from), and without the lock screen, which the app's own suspend stands in for. Anything else (a KWin -# started outside DroidDeck's desktop) gets the real wrapper as it is. if [ "${BL_DESKTOP:-}" = 1 ]; then set -- --wayland-display wayland-0 --socket wayland-1 --width "${BL_WIDTH:-1280}" --height "${BL_HEIGHT:-720}" --no-lockscreen "$@" fi diff --git a/tools/linuxfs/overlay/usr/bin/jupiter-biosupdate b/tools/linuxfs/overlay/usr/bin/jupiter-biosupdate index 7feebb74a..039e4d006 100755 --- a/tools/linuxfs/overlay/usr/bin/jupiter-biosupdate +++ b/tools/linuxfs/overlay/usr/bin/jupiter-biosupdate @@ -1,3 +1,2 @@ #!/bin/sh -# No Deck BIOS here. The client calls this in Deck mode; a clean exit is "nothing to do". exit 0 diff --git a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-biosupdate b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-biosupdate index 7feebb74a..039e4d006 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-biosupdate +++ b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-biosupdate @@ -1,3 +1,2 @@ #!/bin/sh -# No Deck BIOS here. The client calls this in Deck mode; a clean exit is "nothing to do". exit 0 diff --git a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-dock-updater b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-dock-updater index dadcac714..4dfb4b862 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-dock-updater +++ b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/jupiter-dock-updater @@ -1,8 +1,4 @@ #!/bin/sh -# No Deck dock here. The client runs this with --check at start (systemdockmanager.txt) and reads -# the exit code: 0 with nothing printed is taken as "an update is available" and puts a Dock -# Firmware Update row with an Apply button in Settings > System. 7, SteamOS's "nothing to do", -# is read as up to date (state 2 in that log, against state 4 for exit 0; both seen on device). case "${1:-}" in --check) exit 7 ;; esac diff --git a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-priv-write b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-priv-write index c875c2474..039e4d006 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-priv-write +++ b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-priv-write @@ -1,6 +1,2 @@ #!/bin/sh -# steamos-priv-write <path> <value>: how the client in Deck mode sets brightness, TDP and fan -# controls through sysfs. None of those paths is writable from an app on Android, and the app -# already sets the panel's mode and the game-tier power policy from the Android side. Accept -# and do nothing, so the client never logs a failed helper. exit 0 diff --git a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-select-branch b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-select-branch index 6f0c9d806..501e630b1 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-select-branch +++ b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-select-branch @@ -1,8 +1,4 @@ #!/bin/sh -# The SteamOS update-branch selector the client calls in Deck mode. The session forces the client -# branch on the command line and keeps package/beta in step with it, so a selection here would -# only start the restart loop the session script describes. List and report the stable branch; -# accept and ignore a choice. case "${1:-}" in -l) printf 'rel\n' ;; -c) printf 'rel\n' ;; diff --git a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-set-timezone b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-set-timezone index f60c99281..039e4d006 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-set-timezone +++ b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-set-timezone @@ -1,3 +1,2 @@ #!/bin/sh -# The session carries the device's time zone in TZ already; the client's own setter is a no-op. exit 0 diff --git a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-update b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-update index fa99b087a..bc2d00e8e 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-update +++ b/tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/steamos-update @@ -1,8 +1,4 @@ #!/bin/sh -# What the Steam client calls in Deck mode (from /usr/bin/steamos-polkit-helpers/, seen in its -# console log on device; a copy sits in /usr/bin as well) to look for a SteamOS update. There is no SteamOS -# here: the answer is "no update", which is exit 7 in SteamOS's convention, and the client moves -# on. --supports-duplicate-detection is a capability probe and answers yes. Staged from the apk. case "${1:-}" in --supports-duplicate-detection) exit 0 ;; esac diff --git a/tools/linuxfs/overlay/usr/bin/steamos-select-branch b/tools/linuxfs/overlay/usr/bin/steamos-select-branch index 6f0c9d806..501e630b1 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-select-branch +++ b/tools/linuxfs/overlay/usr/bin/steamos-select-branch @@ -1,8 +1,4 @@ #!/bin/sh -# The SteamOS update-branch selector the client calls in Deck mode. The session forces the client -# branch on the command line and keeps package/beta in step with it, so a selection here would -# only start the restart loop the session script describes. List and report the stable branch; -# accept and ignore a choice. case "${1:-}" in -l) printf 'rel\n' ;; -c) printf 'rel\n' ;; diff --git a/tools/linuxfs/overlay/usr/bin/steamos-session-select b/tools/linuxfs/overlay/usr/bin/steamos-session-select index 871f67b48..eaac87341 100644 --- a/tools/linuxfs/overlay/usr/bin/steamos-session-select +++ b/tools/linuxfs/overlay/usr/bin/steamos-session-select @@ -1,10 +1,4 @@ #!/bin/sh -# The SteamOS session switch. Big Picture's power menu runs it for "Switch to Desktop" (a Plasma -# session: "plasma", "plasma-wayland-persistent", ...) and waits for Game Mode to end; with nothing -# here it sat on "Switching to Desktop..." for ever. Here the desktop is DroidDeck's own: the app is -# asked for a desktop session with Steam's desktop client open in it (the request file -# droiddeck-steam-launch uses; the app ends this session and starts that one). Anything else - -# "gamescope", the session Game Mode already is - has nothing to do. echo "steamos-session-select: $*" >&2 case "${1:-}" in ""|gamescope) exit 0 ;; diff --git a/tools/linuxfs/overlay/usr/bin/steamos-update b/tools/linuxfs/overlay/usr/bin/steamos-update index f7536a443..bc2d00e8e 100755 --- a/tools/linuxfs/overlay/usr/bin/steamos-update +++ b/tools/linuxfs/overlay/usr/bin/steamos-update @@ -1,7 +1,4 @@ #!/bin/sh -# What the Steam client calls in Deck mode to look for a SteamOS update. There is no SteamOS -# here: the answer is "no update", which is exit 7 in SteamOS's convention, and the client moves -# on. --supports-duplicate-detection is a capability probe and answers yes. Staged from the apk. case "${1:-}" in --supports-duplicate-detection) exit 0 ;; esac diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent b/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent index c8ca11960..18130a57b 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-agent @@ -68,7 +68,6 @@ class AgentError(Exception): self.code = code -# --- X11: just enough of the protocol for root properties, the window tree and events --------- class X: """One connection to an Xwayland server. The rootfs has xprop but no python X library.""" @@ -266,7 +265,7 @@ class FocusWatcher(threading.Thread): while True: self.changed.wait(timeout=5) self.changed.clear() - time.sleep(0.2) # let a burst of property changes settle + time.sleep(0.2) try: snap = focus_snapshot() except (OSError, AgentError) as e: @@ -286,9 +285,7 @@ class FocusWatcher(threading.Thread): try: x = X(os.environ["DISPLAY"]) wanted = {x.atom(name) for name in FOCUS_PROPS} - x.select(x.root, 0x400000) # PropertyChangeMask - # Waits for events as long as there are none: the connect timeout left in place - # here dropped and remade the connection, and took a snapshot, every few seconds. + x.select(x.root, 0x400000) x.s.settimeout(None) self.changed.set() while True: @@ -300,7 +297,6 @@ class FocusWatcher(threading.Thread): time.sleep(2) -# --- Steam client DevTools (CDP) ----------------------------------------------------------------- class WebSocket: """A minimal RFC 6455 client: text frames out, text frames in. Enough for CDP.""" @@ -384,7 +380,7 @@ class WebSocket: op = b0 & 0x0F if op == 8: raise OSError("DevTools closed the WebSocket") - if op == 9: # ping + if op == 9: continue if op in (0, 1, 2): message += data @@ -481,7 +477,6 @@ def steam_selection(agent_dir, timeout=5): return {"appId": int(value)} -# --- Game processes ------------------------------------------------------------------------------ def game_pids(app_id): """Our processes started for this Steam app (Steam sets SteamAppId / SteamGameId in their @@ -538,7 +533,6 @@ def quit_game(agent_dir, app_id, timeout): return {"appId": int(app_id), "steps": steps, "remaining": game_pids(app_id)} -# --- Requests ------------------------------------------------------------------------------------ VR_HELPER = "/usr/local/bin/droiddeck-steamvr" VR_ACTIONS = ("status", "install", "start", "stop") @@ -716,7 +710,7 @@ def answer(agent_dir, path): result = dict(handle(agent_dir, req), ok=True) except AgentError as e: result = {"ok": False, "error": {"code": e.code, "message": str(e)}} - except Exception as e: # an answer of some kind always goes back + except Exception as e: result = {"ok": False, "error": {"code": "AGENT_FAILED", "message": "%s: %s" % (type(e).__name__, e)}} write_json(agent_dir / "resp" / (rid + ".json"), result) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-appimage-run b/tools/linuxfs/overlay/usr/local/bin/droiddeck-appimage-run index 847c272b6..20e6bbba4 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-appimage-run +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-appimage-run @@ -1,33 +1,17 @@ #!/bin/bash -# An AppImage imported from the front end (AppImageManager), extracted once at import into -# /opt/appimages/user/<id>/app -# and started from there: proot has no FUSE to mount the image, and extracting it at every -# start (APPIMAGE_EXTRACT_AND_RUN) would cost the whole image in /tmp each time. -# droiddeck-appimage-run /opt/appimages/user/<id> [args...] set -u dir=${1:?usage: droiddeck-appimage-run <imported AppImage directory> [args...]} shift export APPDIR=$dir/app -# APPIMAGE is left unset on purpose: with it an app believes it runs from an image file and offers -# to add itself to the menu (DuckStation) or update the file in place - DroidDeck already does the -# one, and there is no image file for the other. unset APPIMAGE export ARGV0=$(cat "$dir/name" 2>/dev/null || basename "$dir") if [ ! -x "$APPDIR/AppRun" ]; then echo "droiddeck-appimage-run: $APPDIR/AppRun is missing; import the AppImage again" >&2 exit 127 fi -# Browsers sandbox their own children with namespaces and a setuid helper, neither of which proot -# can give them (the Flatpak sandboxes get the same; see BwrapSpawner): Firefox and its forks (Zen, -# LibreWolf) by these switches, Electron and Chromium builds by --no-sandbox. Chromium on Wayland -# also asks the render node for a DRM version KGSL cannot give, so with an X server up it stays on -# X11, which it picks from the session type. export MOZ_DISABLE_CONTENT_SANDBOX=1 MOZ_DISABLE_GMP_SANDBOX=1 MOZ_DISABLE_RDD_SANDBOX=1 export MOZ_DISABLE_SOCKET_PROCESS_SANDBOX=1 MOZ_DISABLE_UTILITY_SANDBOX=1 -# Android 16 refuses Firefox the size seal on a tab's shared memory; see droiddeck-desktop. export MOZ_SHM_NO_SEALS=1 -# sharun images start the app through their own ld.so, patched not to read /etc/ld.so.preload, and -# drop LD_PRELOAD unless allowed: without this the app never gets the session's pads. if [ -s /etc/ld.so.preload ]; then preload=$(tr -s '\n' ':' < /etc/ld.so.preload) export LD_PRELOAD="${preload%:}${LD_PRELOAD:+:$LD_PRELOAD}" SHARUN_ALLOW_LD_PRELOAD=1 @@ -74,8 +58,6 @@ fi echo "== appimage $ARGV0 ($APPDIR) ${flags[*]} FEX $fex Qt $qt${QT_QPA_PLATFORM:+ ($QT_QPA_PLATFORM)}" cd "$APPDIR" || exit 1 started=$(mktemp "${TMPDIR:-/tmp}/droiddeck-appimage.XXXXXX") -# Apps expect a session bus; Electron's main process traps (SIGTRAP) on an address it cannot -# parse. A run mode has none, so one is started for the app, as for Flatpak apps. case ${DBUS_SESSION_BUS_ADDRESS:-} in unix:*) "${run[@]}" "${flags[@]}" "$@" ;; *) unset DBUS_SESSION_BUS_ADDRESS @@ -100,10 +82,6 @@ if [ "$rc" -ne 0 ] && [ -n "$started" ]; then done fi rm -f "$started" -# The program a session was started for may hand over to another process and exit: Electron apps -# relaunch themselves (Obsidian on its first start), launchers fork their real program. The -# session ends with this script, and takes the rest with it - so it waits while anything running -# from the image is still up. image_running() { local p exe arg cwd for p in /proc/[0-9]*; do diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-bwrap b/tools/linuxfs/overlay/usr/local/bin/droiddeck-bwrap index 0070565c7..909056fcd 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-bwrap +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-bwrap @@ -1,17 +1,4 @@ #!/usr/bin/python3 -# bubblewrap for Flatpak (FLATPAK_BWRAP), in a runtime that cannot have it. -# -# bwrap builds each sandbox from mount namespaces, which Android gives no app. proot gives the same -# picture - Flatpak's runtime as /usr, the app as /app, host paths bound where Flatpak asks - but a -# proot nested inside the session's proot runs every system call through two tracers, some thirty -# times slower on a program's start. So a sandbox is described to the app instead, over a socket -# only this app's uid may use, and the app starts it as a proot of its own beside the session's -# (BwrapSpawner). This process stands in for the sandbox until it ends: its output, its pid for -# --info-fd, and its exit status come back here. -# -# Sandboxes that remap nothing - Flatpak's install triggers (`--ro-bind / /` and the directory they -# update) and the D-Bus proxy (every top-level directory bound onto itself) - need no proot at all -# and run here directly, with the file descriptors Flatpak handed them. import base64 import json import os @@ -23,7 +10,6 @@ import sys SOCKET = "\0" + os.environ.get("DROIDDECK_BWRAP_SOCKET", "com.droiddeck.launcher.bwrap") VERSION = "bubblewrap 0.11.0 (droiddeck)" -# Options and how many arguments each takes; bwrap(1). NO_ARG = { "--unshare-user", "--unshare-user-try", "--unshare-ipc", "--unshare-pid", "--unshare-net", "--unshare-uts", "--unshare-cgroup", "--unshare-cgroup-try", "--unshare-all", "--share-net", @@ -68,7 +54,6 @@ def expand(argv): a = argv[i] if a == "--args" and i + 1 < len(argv): data = read_fd(int(argv[i + 1])) - # Each argument ends in a NUL; an empty one (--setenv LD_LIBRARY_PATH "") is kept. parts = data.split(b"\0") if parts and parts[-1] == b"": parts.pop() @@ -155,7 +140,6 @@ def parse(argv): elif a == "--dev": s.ops.append({"t": "dev", "dest": args[0]}) elif a in ("--seccomp", "--add-seccomp-fd"): - # proot is the sandbox; there is no filter to install under it. os.close(int(args[0])) elif a == "--block-fd": read_fd(int(args[0])) @@ -165,8 +149,6 @@ def parse(argv): s.info_fds.append(int(args[0])) elif a == "--json-status-fd": s.json_fds.append(int(args[0])) - # Everything else - namespaces, capabilities, labels, locks, --remount-ro, --chmod - has - # nothing to act on under proot. if not s.command: sys.stderr.write("bwrap: No command specified\n") sys.exit(1) @@ -196,7 +178,6 @@ def remaps_nothing(s): if not os.path.islink(op["dest"]) or os.readlink(op["dest"]) != op["target"]: return False elif t == "file": - # The D-Bus proxy's /.flatpak-info names the app to the bus; nothing here reads it. if op["dest"] != "/.flatpak-info": return False elif t == "dir": @@ -258,7 +239,6 @@ def run_by_app(s): for op in s.ops: op = dict(op) if op["t"] == "bind": - # The app resolves no guest symlinks; hand it the path they lead to. op["src"] = real(op["src"]) if op["t"] == "file": op["data"] = base64.b64encode(op["data"]).decode() @@ -274,7 +254,6 @@ def run_by_app(s): sys.stderr.write("bwrap: DroidDeck is not answering (%s); open DroidDeck and try again\n" % e) sys.exit(1) send(sock, b"R", json.dumps(request).encode()) - # This process is the sandbox as far as Flatpak is concerned: ended, it takes the sandbox down. for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP): signal.signal(sig, lambda *_: os._exit(128 + 15)) status = 1 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-bookmarks b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-bookmarks index 865f7e3b6..28bdd8ac5 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-bookmarks +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-bookmarks @@ -29,7 +29,7 @@ def steam_libraries(home, data): try: selected = {Path(e["path"]): str(e["label"]) for e in json.loads(Path(listing).read_text())} except (OSError, ValueError, KeyError, TypeError): - pass # Fall back to the client's registered libraries if the app's list is unreadable. + pass vdf = steam / "steamapps/libraryfolders.vdf" if vdf.is_file(): parser = runpy.run_path(str(Path(__file__).with_name("droiddeck-steam-ui-scale"))) @@ -76,7 +76,6 @@ def update_places(path, libraries): else: if not libraries: return - # Leave KDE's file version unset so it still adds its normal Home/Downloads/etc. places. root = ET.Element("xbel", {"version": "1.0"}) before = ET.tostring(root) seen = set() @@ -112,7 +111,6 @@ def update_places(path, libraries): def bookmarks_file(home, config): - # libfm-qt reads GTK3 bookmarks first, falling back to the legacy file when empty. current = config / "gtk-3.0/bookmarks" legacy = home / ".gtk-bookmarks" if current.is_file() and current.read_text(errors="surrogateescape").strip(): @@ -153,7 +151,6 @@ def main(): home = Path.home() config = Path(os.environ.get("XDG_CONFIG_HOME") or home / ".config") label = os.environ.get("BL_LIBRARY_LABEL") - # An unbound mount-point directory may remain from a previous session. available = bool(label) and (LIBRARY / "steamapps").is_dir() update_bookmark(bookmarks_file(home, config), label if available else None) data = Path(os.environ.get("XDG_DATA_HOME") or home / ".local/share") diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-clipboard b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-clipboard index 0de5aff20..ccb7768c6 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-clipboard +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-clipboard @@ -1,9 +1,5 @@ #!/bin/bash -# Plasma starts this after its nested Xwayland display is available. The session launcher still -# sees the outer compositor before startplasma, so starting its X11 helper there cannot work. [ -n "${BL_ANDROID_CLIPBOARD:-}" ] && [ -n "${DISPLAY:-}" ] || exit 0 -# The native helper dies with its parent. Keep this shell until it exits: Plasma's autostart -# launcher may itself exit immediately after starting us. /usr/local/bin/droiddeck-clipboard "$BL_ANDROID_CLIPBOARD" & child=$! trap 'kill "$child" 2>/dev/null' EXIT diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-games b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-games index 0773601f0..7b59fb468 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-games +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-games @@ -1,10 +1,4 @@ #!/usr/bin/python3 -# The client's installed games, on the desktop. The client writes a menu entry per game itself -# (`steam steam://rungameid/<id>`, with the game's icon), which the desktop's `steam` shim hands to -# the app - so an entry of ours exists only for a game the client wrote none for. Plus two links in -# the home folder to the game files. Run at every desktop start; stale entries go. -# -# droiddeck-desktop-games [steam root] import os import re import sys @@ -12,8 +6,6 @@ import sys HOME = os.environ.get("HOME", "/root") STEAM_ROOT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HOME, ".local/share/Steam") LIBRARIES = ((STEAM_ROOT, "Steam Games"), ("/mnt/droiddeck-sd", "SD Games")) -# The client's own tools and runtimes live in steamapps beside the games; they are not titles. A -# depot with a toolmanifest.vdf is one too (a runtime or a Proton), listed here or not. NOT_GAMES = {228980, 1070560, 1391110, 1493710, 1628350, 3127680, 4183110, 4185400, 4427310, 4628740} APPS_DIR = os.path.join(HOME, ".local/share/applications") PREFIX = "droiddeck-game-" @@ -41,7 +33,6 @@ def games(library): continue name = NAME.search(text) state = STATE.search(text) - # StateFlags 4 = fully installed; anything else is downloading, updating or broken. if not name or not state or (int(state.group(1)) & 4) == 0: continue installdir = INSTALLDIR.search(text) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-window-rules b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-window-rules index df6527ecd..aaf304a8d 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-window-rules +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-desktop-window-rules @@ -1,5 +1,4 @@ #!/bin/bash -# Port the old desktop's melonDS window rule once; later window rules belong to the user. set -euo pipefail config=${XDG_CONFIG_HOME:-$HOME/.config} marker=$config/droiddeck-window-rules-v1 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-esync b/tools/linuxfs/overlay/usr/local/bin/droiddeck-esync index 410a9c442..d4cb5267a 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-esync +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-esync @@ -1,7 +1,4 @@ #!/usr/bin/python3 -# droiddeck-esync: picks and stages the esync pack for each Proton build. -# Credit: esync and ntsync were designed by Elizabeth Figura for Wine and Proton, and fsync by Elizabeth Figura -# and Paul Gofman; droiddeck-esync, droiddeck-ntsync and droiddeck-fsync build on that work. import contextlib import fcntl import hashlib diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex b/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex index 22519f09b..91749f977 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-fex @@ -252,9 +252,6 @@ def install_preloads(root): if os.path.isfile(source): copy_if_changed(source, os.path.join(root, "lib/x86_64-linux-gnu/droiddeck", name)) wanted = [name for name in host_preloads() if name in shipped] - # The fault report is preloaded from the runtime's own list, which only x86 loaders read: through - # LD_PRELOAD it reached FEX's own loader too, and every FEX after it, which could not open it and - # said so at each start. Listed only with both builds, or i386 programs would say so instead. if len(shipped.get(FAULT_REPORT, ())) == len(GUEST_LIBDIRS): wanted.append(FAULT_REPORT) write_if_changed(os.path.join(root, "etc", "ld.so.preload"), "".join("/usr/$LIB/droiddeck/%s\n" % name for name in wanted)) @@ -411,8 +408,6 @@ def split_paths(value): def data_dir(env): - # FEXServer's socket lives here. Under ~/.local its host path passes sun_path's 108 bytes, and - # proot then binds it to a temporary name only the server sees: no client ever reaches it. runtime = env.get("XDG_RUNTIME_DIR") if runtime and os.path.isdir(runtime) and os.access(runtime, os.W_OK): return os.path.join(runtime, "fex") diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak index a8226b513..ca9f8ef70 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak @@ -1,27 +1,14 @@ #!/usr/bin/python3 -# The app's store talks to Flatpak through this: one JSON object per line on stdout, so progress -# reaches the front end as numbers rather than a terminal's redrawn bar. Per-user installation, -# Flathub as the remote, this device's architecture. -# -# droiddeck-flatpak install <app-id> the app and whatever runtimes it needs -# droiddeck-flatpak install-bundle <file> a .flatpak file, its runtimes from Flathub -# droiddeck-flatpak uninstall <app-id> the app, then runtimes nothing uses any more -# droiddeck-flatpak update [<app-id>] one app, or everything with an update -# droiddeck-flatpak updates what has one (a "updates" line) -# -# Lines: {"e":"op","i":1,"n":3,"kind":"install","ref":"..."} as each step starts, -# {"e":"progress","i":1,"n":3,"percent":45,"status":"..."} while it runs, -# {"e":"error","message":"..."} and {"e":"done"} at the end (with "id" for a bundle). Exit status 0 on success. import json import os import sys os.environ.setdefault("FLATPAK_BWRAP", "/usr/local/bin/droiddeck-bwrap") -import gi # noqa: E402 +import gi gi.require_version("Flatpak", "1.0") -from gi.repository import Flatpak, Gio, GLib # noqa: E402 +from gi.repository import Flatpak, Gio, GLib REMOTE = "flathub" @@ -82,12 +69,6 @@ def transaction(inst): progress.connect("changed", changed) def on_operation_error(tr, op, err, details): - # A runtime extension that fails to install (a missing translation, an unneeded GL - # driver) is not worth losing the app over; anything else ends the transaction. - # FLATPAK_TRANSACTION_ERROR_DETAILS_NON_FATAL is 1, but PyGObject does not always hand the - # flags over ("g_value_get_flags: assertion ... failed" and a 0). An extension installed - # alongside an app - its translations, debug data, a GL or codecs runtime - is what Flatpak - # marks non-fatal, as the flatpak CLI does: its failure costs that extension, not the app. try: non_fatal = bool(int(details) & 1) except (TypeError, ValueError): @@ -108,7 +89,6 @@ def transaction(inst): t.connect("ready", on_ready) t.connect("new-operation", on_new_operation) t.connect("operation-error", on_operation_error) - # A dependency found on more than one remote comes from the first (Flathub, the only one). t.connect("choose-remote-for-ref", lambda tr, ref, runtime_ref, remotes: 0) t.connect("end-of-lifed-with-rebase", lambda *a: False) t.connect("add-new-remote", lambda *a: False) @@ -135,7 +115,6 @@ def install(app_id): if app_ref(inst, app_id) is not None: emit(e="done", note="already installed") return 0 - # Flathub publishes apps on "stable"; libflatpak's default branch is "master". branch, last = None, None for candidate in ("stable", "master"): try: @@ -149,8 +128,6 @@ def install(app_id): t, state = transaction(inst) t.add_install(REMOTE, "app/%s/%s/%s" % (app_id, arch, branch), None) rc = run(t, state, report_done=False) - # A failure the transaction did not report (an error handler that raised, say) must not read - # as an install: the app is there, or it is not. if rc == 0 and app_ref(installation(), app_id) is None: emit(e="error", message="%s did not install; see flatpak-install.log" % app_id) return 1 @@ -192,7 +169,6 @@ def uninstall(app_id): t.add_uninstall(ref.format_ref()) rc = run(t, state) if rc == 0: - # Runtimes only this app used; Flatpak's own `uninstall --unused`. try: unused = inst.list_unused_refs(None, None) except GLib.Error: diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-run b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-run index 45a6e37ed..08e6e7a26 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-run +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-run @@ -1,19 +1,10 @@ #!/bin/bash -# A Flatpak app from the front end, full screen under gamescope like an emulator: -# droiddeck-flatpak-run <app-id> [args...] set -u export FLATPAK_BWRAP=/usr/local/bin/droiddeck-bwrap case ":${XDG_DATA_DIRS:-}:" in *"/flatpak/exports/share:"*) ;; *) export XDG_DATA_DIRS=$HOME/.local/share/flatpak/exports/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share} ;; esac -# Under gamescope - a program's session (BL_INSIDE), or droiddeck-gpu from the desktop -# (GAMESCOPE_WAYLAND_DISPLAY) - windows have to reach gamescope's X server. Flatpak would hand an -# app wayland-0 (it finds it in the runtime directory even with WAYLAND_DISPLAY unset) and the -# window would open behind gamescope. gamescope's own Wayland socket is no way out: Chromium on -# Wayland asks the render node it names for its DRM version, which KGSL cannot answer, and exits. -# The session is X11 as far as an app can tell; Electron picks its platform from that. -# Its own flatpak options (--branch, --command, from a menu entry) come first, then the app id. socket=() if [ -n "${BL_INSIDE:-}" ] || [ -n "${GAMESCOPE_WAYLAND_DISPLAY:-}" ]; then unset WAYLAND_DISPLAY @@ -21,7 +12,6 @@ if [ -n "${BL_INSIDE:-}" ] || [ -n "${GAMESCOPE_WAYLAND_DISPLAY:-}" ]; then socket=(--nosocket=wayland --socket=x11) fi echo "== flatpak run ${socket[*]} $*" -# Apps expect a session bus (settings, portals, single-instance checks); a run mode has none. if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-run-session >/dev/null 2>&1; then exec dbus-run-session -- flatpak run "${socket[@]}" "$@" fi diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-setup b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-setup index 2d0b2e790..7d117c453 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-setup +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-flatpak-setup @@ -1,26 +1,10 @@ #!/bin/bash -# Puts Flatpak into the runtime, with Flathub as a per-user remote. Run by the app under proot's -# fake root (pacman and pacman-key refuse any other uid), outside a session. -# -# The runtime's pacman database lists only the base image: the desktop and emulator packages were -# unpacked over it without registering. Asked for flatpak the usual way, pacman "reinstalls" about -# a hundred of those - Mesa among them, over the runtime's own Turnip build with its KGSL backend - -# and then runs every hook in the image, mkinitcpio included. So pacman only fetches and checks the -# few packages Flatpak needs on top of this image, verifying each against the Arch Linux ARM -# keyring, and they are unpacked here without hooks or scripts, like the desktop package itself. -# -# droiddeck-flatpak-setup install or repair; prints "== STEP ..." milestones set -u export LANG=C.UTF-8 step() { echo "== STEP $*"; } fail() { echo "== FAIL $*"; exit 1; } -# Flatpak, the libraries it and appstream add to this image, and the D-Bus proxy flatpak run puts -# between an app and the session bus. bubblewrap is not among them: Android gives an app no user -# namespaces, and droiddeck-bwrap stands in for it (FLATPAK_BWRAP). PACKAGES="flatpak ostree appstream libxmlb libmalcontent libfyaml accountsservice composefs xdg-dbus-proxy" -# Libraries Flatpak and the store's helper also need, which the Desktop package brings but a -# runtime without it may lack: each is fetched only when its file is not already there. while read -r pkg probe; do [ -n "$pkg" ] || continue compgen -G "$probe" >/dev/null || PACKAGES="$PACKAGES $pkg" @@ -41,8 +25,6 @@ WORK=/var/cache/droiddeck-flatpak CONF=$WORK/pacman.conf mkdir -p "$WORK/pkg" "$WORK/db" || fail "cannot create $WORK" -# The image's pacman.conf names a download user this rootfs does not have (only root exists) and a -# sandbox proot cannot set up. Its one mirror is plain HTTP; these serve the same repos over TLS. { for m in https://ca.us.mirror.archlinuxarm.org https://fl.us.mirror.archlinuxarm.org \ https://de3.mirror.archlinuxarm.org https://nl.mirror.archlinuxarm.org; do echo "Server = $m/\$arch/\$repo"; done @@ -58,8 +40,6 @@ if [ ! -f /etc/pacman.d/gnupg/pubring.gpg ] && [ ! -f /etc/pacman.d/gnupg/pubrin pacman-key --populate archlinuxarm >/dev/null 2>&1 || fail "pacman-key --populate failed" fi -# A database of our own, so the image's stays exactly as it was built: -Sy on it would leave the -# rest of the image a partial upgrade behind the repos. step "Reading the package lists" pacman --config "$CONF" --dbpath "$WORK/db" --noconfirm -Sy >/dev/null 2>&1 \ || pacman --config "$CONF" --dbpath "$WORK/db" --noconfirm -Sy 2>&1 | tail -3 \ @@ -67,8 +47,6 @@ pacman --config "$CONF" --dbpath "$WORK/db" --noconfirm -Sy >/dev/null 2>&1 \ [ -f "$WORK/db/sync/extra.db" ] || fail "could not reach the Arch Linux ARM mirrors" step "Downloading Flatpak" -# -w only downloads; pacman still checks each package's signature against the keyring. -dd: the -# dependencies are already in the image (and checked below by what the binaries actually link). out=$(pacman --config "$CONF" --dbpath "$WORK/db" --cachedir "$WORK/pkg" --noconfirm -Sw -dd $PACKAGES 2>&1) rc=$? echo "$out" | grep -vE '^\s*$' | tail -4 @@ -84,10 +62,8 @@ for name in $PACKAGES; do echo "$name $ver" >> "$WORK/installed.new" done mv -f "$WORK/installed.new" "$WORK/installed" -# Only the newest file of each package is worth keeping for a repair. find "$WORK/pkg" -type f -mtime +1 -delete 2>/dev/null -# What the binaries link is the real dependency list: anything this image lacks shows here. missing=$(for f in /usr/bin/flatpak /usr/lib/libflatpak.so.0 /usr/lib/libostree-1.so.1 /usr/bin/xdg-dbus-proxy; do ldd "$f" 2>&1 | grep -E 'not found|version .* not found' done | sort -u) @@ -98,9 +74,6 @@ python3 -c 'import gi; gi.require_version("Flatpak", "1.0"); from gi.repository step "Adding Flathub" export FLATPAK_BWRAP=/usr/local/bin/droiddeck-bwrap -# Flathub's descriptor - its URL and the key its commits are signed with - is carried here rather -# than fetched: adding the remote then needs no network, and a descriptor that did not arrive -# whole cannot be what failed. Flathub's own copy: https://dl.flathub.org/repo/flathub.flatpakrepo cat > "$WORK/flathub.flatpakrepo" <<'REPO' [Flatpak Repo] Title=Flathub @@ -115,7 +88,6 @@ if ! flatpak remote-list --user --columns=name 2>/dev/null | grep -qx flathub; t out=$(flatpak remote-add --user --if-not-exists flathub "$WORK/flathub.flatpakrepo" 2>&1) rc=$? echo "$out" - # What Flatpak said goes up with the failure, so the app can show it rather than a guess. [ $rc -eq 0 ] || fail "could not add the Flathub remote: $(echo "$out" | grep -v '^[[:space:]]*$' | tail -1)" fi echo "== DONE $(flatpak --version)" diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-fonts b/tools/linuxfs/overlay/usr/local/bin/droiddeck-fonts index cddcc15bc..030585f3d 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-fonts +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-fonts @@ -1,24 +1,12 @@ #!/bin/bash -# DroidDeck's fontconfig setup for the Linux runtime. The rootfs ships DejaVu alone and no rules -# for a default family: applications then find fonts but no default, and Firefox logs "no fonts" -# and draws pages without text. DejaVu also has no CJK, Thai or Arabic glyphs, so those draw as -# tofu boxes. The app binds Android's fonts for the session, which adds language-specific fallbacks -# separately. This script links fontconfig's default rules and writes generic DejaVu aliases to an -# owned conf.d file. Fontconfig reads local.conf afterward, so user overrides keep precedence and -# are never replaced. -# The first argument is the root to set up, "/" in a session; the tests point it at a temp tree. set -u root=${1:-/} root=${root%/} -# "/" strips to empty above; put it back so the paths below read as absolute and the test for a -# real session ("/") still holds. [ -n "$root" ] || root=/ conf_dir=$root/etc/fonts defaults_dir=$root/usr/share/fontconfig/conf.default mkdir -p "$conf_dir/conf.d" 2>/dev/null -# The default rules (sans-serif -> DejaVu Sans and the rest) live as links in /etc/fonts/conf.d, -# and the rootfs has none. Linked by name, so a file we ship is never overwritten. if [ -d "$defaults_dir" ]; then for f in "$defaults_dir"/*.conf; do [ -e "$f" ] || continue diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env index 454fb7e74..f74779c86 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env @@ -25,8 +25,6 @@ def game_id(env): return str(number % 2**32) -# A .NET (CoreCLR) runtime under FEX crashes in the runtime itself unless memory ordering is fully -# emulated and multiblock is off: the Stability preset. DOTNET_FEX = { "FEX_TSOENABLED": "1", "FEX_VECTORTSOENABLED": "1", @@ -69,8 +67,6 @@ class Game: return any(a == name or a.startswith(name + "=") for a in self.args) -# Each fix looks at a Game and returns (environment, extra arguments, note) for the games it -# applies to, or None. Adding one is writing a function and decorating it. FIXES = [] @@ -91,16 +87,12 @@ def dotnet_memory_ordering(game): @fix def godot_vulkan(game): - # Godot 4 on Direct3D 12 through VKD3D-Proton fails to create its swap chain and runs behind a - # black window; its own Vulkan renderer draws. if is_godot(game) and not game.has_arg("--rendering-driver"): return {}, ["--rendering-driver", "vulkan"], "Godot: Vulkan renderer" @fix def godot_sentry(game): - # The Sentry GDExtension's native crash handler takes the .NET runtime down under FEX. Godot - # loads it on demand and carries on without it. sentry = sorted(n[:-4] for n in game.files if n.startswith("libsentry.windows.") and n.endswith(".dll")) if is_godot(game) and sentry: return {"WINEDLLOVERRIDES": ";".join(n + "=d" for n in sentry)}, [], "Godot: Sentry crash handler off" @@ -166,8 +158,6 @@ def apply_config(env, config, app_id, fixes=None): result[name] = value else: raise ValueError("invalid environment value") - # The app's texture filtering (Display page), as DXVK options appended after the profiles so - # a DXVK_CONFIG entry of the user's keeps its own. game_dxvk = config.get("dxvkConfigGames", {}) if not isinstance(game_dxvk, dict): raise ValueError("invalid game dxvk configuration") diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-gpu-window b/tools/linuxfs/overlay/usr/local/bin/droiddeck-gpu-window index 42a01f274..f5cb4007c 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-gpu-window +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-gpu-window @@ -8,8 +8,6 @@ import sys def advertise_compositing(): - # This is gamescope's private X server: it composites alpha buffers already, but GTK also - # requires the compositing-manager selection and frame-extents hint before enabling CSD. x = c.CDLL('libX11.so.6') display_type, window_type = c.c_void_p, c.c_ulong x.XOpenDisplay.argtypes = [c.c_char_p] @@ -41,7 +39,6 @@ def advertise_compositing(): x.XChangeProperty(display, root, atom('_NET_SUPPORTED'), atom('ATOM'), 32, 2, c.byref(extents), 1) x.XSync.argtypes = [display_type, c.c_int] x.XSync(display, False) - # Keep the selection's connection alive until the program exits. return x, display diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-launcher-cores b/tools/linuxfs/overlay/usr/local/bin/droiddeck-launcher-cores index dad3d1033..3ea0c5681 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-launcher-cores +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-launcher-cores @@ -22,14 +22,10 @@ import sys import time from pathlib import Path -# Image paths (lower case, backslashes), matched at the end: the helpers held back, and the -# programs whose window having focus means the user is looking at the launcher. HELPERS = ("\\rockstar games\\social club\\socialclubhelper.exe",) FRONTS = HELPERS + ("\\rockstar games\\launcher\\launcher.exe",) NICE = 19 POLL = 2.0 -# How often a held launcher is looked over again: for a helper the unheld Launcher.exe started -# since, and for threads the program has moved back. RESCAN = 10.0 STEAM_CLIENT = 769 @@ -72,8 +68,6 @@ class Cores: try: os.sched_setaffinity(tid, cpus) except OSError: - # A mask outside the app's cpuset now (Android shrinks it in the background) is - # EINVAL; the session's own is always allowed. os.sched_setaffinity(tid, self.own()) os.setpriority(os.PRIO_PROCESS, tid, nice) @@ -83,7 +77,7 @@ class Holder: self.proc = proc self.cpufreq = cpufreq self.cores = cores or Cores() - self.held = {} # pid -> {tid: (cpus, nice) before the hold} + self.held = {} self.core = None def slow_core(self): @@ -117,10 +111,8 @@ class Holder: if now != hold: self.cores.set(tid, *hold) except OSError: - continue # the thread ended + continue if tid not in before: - # A thread that already has the hold inherited it from a held parent: it gets - # what its process had, or the helpers' first. before[tid] = now if now != hold else before.get(pid) or self.first() or now for pid in [pid for pid in self.held if not (self.proc / str(pid)).exists()]: del self.held[pid] @@ -191,8 +183,6 @@ class Governor: self.holder.release() self.report("released: the launcher has focus") return - # Nothing to look for until a game has had focus: launchers only run for one, and a launcher - # left running after its game stays held through the Steam client's own focus. if self.app in (None, 0, STEAM_CLIENT) and not self.holder.held: return now = self.clock() diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 b/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 index ce74b0b50..7ce786740 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 @@ -14,7 +14,7 @@ import uuid import gi gi.require_version("Gio", "2.0") -from gi.repository import Gio, GLib # noqa: E402 +from gi.repository import Gio, GLib NAME = "org.freedesktop.login1" ROOT = "/org/freedesktop/login1" @@ -56,8 +56,6 @@ class LoginManager: self.preparation = None info = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION).interfaces[0] self.registration = bus.register_object(ROOT, info, self.method, self.property, None) - # Steam can issue its suspend command through the legacy UPower service even - # while listening to login1's sleep/resume notifications. Use one handshake. upower_info = Gio.DBusNodeInfo.new_for_xml(UPOWER_INTROSPECTION).interfaces[0] self.upower_registration = bus.register_object(UPOWER_ROOT, upower_info, self.method, None, None) self.monitor = Gio.File.new_for_path(str(self.directory)).monitor_directory(Gio.FileMonitorFlags.NONE, None) @@ -86,8 +84,6 @@ class LoginManager: self.pending = uuid.uuid4().hex self.notify(True) reply.return_value(None) - # Flush the signal and method reply without blocking this D-Bus callback. Once both are - # on the wire, publishing the request lets the host safely freeze this service. self.bus.flush(None, self.publish_after_flush, self.pending) def publish_after_flush(self, connection, result, token): @@ -122,7 +118,6 @@ class LoginManager: return if not re.fullmatch(r"[a-f0-9]{32}", token) or self.pending is not None: return - # Android may have returned before this helper read its request. if self.read_state() == token + ":awake": return self.pending = token @@ -138,8 +133,6 @@ class LoginManager: return self.check_state() if self.pending == token: - # Keep Steam runnable while it handles the system sleep notification. - # This is a bounded grace period, not a claim that every game has saved. self.preparation = GLib.timeout_add(NATIVE_PREPARE_MS, self.finish_preparation, token) def finish_preparation(self, token): @@ -171,7 +164,6 @@ class LoginManager: def check_ack(self): self.timeout = None if self.pending and self.read_state() != self.pending + ":paused": - # A missing host or a failed freeze must not strand Steam in black. self.request.unlink(missing_ok=True) self.native_ready.unlink(missing_ok=True) self.resume() @@ -180,8 +172,6 @@ class LoginManager: def resume(self, force=False): if self.pending is None: if force: - # A missed suspend command leaves Steam without the first half of - # the handshake. Replay both notifications, without pausing again. self.notify(True) self.bus.flush(None, self.finish_recovery, None) return diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install b/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install index c1fe35846..87e741c46 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-msi-install @@ -43,7 +43,6 @@ CABEXTRACT = os.environ.get("DROIDDECK_CABEXTRACT", str(TOOLS / "cabextract")) SEVENZIP = os.environ.get("DROIDDECK_7Z", str(TOOLS / "7z")) MSI_MAGIC = b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" BURN_MAGIC = 0x00F14300 -# A bundle's manifest names ARM64 packages too; a game's prefix wants the x86 and x64 ones. SKIP_PLATFORMS = ("arm64", "arm") @@ -53,9 +52,6 @@ def tool_env(): env["LD_LIBRARY_PATH"] = ":".join(p for p in (str(TOOLS), env.get("LD_LIBRARY_PATH", "")) if p) return env -# The folders Windows Installer knows by name, as a 64-bit Windows 10 prefix lays them out (Proton's -# user is steamuser). None is a folder whose files a game never needs: installer temp files and -# shortcuts. SKIP = None KNOWN_FOLDERS = { "TARGETDIR": "C:\\", "ROOTDRIVE": "C:\\", "WindowsVolume": "C:\\", @@ -78,25 +74,17 @@ for _name in ("TempFolder", "DesktopFolder", "ProgramMenuFolder", "StartMenuFold "TemplateFolder", "MyPicturesFolder", "AdminToolsFolder"): KNOWN_FOLDERS[_name] = SKIP -# What Wine's msi reports on a 64-bit Windows 10 prefix, for the packages' conditions. SYSTEM_PROPERTIES = { "VersionNT": "603", "VersionNT64": "603", "WindowsBuild": "9600", "ServicePackLevel": "0", "Msix64": "6", "ALLUSERS": "1", "Privileged": "1", "AdminUser": "1", "MsiNTProductType": "1", "UILevel": "2", "INSTALLLEVEL": "1", "ProgramFilesFolder": None, "VersionMsi": "5.00", } -# Win64 component (msidbComponentAttributes64bit); file attributes. COMPONENT_64BIT = 0x100 FILE_NONCOMPRESSED = 0x2000 -# Custom action types that only set a property or a folder: done here, the rest are named in notes. SET_PROPERTY, SET_DIRECTORY, ERROR_ACTION = 51, 35, 19 -# Microsoft's "SFP CA" merge module (MDAC, MSXML): on Windows 2000 and later it parks system DLLs in a -# cache folder and its own code copies them into the system folder past Windows File Protection. -# Skipping the folder change puts them where that copy would. Its actions, and the side-by-side -# merge module's (done here from MsiAssembly), are then left out of the notes. SFP_CACHE_ACTION = "CDirSystemSFPCacheDir" HANDLED_ACTIONS = ("Wdsfpca_", "SxsInstallCA", "SxsUninstallCA") -# Tables whose work needs the package's own code or is not done here yet. UNSUPPORTED_TABLES = ("SelfReg", "Class", "ProgId", "TypeLib", "Extension", "Verb", "MIME", "AppId", "IniFile", "ServiceInstall", "MoveFile", "IsolatedComponent", "PublishComponent", "ODBCDriver", "ODBCDataSource") @@ -123,7 +111,6 @@ class Span: progress(text, self.lo + (self.hi - self.lo) * max(0.0, min(1.0, fraction))) -# ---------------------------------------------------------------- reading the package IDT_ESCAPES = str.maketrans({"\x19": "\n", "\x11": "\r", "\x15": "\t", "\x1b": ";"}) @@ -182,7 +169,6 @@ def to_int(value, default=0): return default -# ---------------------------------------------------------------- conditions TOKEN = re.compile(r'\s*(?:(?P<str>"[^"]*")|(?P<num>-?\d+)(?![\w.])|(?P<op>~?(?:<>|<=|>=|><|<<|>>|=|<|>))' r'|(?P<paren>[()])|(?P<word>[%$?&!]?[A-Za-z_][\w.]*))') @@ -330,7 +316,6 @@ class Conditions: return value not in ("", None) -# ---------------------------------------------------------------- the install def win_to_rel(path): """C:\\windows\\x\\ -> drive_c/windows/x; None for a path off drive C or a folder left out.""" @@ -375,7 +360,7 @@ class Installer: self.span = span or Span() self.notes = [] self.registry = [] - self.files = {} # File key -> rel path it was installed to + self.files = {} self.counts = {"files": 0, "assemblies": 0, "registry": 0} template = self.pkg.summary.get("template", "") self.platform = template.split(";")[0].strip().lower() @@ -385,7 +370,6 @@ class Installer: for name, path in KNOWN_FOLDERS.items(): self.props[name] = path if path is not None else "\0skip" for row in self.pkg.table("Property"): - # The package's own defaults, but never over the folders and the system it runs on. if row["Property"] not in self.props or row["Property"] in ("INSTALLLEVEL", "ALLUSERS", "UILevel"): self.props[row["Property"]] = row["Value"] self.props["ALLUSERS"] = "1" @@ -397,7 +381,6 @@ class Installer: self._resolving = set() self._set_by_action = set() - # -- states, folders, formatted text def feature_state(self, name): return 3 if name in self.included_features else -1 @@ -411,13 +394,11 @@ class Installer: return None value = self.props.get(key) row = self.directories.get(key) - # A folder Windows knows by name, one a custom action set, or a property naming a path. if key in KNOWN_FOLDERS or key in self._set_by_action or row is None: return None if value in (None, "", "\0skip") else win_to_rel(value) parent_key = row.get("Directory_Parent", "") default = long_name(row.get("DefaultDir", "").split(":")[0]) if not parent_key or parent_key == key: - # A root: TARGETDIR (SourceDir) is drive C. return "drive_c" self._resolving.add(key) try: @@ -466,7 +447,6 @@ class Installer: text = re.sub(r"\[([^\[\]]*)\]", one, text) return text - # -- what gets installed def run_set_actions(self): """The property/folder-setting custom actions, in sequence order; the rest go to the notes.""" @@ -581,8 +561,6 @@ class Installer: self.pkg.stream(cabinet[1:], cab) own = True else: - # An external cabinet: a file beside the package, as an installer wrapper lays it out - # (vc_red.cab beside vc_red.msi, netfx_Full.mzz beside netfx_Full_x64.msi). cab = sibling(Path(self.pkg.path), cabinet) if cab is None: self.notes.append("external cabinet %s is not part of the package; its files are missing" % cabinet) @@ -601,9 +579,6 @@ class Installer: def put(self, source, rel): target = self.out / rel - # A DuplicateFile row can name the file's own folder and name (the .NET host packages copy - # dotnet.exe onto itself); Windows Installer takes that as nothing to do. Found and diagnosed - # by whatsupbros on the DroidDeck Discord. if target.exists() and Path(source).samefile(target): return False target.parent.mkdir(parents=True, exist_ok=True) @@ -615,7 +590,6 @@ class Installer: files = self.pkg.table("File") missing = 0 destinations = {} - # Where each file goes: its component's folder, or the assembly store for a global assembly. for row in files: comp = row["Component_"] if comp not in self.included_components: @@ -676,9 +650,6 @@ class Installer: self.notes.append("%d file(s) were not in the package's cabinets" % missing) def value(self, hive, key, name, kind, data, wow64): - # A 32-bit component writes through the 32-bit view, as Wine's msi does: HKLM\Software is - # Software\Wow6432Node, and the prefix's link keys there (Classes, TypeLib, Fonts...) lead - # to the keys both views share - droiddeck-wincomponents follows them when it writes. lower = key.lower() if hive == "HKLM" and wow64 and lower.startswith("software\\") and not lower.startswith("software\\wow6432node\\"): key = "Software\\Wow6432Node\\" + key[len("software\\"):] @@ -787,7 +758,6 @@ class Installer: self.check() if not self.counts["files"] and not self.registry: raise MsiError("the package installs nothing here") - # A component made of several packages (32- and 64-bit) gets them all in one registry.json. values = [] try: with open(self.out / "registry.json", encoding="utf-8") as source: @@ -801,7 +771,6 @@ class Installer: "counts": self.counts, "notes": self.notes} -# ---------------------------------------------------------------- installer wrappers def sibling(path, name): """A file next to [path] called [name], whatever its case; None when there is none.""" @@ -979,8 +948,6 @@ def install_wrapper(path, out): try: result = Installer(str(package), out, span=Span(lo, hi)).install() except MsiError as error: - # A package with nothing for this prefix (a patch for another Windows) is skipped, - # unless it is the only one; anything else is the whole installer's failure. if "installs nothing" in str(error) and len(packages) > 1: total["notes"].append("%s: %s" % (package.name, error)) continue diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-netmanager b/tools/linuxfs/overlay/usr/local/bin/droiddeck-netmanager index 7d468e5ff..f8df451bb 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-netmanager +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-netmanager @@ -1,8 +1,4 @@ #!/usr/bin/python3 -# Based on WinNative (maxjivi05, feature/wayland-gamescope e7a224ae), GPL-3.0. -# Steam speaks NetworkManager; Android owns the radios, routes and connection. Export the -# app's live snapshot through that API, including nearby Wi-Fi access points. Requests to -# scan are handed to Android; Android settings still handle joining networks. import ipaddress import json import re @@ -16,7 +12,7 @@ from xml.sax.saxutils import escape import gi gi.require_version("Gio", "2.0") -from gi.repository import Gio, GLib # noqa: E402 +from gi.repository import Gio, GLib NAME = "org.freedesktop.NetworkManager" ROOT = "/org/freedesktop/NetworkManager" @@ -60,7 +56,6 @@ def v(signature, value): def read_snapshot(): - # Atomic replacement by the app means we never see a partially written handover. try: state = json.loads(SNAPSHOT.read_text()) if not isinstance(state, dict) or not isinstance(state.get("connected"), bool): @@ -71,7 +66,6 @@ def read_snapshot(): def legacy_snapshot(): - # Older APKs do not publish transport metadata. Never guess Wi-Fi from an interface name. try: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.connect(("192.0.2.1", 9)) @@ -101,10 +95,10 @@ def connection_name(state): def connectivity(state): if not state["connected"]: - return 1 # NONE + return 1 if state.get("validated"): - return 4 # FULL - return 2 if state.get("captivePortal") else 3 # PORTAL / LIMITED + return 4 + return 2 if state.get("captivePortal") else 3 def metered(state): @@ -155,7 +149,6 @@ def active_ap(state): def ap_security(capabilities): - # NetworkManager's cipher/key-management flags, from Android's beacon capabilities. wpa = rsn = 0 for group in re.findall(r"\[([^]]+)\]", capabilities.upper()): flags = 0 @@ -280,7 +273,6 @@ def objects(state): if present: found[path] = {NAME + f".IP{version}Config": ip_config(state, version)} if up and not wifi and state.get("wifiEnabled"): - # Android can keep its Wi-Fi radio available while cellular/VPN is the default route. wireless = objects(dict(state, connected=False, transport="none", interface=state.get("wifiInterface", "wlan0"), metered=None)) wifi_device = ROOT + "/Devices/Wifi" @@ -304,13 +296,11 @@ def settings(state): "ssid": v("ay", list((state.get("ssid") or "Wi-Fi").encode("utf-8"))), "mode": v("s", "infrastructure"), } elif state.get("transport") == "cellular": - result["gsm"] = {} # Android owns the SIM/APN; no credentials are exported. + result["gsm"] = {} return result def interface_info(interface, values): - # Register properties on the actual objects as well as returning ObjectManager snapshots. - # Steam/libnm also use Properties.Get/GetAll and introspect the wireless methods. props = "".join(f'<property name="{escape(key)}" type="{value.get_type_string()}" access="read"/>' for key, value in values.items()) node = Gio.DBusNodeInfo.new_for_xml(f'<node><interface name="{interface}">{METHODS.get(interface, "")}{props}</interface></node>') @@ -372,7 +362,7 @@ class NetworkManager: def check(self): state = read_snapshot() if state is None: - return True # A transient read failure must not replace a known link with Ethernet. + return True if state == self.state: return True before, after = self.objects, objects(state) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-open b/tools/linuxfs/overlay/usr/local/bin/droiddeck-open index 3ceb080df..0809a228f 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-open +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-open @@ -33,9 +33,7 @@ APPIMAGE_RUN = os.path.join(BIN, "droiddeck-appimage-run") SCRIPT_RUN = os.path.join(BIN, "droiddeck-script-run") STEAM_LAUNCH = os.path.join(BIN, "droiddeck-steam-launch") CACHE = os.path.join(HOME, ".cache", "droiddeck") -# Where the app adds each kind, and what an added one holds. IMPORTS = {"appimage": ("/opt/appimages/user", "app"), "folder": ("/opt/droiddeck-apps", "entry")} -# Android's own folders on shared storage: never copied in whole. SHARED_FOLDERS = ("alarms", "android", "audiobooks", "dcim", "documents", "download", "movies", "music", "notifications", "pictures", "podcasts", "recordings", "ringtones") SCAN_LIMIT = 4000 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-open-url b/tools/linuxfs/overlay/usr/local/bin/droiddeck-open-url index 69d5c65aa..890833519 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-open-url +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-open-url @@ -36,7 +36,6 @@ def where(url): def main(argv): url = argv[0] if argv else "" - # The Steam client's own form (steam://openurl/<address>), in case a launcher passes it on. if url.startswith("steam://openurl/"): url = url[len("steam://openurl/"):] launch_dir = os.environ.get("BL_LAUNCH_DIR", "") diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-pad-defaults b/tools/linuxfs/overlay/usr/local/bin/droiddeck-pad-defaults index 4e15fae07..04966c4b2 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-pad-defaults +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-pad-defaults @@ -1,9 +1,4 @@ #!/usr/bin/python3 -# Player 1 on the session's controller, for every emulator that would otherwise start on the -# keyboard. The session's pad is an Xbox 360 controller (libfakeinput's identity outside Steam) that -# SDL maps by itself; what an emulator lacks is a profile that points Player 1 at it. One is -# written only where the emulator has none yet - a profile the user made is never touched. -# Run at every desktop start and before every program from the rail. import os HOME = os.environ.get("HOME", "/root") @@ -21,10 +16,6 @@ def seed(path, text, what, done="Player 1 on the controller"): return True -# RPCS3: its global input configuration (rpcs3::utils::get_input_config_dir() + "Default.yml"). -# Without one it adds the keyboard pad handler (pad_config.cpp, cfg_input::load). SDL handler -# names from rpcs3/Input/sdl_pad_handler.cpp (button_list, init_config); the device is -# "<SDL gamepad name> <n>" (sdl_pad_handler::enumerate_devices). RPCS3_PLAYER1 = """Player 1 Input: Handler: SDL Device: Xbox 360 Controller 1 @@ -71,13 +62,8 @@ def rpcs3(): rpcs3_shader_mode() -# RPCS3's default shader mode (Async Recompiler with Shader Interpreter) builds 6650 interpreter -# pipelines at the start of every boot, minutes on an Adreno. The async recompiler compiles only -# what a game draws, in the background, once. A new install starts with it (a config.yml that -# holds only this is merged over RPCS3's defaults); an existing one still on the default is moved -# once, and a marker keeps any later choice the user makes in RPCS3's own settings. RPCS3_INTERPRETER = "Async Recompiler with Shader Interpreter" -RPCS3_RECOMPILER = "Async Recompiler (multi-threaded)" # the GUI says "Async Shader Recompiler"; this is the value (system_config_types.cpp) +RPCS3_RECOMPILER = "Async Recompiler (multi-threaded)" def rpcs3_shader_mode(): @@ -101,7 +87,6 @@ def rpcs3_shader_mode(): open(marker, "w").close() -# ---- ini helpers (DuckStation and PCSX2 share one ini dialect, keys may repeat) ------------- def ini_section(lines, name): """(start, end) line indices of [name]'s body, or None.""" @@ -132,7 +117,6 @@ def patch_ini_section(path, section, body, what, needle="SDL-", require=None, do return new = body.strip("\n").splitlines() if span: - # The controller type the emulator already wrote stays; only bindings are added. have = {l.split("=", 1)[0].strip() for l in lines[span[0]:span[1]] if "=" in l} new = [l for l in new if not (l.split("=", 1)[0].strip() == "Type" and "Type" in have)] end = span[1] @@ -147,8 +131,6 @@ def patch_ini_section(path, section, body, what, needle="SDL-", require=None, do print(f"droiddeck-pad-defaults: {what}: {done} ({path})") -# DuckStation: SDL-<n>/<name>, axes signed (src/util/sdl_input_source.cpp); Pad1 keys from -# src/core/analog_controller.cpp. Its defaults bind the keyboard only (settings.cpp). DUCKSTATION_PAD1 = """Type = AnalogController Up = SDL-0/DPadUp Down = SDL-0/DPadDown @@ -244,17 +226,12 @@ def bios_folder(base): d = os.path.join(base, "bios") if os.path.isdir(ROMS): try: - os.makedirs(d, exist_ok=True) # where the user is told to put their BIOS + os.makedirs(d, exist_ok=True) except OSError: pass return d -# DuckStation (PS1) - set up like ARMSX2: users bring their own BIOS and games, in the ROMs folder. -# Front-end runs use -batch -bigpicture -fullscreen, so the guide button is the pause menu -# (Hotkeys OpenPauseMenu, src/core/hotkeys.cpp), not the analog toggle. Keys from -# src/core/settings.cpp: [BIOS] SearchDirectory, [GPU] Renderer ("Vulkan"), [Main] -# SetupWizardIncomplete (src/duckstation-qt/qthost.cpp). DUCKSTATION_PAD1_GUIDE_FREE = "\n".join(l for l in DUCKSTATION_PAD1.splitlines() if not l.startswith("Analog ")) + "\n" @@ -266,8 +243,6 @@ def duckstation(): if not os.path.isfile(ini) and os.path.isfile(legacy): ini = legacy if not os.path.isfile(ini): - # A new player: everything DuckStation's first-time setup asks is answered here (it finds - # the BIOS in its folder by itself), so a game from the front end boots straight away. bios = bios_folder(ps1) seed(ini, "[Main]\nSetupWizardIncomplete = false\nInhibitScreensaver = false\n\n" f"[BIOS]\nSearchDirectory = {bios}\n\n" @@ -282,17 +257,12 @@ def duckstation(): done="the guide button opens the pause menu") patch_ini_section(ini, "GameList", f"RecursivePaths = {ps1}\n", "DuckStation", needle=ROMS, done="games listed from the ROMs folder") - # One set up by an earlier build still has its first-time setup pending, in the way of a game. if ini_get(ini, "Main", "SetupWizardIncomplete") == "true": ini_set(ini, "Main", "SetupWizardIncomplete", "false") print("droiddeck-pad-defaults: DuckStation: first-time setup already done here") - # Keeping the screen awake goes through org.freedesktop.ScreenSaver, which nothing in the session - # provides (Android decides when the screen sleeps): DuckStation only warns it failed, every game. if (ini_get(ini, "Main", "InhibitScreensaver") or "true") == "true": ini_set(ini, "Main", "InhibitScreensaver", "false") print("droiddeck-pad-defaults: DuckStation: no screensaver inhibit (no such service here)") - # Once, for a DuckStation set up before this: its own empty BIOS folder moves to the ROMs folder, - # and Automatic becomes Vulkan. A later choice in DuckStation's settings is kept. marker = os.path.join(os.path.dirname(ini), ".droiddeck-setup") if os.path.exists(marker): return @@ -307,8 +277,6 @@ def duckstation(): open(marker, "w").close() -# PCSX2: FaceSouth/... names (pcsx2/Input/SDLInputSource.cpp), keys from SIO/Pad/PadDualshock2.cpp. -# An ini without [UI] SettingsVersion is thrown away and rebuilt, so only a complete one is patched. PCSX2_PAD1 = """Type = DualShock2 Up = SDL-0/DPadUp Right = SDL-0/DPadRight @@ -346,8 +314,6 @@ def pcsx2(): -# Dolphin: SDL/<id>/<SDL gamepad name>; control names from InputCommon/.../SDL/SDLGamepad.h (Left Y+ -# is up). A section that exists is taken whole - keys it lacks are unbound - so both are complete. DOLPHIN_GC = """[GCPad1] Device = SDL/0/Xbox 360 Controller Buttons/A = `Button S` @@ -406,11 +372,6 @@ Nunchuk/Stick/Right = `Left X+` """ -# ARMSX2 - the PS2 emulator (PCSX2 with ARM64 recompilers; upstream PCSX2 interprets on ARM64). Its -# files are PCSX2's under $XDG_CONFIG_HOME/ARMSX2 (pcsx2/Pcsx2Config.cpp). The user supplies the BIOS -# and the games; they live in the ROMs folder (/root/ROMs). The guide button opens the pause menu -# (Hotkeys OpenPauseMenu) - the way out of a game started from the front end, which runs ARMSX2 -# without its window (-batch -bigpicture) - so it is not also the analog toggle here. ARMSX2_PAD1 = "\n".join(l for l in PCSX2_PAD1.splitlines() if not l.startswith("Analog ")) + "\n" ROMS = "/root/ROMs" @@ -432,7 +393,7 @@ def ps2_bios_folder(): d = os.path.join(base, "bios") if os.path.isdir(ROMS): try: - os.makedirs(d, exist_ok=True) # where the user is told to put their BIOS + os.makedirs(d, exist_ok=True) except OSError: pass return d @@ -443,8 +404,6 @@ def armsx2(): ini = os.path.join(root, "inis/PCSX2.ini") old = os.path.join(CONFIG, "PCSX2") if not os.path.isfile(ini) and os.path.isfile(os.path.join(old, "inis/PCSX2.ini")): - # Someone who played on PCSX2 here: the same settings, BIOS choice and memory cards (the - # saves), copied once. PCSX2's own folder is left as it was. import shutil for sub in ("inis", "memcards", "bios", "gamesettings", "cheats", "patches", "covers", "inputprofiles", "textures"): @@ -453,22 +412,16 @@ def armsx2(): shutil.copytree(src, os.path.join(root, sub), dirs_exist_ok=True) print(f"droiddeck-pad-defaults: ARMSX2: settings and memory cards copied from PCSX2 ({root})") elif not os.path.isfile(ini): - # A new player: everything ARMSX2's first-time setup asks is answered here - the BIOS - # folder in the ROMs folder, the ps2 folder as its game list, the controller on Player 1 - - # and the BIOS file itself is picked below once one is there. bios, games = ps2_bios_folder(), ps2_folder() seed(ini, "[UI]\nSettingsVersion = 1\nSetupWizardIncomplete = false\n\n" f"[Folders]\nBios = {bios}\n\n" f"[GameList]\nRecursivePaths = {games}\n\n" - # Vulkan outright rather than Auto's pick: the renderer every other path here uses. "[EmuCore/GS]\nRenderer = 14\n\n" "[Hotkeys]\nOpenPauseMenu = Keyboard/Escape\nOpenPauseMenu = SDL-0/Guide\n\n" "[Pad1]\n" + ARMSX2_PAD1, "ARMSX2", "first-time setup, BIOS from " + bios) armsx2_bios(ini) return patch_ini_section(ini, "Pad1", ARMSX2_PAD1, "ARMSX2", require="SettingsVersion") - # The guide button is the pause menu here; PCSX2's profile (ours, copied over) had it as the - # analog toggle as well. with open(ini, encoding="utf-8", errors="replace") as f: lines = f.read().splitlines() kept = [l for l in lines if l.replace(" ", "") != "Analog=SDL-0/Guide"] @@ -497,7 +450,6 @@ def armsx2_bios(ini): if current and os.path.isfile(os.path.join(folder, current)): return try: - # A PS2 BIOS dump is 4 MiB (a few are 8); the .nvm/.mec files beside it are not dumps. dumps = sorted(f for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f)) and 4 << 20 <= os.path.getsize(os.path.join(folder, f)) <= 8 << 20) except OSError: @@ -510,12 +462,6 @@ def armsx2_bios(ini): print(f"droiddeck-pad-defaults: ARMSX2: BIOS {pick}") -# Dolphin (PC) has no controller menu, so the guide button is its Toggle Fullscreen hotkey: out of full -# screen is Dolphin's own window with its Config / Graphics / Stop buttons. Hotkeys.ini is all-or- -# nothing - a file with one line drops every other default - so a new one carries Dolphin's defaults, -# generated from HotkeyManager.cpp's LoadDefaults (Linux branch, RetroAchievements on) plus the guide; -# an existing one only gets the guide added to its Toggle Fullscreen. The Wii remote's Home, which -# was on the guide button, is on the right shoulder button instead. DOLPHIN_GUIDE = "`SDL/0/Xbox 360 Controller:Guide`" DOLPHIN_HOTKEYS = """[Hotkeys] Device = XInput2/0/Virtual core pointer @@ -632,11 +578,6 @@ def dolphin(): ini_set(hotkeys, "Hotkeys", "General/Toggle Fullscreen", (current + " | " if current else "") + DOLPHIN_GUIDE) print("droiddeck-pad-defaults: Dolphin: the guide button shows Dolphin's window (Toggle Fullscreen)") - # Games from the ROMs folder's gamecube and wii folders, subfolders included; Vulkan; no - # NKit warning (it will not let a game start until a box is ticked - games run either way, - # the warning is about loading times, netplay and save states); no statistics question. - # Names from Dolphin's MainSettings.cpp / UISettings.cpp. Only what is not set yet is added, - # except the game folders, which are added to whatever the user has. ini = os.path.join(d, "Dolphin.ini") gc = system_folder(("gamecube", "gc", "ngc"), "gamecube") wii = system_folder(("wii",), "wii") @@ -655,9 +596,6 @@ def dolphin(): count = 0 paths = [ini_get(ini, "General", "ISOPath%d" % i) for i in range(count)] changed = [] - # The whole ROMs folder as a game folder (what Dolphin's "set a games directory" was pointed at - # before) would, searched with its subfolders, have Dolphin open every PS2 and PS3 image too; - # the system folders below cover it. if any((p or "").rstrip("/") == ROMS for p in paths): paths = [p for p in paths if p and p.rstrip("/") != ROMS] for i, p in enumerate(paths): @@ -680,7 +618,6 @@ def dolphin(): changed.append("subfolders") if changed: print("droiddeck-pad-defaults: Dolphin: games from " + ", ".join(changed)) - # Once, for a Dolphin set up before this: Vulkan unless it was chosen already, and the NKit box. marker = os.path.join(d, ".droiddeck-setup") if os.path.exists(marker): return @@ -694,10 +631,6 @@ def dolphin(): print("droiddeck-pad-defaults: Dolphin: Vulkan, no NKit warning") -# Cemu: an SDLController profile for the GamePad. The uuid is "<index>_<SDL GUID>" and SDL's GUID -# carries a CRC of the device name; this is the one SDL reports for the session's pad -# ("Xbox 360 Controller (0)", 045e:028e - read from RPCS3's SDL log on the device). Mapping ids -# from VPADController.h, values from Controller.h, Cemu's own SDL default layout plus Home. CEMU_MAP = [(1, 1), (2, 0), (3, 3), (4, 2), (5, 9), (6, 10), (7, 42), (8, 43), (9, 6), (10, 4), (11, 11), (12, 12), (13, 13), (14, 14), (15, 7), (16, 8), (17, 45), (18, 39), (19, 44), (20, 38), (21, 47), (22, 41), (23, 46), (24, 40), (27, 5)] @@ -717,11 +650,6 @@ CEMU_XML = """<?xml version="1.0" encoding="UTF-8"?> """ -# Cemu's settings, so its Getting Started dialog (shown while settings.xml is missing, and whose -# folder picker cannot reach ROMs) never blocks a launch: the Wii U games folder, Vulkan -# (Graphic api 1), and audio through Cubeb (the only backend on Linux; api 3) on the default -# device for both the TV and the GamePad speaker - some titles play everything through the -# GamePad - at full volume (Cemu's own default is 20 %). Keys from config/CemuConfig.cpp. CEMU_SETTINGS = """<?xml version="1.0" encoding="UTF-8"?> <content> <mlc_path></mlc_path> @@ -749,11 +677,6 @@ def cemu(): done=f"games in {games}, Vulkan, Cubeb audio on the default device") -# melonDS 1.x: raw SDL joystick indices. The session's pad has the kernel's Xbox 360 layout -# (buttons A B X Y LB RB Back Start Guide LS RS = 0..10, hat 0 for the d-pad); the d-pad values -# also take the left stick (EmuInstanceInput.cpp; encoding InputConfig/MapButton.h). The guide -# button toggles full screen (HK_FullscreenToggle), as it does in Dolphin: the front end starts -# melonDS full screen (-f), and leaving it shows the window's menus for settings. MELONDS_GUIDE = 8 MELONDS_JOY = f"""[Instance0.Joystick] A = 1 @@ -771,28 +694,15 @@ Right = 65794 HK_FullscreenToggle = {MELONDS_GUIDE} """ -# The screen a handheld's wide panel suits best: the top screen as big as the panel's height allows -# with the bottom screen alone beside it, centred (ScreenLayout.h: Horizontal = 2, EmphTop = 1; -# melonDS's Hybrid shows the big screen a second time, small, above the bottom one; nobody needs -# it twice). Sharp pixels: no filtering, and no integer steps that leave the panel half used. Keys as -# melonDS writes them (Window.cpp windowCfg). MELONDS_WINDOW = """[Instance0.Window0] ScreenLayout = 2 ScreenSizing = 1 IntegerScaling = false ScreenFilter = false """ -# melonDS's own defaults for those, which an existing install is moved off once. Hybrid (3) was -# this preset's first layout, and moves on too. MELONDS_WINDOW_DEFAULTS = {"ScreenLayout": ("0", "3"), "ScreenSizing": ("0",), "ScreenFilter": ("true",)} -# The window melonDS returns to when the guide button leaves full screen: 1x wide (the width of -# two DS screens side by side, Screen.cpp screenGetMinSize) and 16:9, so gamescope stretches it to -# fill the panel and the menu bar and cursor come out large enough to use. A bigger window keeps -# its menus at their real size, tiny on a handheld. Qt's saveGeometry() v3 blob: magic, version, -# frame and normal rects (x, y, right, bottom), screen, maximized, full screen, screen width, and -# the normal rect again (QWidget::restoreGeometry). def melonds_geometry(): import base64, struct w, h = 512, 288 @@ -845,11 +755,6 @@ def toml_str(path): return '"' + path.replace("\\", "\\\\").replace('"', '\\"') + '"' -# The user's own BIOS, firmware and DSi NAND, found in the ROMs folder's nds/bios (or firmware) -# folder by what each file is: the DS BIOSes by size and checksum (the GBA BIOS is also 16 KB), -# firmware and NAND by size, the DSi boot ROMs (both 64 KB) by the 7 or 9 in their names. A path -# is only filled where melonDS has none, and external files are only switched on when a whole set -# was found - without them melonDS runs DS games on its built-in free BIOS. DS_BIOS7_MD5 = "df692a80a5b1bc90728bc3dfc76cd948" DS_BIOS9_MD5 = "a392174eb3e572fed6447e956bde4b25" @@ -924,7 +829,6 @@ def melonds(): lines = text.splitlines() done = [] if not any(isinstance(v, int) and v != -1 and k != "HK_FullscreenToggle" for k, v in joy.items()): - # Replace melonDS's own all-unbound table with ours (a TOML table can be defined only once). out, skipping = [], False for line in lines: t = line.strip() @@ -937,8 +841,6 @@ def melonds(): elif joy.get("HK_FullscreenToggle", -1) == -1 and MELONDS_GUIDE not in joy.values(): toml_set(lines, "Instance0.Joystick", "HK_FullscreenToggle", str(MELONDS_GUIDE)) done.append("guide button toggles full screen") - # Once, for a melonDS set up before this: the big top screen, where it is still on its own - # defaults. The marker keeps whatever the user picks in melonDS's View menu afterwards. if not os.path.exists(marker): for l in MELONDS_WINDOW.splitlines()[1:]: k, v = (x.strip() for x in l.split("=", 1)) @@ -957,9 +859,6 @@ def melonds(): print(f"droiddeck-pad-defaults: melonDS: {'; '.join(done)} ({path})") -# PPSSPP (SDL): its pad defaults leave the shoulders, the right stick and the triggers unbound on -# SDL builds (SDLJoystick.cpp codes vs KeyMapDefaults.cpp); a [ControlMapping] that exists replaces -# every default, so this one is complete, keyboard included (device 1 keyboard, 10 pad). PPSSPP_CONTROLS = """[ControlMapping] Up = 1-19,10-19 Down = 1-20,10-20 @@ -989,8 +888,6 @@ def ppsspp(): seed(os.path.join(CONFIG, "ppsspp/PSP/SYSTEM/controls.ini"), PPSSPP_CONTROLS, "PPSSPP") -# RetroArch: the udev joypad driver finds nothing here (no udev database in the runtime); sdl2 -# does, and matches this profile by vendor/product (task_autodetect.c). RETROARCH_PROFILE = """input_driver = "sdl2" input_device = "Xbox 360 Controller" input_vendor_id = "1118" @@ -1044,11 +941,6 @@ def retroarch(): print(f"droiddeck-pad-defaults: RetroArch: joypad driver udev -> sdl2 ({cfg})") -# yuzu and its forks: Player 1 in [Controls] of <name>/qt-config.ini, which each writes at its first -# start with the keyboard bound. Bindings are input_common ParamPackages, laid out by position as the -# frontend's own auto-map does (sdl_driver.cpp GetDefaultButtonBinding); the guid is SDL's with the -# name CRC cleared (GetGUID). A key is only read while its \default is false. Done once per emulator, -# and only while Player 1 is still on the keyboard. SWITCH_PAD = "engine:sdl,port:0,guid:030000005e0400008e02000010010000," SWITCH_PLAYER1 = { "button_a": "button:1", "button_b": "button:0", "button_x": "button:3", "button_y": "button:2", diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-extra b/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-extra index 68997cc5e..8625e5555 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-extra +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-extra @@ -1,24 +1,4 @@ #!/usr/bin/python3 -# Installs a third-party Proton build into the client's compatibilitytools.d, so it appears in the -# Compatibility dropdown beside our own Proton ARM64 and can be picked per game. -# -# droiddeck-proton-extra <steam root> ge [--tag GE-Proton11-7] -# droiddeck-proton-extra <steam root> cachyos [--tag cachyos-11.0-20260703-slr] -# droiddeck-proton-extra <steam root> droiddeck [--tag DroidDeck-Proton-11-7.5] -# droiddeck-proton-extra <steam root> <url of an aarch64 tarball> -# droiddeck-proton-extra <steam root> /path/to/an/aarch64/tarball -# droiddeck-proton-extra <steam root> --list what is installed now -# -# All three projects publish native ARM64 builds - GloriousEggroll and DroidDeck-Proton as -# "*-aarch64.tar.gz", CachyOS as "*-arm64.tar.xz" - and all are ordinary Proton trees: a python -# entry point beside a files/ directory holding Wine, DXVK and VKD3D. That is the same shape as the -# depot the Steam client downloads, so the runtime can run them the same way. -# -# What it cannot use is their toolmanifest. Like Valve's ARM64 depot they declare -# require_tool_appid, which makes Steam stack pressure-vessel underneath - and pressure-vessel -# needs unprivileged user namespaces, which an Android app does not get. Patching that out is -# steam-compatibility's job (adopt_extras); this script only puts the tree in place and then -# calls it, so an install is usable without restarting the client. import hashlib import json import os @@ -30,17 +10,12 @@ import urllib.error import urllib.request RELEASES = "https://api.github.com/repos/%s/releases" -# Each source: the GitHub repo, and how its ARM64 asset is named. Ordered newest-release-first by -# the API, so "latest usable" is the first full release that actually carries an ARM64 asset - the -# projects do not build one for every tag. SOURCES = { "ge": ("GloriousEggroll/proton-ge-custom", re.compile(r"aarch64\.tar\.(gz|xz)$")), "cachyos": ("CachyOS/proton-cachyos", re.compile(r"arm64\.tar\.(gz|xz)$")), "droiddeck": ("The412Banner/DroidDeck-Proton", re.compile(r"aarch64\.tar\.(gz|xz)$")), } USER_AGENT = "droiddeck-proton-extra" -# Room for the tarball and the tree it unpacks to. GE-Proton's aarch64 build is ~650 MB packed and -# unpacks to about three times that; refusing early beats a half-written tool directory. NEED_BYTES = 4 * 1024 * 1024 * 1024 @@ -121,8 +96,6 @@ def extract(tarball, into): rc = subprocess.call(["tar", "-xf", tarball, "-C", into]) if rc != 0: raise SystemExit("droiddeck-proton-extra: tar failed (%d)" % rc) - # A Proton tarball holds one top-level directory. Take whichever entry has the entry point so a - # stray ._ file or a nested layout does not decide it. entries = [e for e in sorted(os.listdir(into)) if not e.startswith(".")] for entry in entries: path = os.path.join(into, entry) @@ -162,9 +135,6 @@ def main(): argv.pop(0) tag = argv.pop(0) if argv else None - # A tarball already on the device is used as it is: a session that has to pull 650 MB down - # first shows nothing on screen for ten minutes, so the download can be done ahead of time and - # handed over by path. local = None if what in SOURCES: tag, url, sums = release_asset(what, tag) if tag else pick_asset(what) @@ -200,8 +170,6 @@ def main(): tree = extract(tarball, unpacked) name = os.path.basename(tree) final = os.path.join(tools, name) - # Replaced rather than merged: a half-written tree from an interrupted run would otherwise - # leave stale files behind that Proton would load. if os.path.exists(final): shutil.rmtree(final, ignore_errors=True) os.rename(tree, final) @@ -211,8 +179,6 @@ def main(): shutil.rmtree(staging, ignore_errors=True) log("installed %s" % name) - # Patches out require_tool_appid and wraps the entry point, which is what makes it launchable - # here. Doing it now means the client picks the tool up on its next restart with no extra step. subprocess.call(["steam-compatibility", steam_root]) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-script-run b/tools/linuxfs/overlay/usr/local/bin/droiddeck-script-run index c4dd30c57..55606d8c5 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-script-run +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-script-run @@ -1,18 +1,14 @@ #!/bin/bash -# A script added on the Desktop page (UserApps), run from its own folder: -# droiddeck-script-run /opt/droiddeck-apps/<id> [args...] set -u dir=${1:?usage: droiddeck-script-run <added app directory> [args...]} shift entry=$(cat "$dir/entry" 2>/dev/null) || { echo "droiddeck-script-run: $dir has no entry" >&2; exit 127; } cd -P "$dir/files" 2>/dev/null || { echo "droiddeck-script-run: $dir/files is missing; add the script again" >&2; exit 127; } folder=$PWD -# Shared storage is noexec, so the script is handed to its interpreter rather than executed. first= IFS= read -r first < "$entry" first=${first%$'\r'} interp=(/bin/bash) -# A program as the entry (the desktop's copy of a game's folder) runs itself. case $first in '#!'*) read -r -a interp <<< "${first#'#!'}" ;; $'\x7f'ELF*) interp=() ;; esac fex=$(cat "$dir/fex" 2>/dev/null) case $fex in on|off) ;; *) fex=auto ;; esac @@ -31,7 +27,6 @@ case ${DBUS_SESSION_BUS_ADDRESS:-} in fi ;; esac rc=$? -# A launcher script often starts the game and returns; the session lasts while it runs. running() { local p exe arg cwd for p in /proc/[0-9]*; do diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists b/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists index 053df0e93..e6a8ecb4d 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists @@ -1,40 +1,11 @@ #!/bin/bash -# Marks Steam's shared redistributables as already run in every Proton prefix, and in the -# template Proton makes new prefixes from. -# -# A game whose depot lists _CommonRedist makes the client run the real installer inside the -# prefix on first launch. Those are x86 bundles, so they run under FEX, and the 2019 one leaves -# a task behind every time: its main thread exits while a helper stays blocked reading a pipe it -# also holds the write end of, so the process never reaps and the client keeps waiting on it. -# -# The client skips a "Run Process" block whose HasRunKey is already set, which is the same escape -# WineUtils.seedVcRedistRegistryIfDllsPresent() takes for the app's own containers. The names and -# versions come out of the install scripts rather than a list kept here, because Valve revises -# them; a prefix already carrying the same set is left alone. -# -# Games also carry install scripts of their own (<game>/_CommonRedist/vcredist/2022/installscript.vdf -# and the like), often with another version in the value name; those are read too. And the VC++ -# 2015-2022 runtime is recorded as installed the way Microsoft's installers look for it: on ARM64 -# Proton the x64 bundle otherwise stops with "Microsoft Visual C++ 2015-2022 Redistributable (arm64) -# is required", while Proton's own msvcp140/vcruntime140 already serve the game. -# -# A Vulkan runtime installer is the one left to run. Proton has no stand-in for it: for a game whose -# Steam compat config says nativevulkanloader (Red Dead Redemption 2) it loads vulkan-1.dll native -# only, and Khronos' loader is in the prefix only once that installer has run - without it the game -# exits with STATUS_DLL_NOT_FOUND before it opens a window. The installer finishes in a second. set -u steam_root=${1:?steam root} LIBRARIES=${DROIDDECK_LIBRARIES-/mnt/droiddeck /mnt/droiddeck-sd} redist="$steam_root/steamapps/common/Steamworks Shared/_CommonRedist" -# Every process an install script would run, as the registry key and value the client writes once -# it has run it. Names sit one level inside "Run Process", the key they share a level further in. -# "read_scripts vulkan" lists only the Vulkan runtime installers, which the plain call leaves out. read_scripts() { xargs -0 -r awk -v want="${1:-redists}" ' - # A block whose HasRunKey is set is one process; its name is the key the block opened under, at - # whatever depth - an install script nests it under "Run Process", the client'"'"'s evaluator script - # for a game wraps the same blocks once more. FNR == 1 { depth = 0; pending = "" } /^[[:space:]]*\{/ { depth++; name[depth] = pending; key[depth] = ""; vulkan[depth] = 0; pending = ""; next } /^[[:space:]]*\}/ { @@ -56,8 +27,6 @@ read_scripts() { xargs -0 -r awk -v want="${1:-redists}" ' } '; } pairs=$(find "$redist" -name installscript.vdf -print0 2>/dev/null | sort -z | read_scripts) -# The client downloads the scripts with the first game that wants them, which is too late for that -# game; until then the set they held when this was written stands in. (WinNative's list.) [ -n "$pairs" ] || pairs=$(sed 's/^\([^ ]*\) /HKEY_LOCAL_MACHINE\\\\Software\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\\1\t/' <<'KNOWN' DirectX\\Jun2010 dxsetup .NET\\4.8 4.8 @@ -78,8 +47,6 @@ vcredist\\2022 x64 14.51.36247.0 KNOWN ) -# The games' own scripts. Finding them walks every game folder, so the list is kept until a game is -# installed, updated or removed (its appmanifest changes) - this runs every quarter minute. cache="$steam_root/.droiddeck-game-scripts" libraries="$steam_root" for library in $LIBRARIES; do libraries="$libraries @@ -96,21 +63,14 @@ if [ "$(head -n 1 "$cache" 2>/dev/null)" != "$sig" ]; then done <<<"$libraries" | sort -u } >"$cache.tmp" && mv -f "$cache.tmp" "$cache" fi -# DROIDDECK_SEED_SCRIPTS: more scripts, one path a line - droiddeck-game-env passes the client's -# evaluator script for the game about to run its installers, read before the client looks. game_scripts() { { tail -n +2 "$cache" 2>/dev/null; printf '%s\n' "${DROIDDECK_SEED_SCRIPTS:-}"; } | sed '/^$/d' | tr '\n' '\0'; } game_pairs=$(game_scripts | read_scripts) [ -n "$(printf '%s' "$game_pairs" | tr -d '[:space:]')" ] && pairs=$(printf '%s\n%s\n' "$pairs" "$game_pairs" | sort -u | sed '/^$/d') -# A Vulkan runtime installer marked as run by an earlier seeding still has its marker: those come out -# again below, so the client runs the installer on the game's next launch. declare -A vulkan=() while IFS= read -r line; do [[ $line == *$'\t'* ]] && vulkan[$line]=1 done < <(game_scripts | read_scripts vulkan) -# The VC++ 2015-2022 runtime as its installers check for it, for each architecture and both registry -# views. The version is the newest the scripts above know, so any 2015-2022 bundle finds itself -# already installed and exits without asking for anything. VCRT_MAJOR=14 VCRT_MINOR=51 VCRT_BUILD=36247 vcrt() { local view arch @@ -124,9 +84,6 @@ vcrt() { done } -# Each prefix's marker lists the names it was given, so it is handed only what it lacks. One stamp -# for the whole set flipped between the session's set and the evaluator's (which adds the game's -# own names) on every launch, and every prefix was appended to again, inside the launch. want=() declare -A wanted=() while IFS= read -r line; do @@ -142,9 +99,6 @@ block_for() { printf '%s\n' "$@" | awk -F'\t' -v now="$now" ' if (path == $1) next printf "\n[%s] %d\n\"%s\"=dword:00000001\n", path, now, $2 - # The client writes these from 32-bit steam.exe, so its own copy lands under Wow6432Node; - # both spellings are seeded because the reader picks by its own word size. - # Joined rather than sub()stituted: gawk and mawk read backslashes in a replacement differently. if (substr(path, 1, 10) != "Software\\\\") next printf "\n[%s] %d\n\"%s\"=dword:00000001\n", "Software\\\\Wow6432Node\\\\" substr(path, 11), now, $2 @@ -154,7 +108,6 @@ block_for() { printf '%s\n' "$@" | awk -F'\t' -v now="$now" ' for line; do [[ $line == vcrt$'\t'* ]] && vcrt "$now"; done } -# Takes the given markers out of a registry file again, in both views, keeping its date. unseed() { local reg=$1 out shift @@ -186,9 +139,6 @@ unseed() { return 1 } -# Proton builds a new prefix from its template, and the client runs the install scripts in that -# prefix moments later, so the template carries the markers too. Its date is kept: Proton reads a -# changed one as a new template and upgrades every prefix made from the old. for reg in "$steam_root"/steamapps/common/Proton*/files/share/default_pfx*/system.reg \ /mnt/droiddeck-sd/steamapps/common/Proton*/files/share/default_pfx*/system.reg \ /root/Games/*/steamapps/common/Proton*/files/share/default_pfx*/system.reg \ diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index 6eb0059e8..f915472ed 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -1,27 +1,14 @@ #!/bin/bash -# The Linux runtime's session. DroidDeck execs this under proot with the output size and mode; -# the first pass starts gamescope, which execs this script again inside it for the program: -# droiddeck-session desktop the file manager -# droiddeck-session run <path> a Linux program (AppImage, script or ELF) -# droiddeck-session steam [steam-url] the native Steam client -# droiddeck-session lxqt the KDE Plasma desktop (DroidDeck's desktop package) -# BL_WIDTH / BL_HEIGHT size the output, BL_FPS limits it, BL_LOG collects everything said here. set -u export HOME=${HOME:-/root} mode=${1:-desktop} shift || true -# One line per milestone, greppable as "== STEP", so a first run reads as a narrative rather than -# something to be reconstructed from Steam's own chatter. The app shows these while it waits. step() { echo "== STEP $(date -u +%H:%M:%S) $*"; } if [ -n "${BL_LOG:-}" ] && [ -z "${BL_INSIDE:-}" ]; then mkdir -p "$(dirname "$BL_LOG")" 2>/dev/null - # The client alone writes megabytes an hour; one earlier log is kept, not every session's. if [ "$(stat -c %s "$BL_LOG" 2>/dev/null || echo 0)" -gt 8388608 ]; then mv -f "$BL_LOG" "$BL_LOG.1" fi - # A guest looping on an error writes gigabytes in minutes (an Xwayland proot had lost track of - # managed 9 GB of one line in 19 min): the log keeps its first BL_LOG_CAP bytes and the rest is - # drained, not refused - a writer that filled it must not die of SIGPIPE. exec > >({ dd of="$BL_LOG" oflag=append conv=notrunc iflag=count_bytes count="${BL_LOG_CAP:-268435456}" bs=65536 status=none; cat >/dev/null; }) 2>&1 echo "== droiddeck-session $(date) mode=$mode $*" echo "== uid=$(id -u) size=${BL_WIDTH:-?}x${BL_HEIGHT:-?} fps=${BL_FPS:-0} icd=${VK_ICD_FILENAMES:-none}" @@ -29,11 +16,6 @@ if [ -n "${BL_LOG:-}" ] && [ -z "${BL_INSIDE:-}" ]; then echo "== rootfs: $(cat /.version 2>/dev/null || echo unversioned)" fi -# Every program loads the locale LANG names (C.UTF-8) before main(). glibc ships it as a directory, -# which it looks for once per category under two spellings before reading gconv's module lists: -# about 30 opens, each a stop in proot, for every process in the session, a script's dirname or cat -# included. An archive holding it (and gconv's cache) makes that one open. Built again when the -# glibc package changes, or when the archive does (locale-gen rewrites it). prepare_locale() { local dir=/usr/lib/locale stamp=/usr/lib/locale/.droiddeck-archive want have= pkg pkg=(/var/lib/pacman/local/glibc-[0-9]*) @@ -41,7 +23,6 @@ prepare_locale() { [ -f "$stamp" ] && read -r have < "$stamp" [ "$have" = "$want" ] && [ -f "$dir/locale-archive" ] && [ ! "$dir/locale-archive" -nt "$stamp" ] && return 0 [ -d "$dir/C.utf8" ] && command -v localedef >/dev/null 2>&1 || return 0 - # localedef names a new archive by hard link, which Android refuses; libblsession renames it. DROIDDECK_LINK_RENAME=1 localedef --add-to-archive --replace "$dir/C.utf8" >/dev/null 2>&1 || return 0 rm -f /usr/lib/gconv/gconv-modules.cache command -v iconvconfig >/dev/null 2>&1 && iconvconfig >/dev/null 2>&1 @@ -50,12 +31,6 @@ prepare_locale() { } [ -z "${BL_INSIDE:-}" ] && prepare_locale -# What earlier sessions left in /tmp. The guest's /tmp is a directory in the rootfs, never emptied -# by a boot, and Android ends sessions by killing them: each left its mangoapp config, the client's -# temporary directories and shared-memory sockets, Chromium's singleton directory (the web -# helper's, an Electron app's), a session bus socket per run and a run's start stamp - 1,400 -# entries on a device after a few weeks. Each kind is swept while nothing that could own one runs, -# with one find for the lot. clear_stale_tmp() { local running names=() n running=$(pgrep -af 'steamwebhelper|steamrtarm64/steam|--type=|droiddeck-appimage-run|dbus-run-session|dbus-daemon.*--session|dbus-launch|kwin|plasmashell' 2>/dev/null) @@ -81,22 +56,13 @@ clear_stale_tmp() { } [ -z "${BL_INSIDE:-}" ] && clear_stale_tmp -# Until the runtime is stable, Steam's own logs are copied beside the session log at exit so a -# failed launch can be handed over as one folder. collect_steam_logs() { [ -n "${BL_DEBUG_DIR:-}" ] || return 0 local steam_root=${XDG_DATA_HOME:-$HOME/.local/share}/Steam mkdir -p "$BL_DEBUG_DIR/steam" 2>/dev/null || return 0 - # Into steam/ beside this session's own logs. The app scrubs these line by line after the - # session ends - they carry the account's session token and id - so nothing here is the last - # word on what the user ends up sharing; this is just the copy that survives the runtime. - # Up to 8 MB each, as the app's own copy takes them: the client's CEF and *.previous.txt logs - # reach 17 MB and say little about this session, and every share scrubs whatever is here. [ -d "$steam_root/logs" ] && find "$steam_root/logs" -maxdepth 1 -type f -size -8192k \ -exec cp {} "$BL_DEBUG_DIR/steam/" \; 2>/dev/null [ -f "$HOME/.steam/steam/logs/steamwebhelper.log" ] && cp "$HOME/.steam/steam/logs/steamwebhelper.log" "$BL_DEBUG_DIR/steam/" 2>/dev/null - # NOT copied, ever: loginusers.vdf, config.vdf and the ssfn* files, which hold the credentials - # themselves rather than a mention of them. local sync_store=$HOME/.local/share/droiddeck-esync sync_file if [ -d "$sync_store" ] && mkdir -p "$BL_DEBUG_DIR/droiddeck-esync" 2>/dev/null; then for sync_file in launches.log tools.tsv wanted.tsv prefixes.json; do @@ -109,20 +75,9 @@ collect_steam_logs() { fi echo "== steam logs copied to $BL_DEBUG_DIR/steam" } -# A session that is killed never reaches the copy below, and those are the sessions worth reading. trap 'collect_steam_logs 2>/dev/null || true' TERM INT HUP -# The driver everything in this session draws with: gamescope's compositing, the client's UI -# (OpenGL through Zink), every game Proton launches, and in desktop mode every program on it. -# BL_VK_DRIVER names an imported driver's ICD manifest on the Android side, which is bound into the -# session at its own path; unset means the runtime keeps the Turnip it was built with. -# VK_DRIVER_FILES REPLACES the loader's search, so a manifest whose library is unreadable would -# leave the session with no Vulkan at all - check both before setting it, and say so in the log -# rather than fail silently half a minute later. if [ -n "${BL_VK_DRIVER:-}" ]; then bl_icd_lib=$(sed -n 's/.*"library_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$BL_VK_DRIVER" 2>/dev/null | head -1) - # JSON may escape the separator: org.json writes "\/data\/user\/0\/...". The Vulkan loader - # parses that correctly; this check has to as well, or an absolute path looks relative and gets - # glued onto the manifest's directory - which is exactly what happened the first time. bl_icd_lib=$(printf '%s' "$bl_icd_lib" | sed 's|\\/|/|g') case $bl_icd_lib in /*) ;; @@ -130,7 +85,6 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then esac if [ -r "$BL_VK_DRIVER" ] && [ -n "$bl_icd_lib" ] && [ -r "$bl_icd_lib" ]; then export VK_DRIVER_FILES="$BL_VK_DRIVER" - # Loaders older than 1.3.207 only know the old name; both are harmless together. export VK_ICD_FILENAMES="$BL_VK_DRIVER" echo "== vulkan driver: imported $bl_icd_lib (via $BL_VK_DRIVER)" else @@ -138,20 +92,8 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then fi unset bl_icd_lib fi -# The app's gamescope build (tools/gamescope) asks for realtime-priority Vulkan queues on this -# switch instead of on CAP_SYS_NICE, which proot never has; it falls back to normal priority -# where the driver refuses, and the runtime's own build ignores it. On KGSL Turnip (Mesa 26.2) it -# changes nothing yet: the driver reports one submit-queue priority and creates every KGSL context -# with the same flags, so no priority reaches the GPU. Kept for a driver that maps it, and off -# unless the app's Performance page turns it on (BL_GAMESCOPE_REALTIME=1). export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0} -# A sleep that starts no process. Under proot every exec is a traced execve plus the loader's -# opens and maps, and the watchers below poll for the whole session: `sleep 1` in a loop was 60 -# execs a minute each. read -t on a pipe nothing ever writes to waits in bash itself. -# The agent bridge's guest side (docs/agent-control.md): runs what the app's agent provider asks -# for inside this session and publishes gamescope's focus. Its files live under the session root -# ($BL_LAUNCH_DIR/agent), which the app empties before each session. start_agent() { [ -n "${BL_LAUNCH_DIR:-}" ] && [ -x /usr/local/bin/droiddeck-agent ] || return 0 mkdir -p "$BL_LAUNCH_DIR/agent" @@ -165,23 +107,13 @@ export XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share} export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} export XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache} mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" -# No SteamVR runs at session start. A marker left by a session that died with SteamVR up would put -# the Cardboard VR layer's display stand-in into this one (docs/cardboard-vr.md). rm -f "${XDG_RUNTIME_DIR:-/run/droiddeck}/droiddeck-steamvr/active" if [ "$mode" = lxqt ] || [ "$mode" = desktop ]; then /usr/local/bin/droiddeck-desktop-bookmarks || true fi -# Flatpak (the store's apps): its sandboxes through the app instead of bubblewrap, which needs the -# user namespaces Android keeps from apps, and its apps' menu entries and icons on the desktop. export FLATPAK_BWRAP=/usr/local/bin/droiddeck-bwrap export XDG_DATA_DIRS=$XDG_DATA_HOME/flatpak/exports/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share} -# The cores a program runs on outside Steam (the desktop and everything started from it, a -# program from the rail): the user's game cores when set (BL_GAME_CPUS), else every core but the -# slowest cluster - Android is free to park an emulator's threads on the little cores, and those -# are what compile shaders and recompile PPU/SPU code. BL_PIN_CORES=0 leaves the scheduler alone. -# A prime-only client mask can be reset without a thread-count change (Steam or Android). -# Read status with shell builtins; only invoke taskset when a single-core mask has drifted. client_single_core_matches() { local wanted=$1 task key cpus shift @@ -216,9 +148,7 @@ program_cores() { } if [ -z "${BL_INSIDE:-}" ]; then - # Install the shared default-family aliases before Steam, gamescope or the desktop queries fonts. /usr/local/bin/droiddeck-fonts 2>&1 | sed 's/^/[fonts] /' - # Android's directory binding and language-specific family matches handle CJK and Thai fonts. device_fonts=/usr/local/share/fonts/android if [ -d "$device_fonts" ] && [ -d /etc/fonts/conf.d ] && command -v fc-cache >/dev/null 2>&1; then fonts_rules=$( @@ -239,14 +169,10 @@ if [ -z "${BL_INSIDE:-}" ]; then echo "== fontconfig: indexed $(ls "$device_fonts" | wc -l) device fonts, zh falls back to $(fc-match -f '%{family[0]}' ':lang=zh-cn' 2>/dev/null)" fi fi - # The compositor may still be starting, and a socket left by an earlier session is not - # a compositor: probe until a connection succeeds. for _ in $(seq 1 100); do wayland-info >/dev/null 2>&1 && break sleep 0.1 done - # The desktop (DroidDeck's hosted package) is its own nested compositor, KWin, drawing straight - # into the app's: no gamescope in between. if [ "$mode" = lxqt ]; then step "starting the desktop" start_agent --no-focus @@ -254,9 +180,6 @@ if [ -z "${BL_INSIDE:-}" ]; then cores=$(program_cores) if [ -n "$cores" ]; then echo "== desktop cores: $cores (the desktop and every program started from it)" - # Pin this shell and let exec carry the mask. Android keeps the app on a smaller cpuset - # until its activity is on top (foreground 0-5, background fewer), and a mask with no core - # of that set is EINVAL: taskset -c then exits 1 and takes the whole session with it. taskset -cp "$cores" $$ >/dev/null 2>&1 || echo "== desktop cores: not allowed right now, every core" fi exec /usr/local/bin/droiddeck-desktop @@ -264,9 +187,6 @@ if [ -z "${BL_INSIDE:-}" ]; then width=${BL_WIDTH:-1280} height=${BL_HEIGHT:-720} nested=$width - # Follow screen (BL_FOLLOW_OUTPUT=1): the app resizes gamescope's output when a foldable opens or - # closes, and with Steam on its own Xwayland gamescope resizes Steam's X screen with it, as on a - # Steam Deck docking. A stretched 16:9 nested size would pin it, so the two are exclusive. if [ "${BL_FOLLOW_OUTPUT:-0}" = 1 ]; then echo "== gamescope: the output follows the screen (${width}x${height} to start)" elif [ "$mode" = steam ] && [ "${BL_GAMESCOPE_STRETCH_16X9:-0}" = 1 ]; then @@ -278,51 +198,21 @@ if [ -z "${BL_INSIDE:-}" ]; then fi args=(--backend wayland --expose-wayland -f -W "$width" -H "$height" -w "$nested" -h "$height") [ "$nested" != "$width" ] && args+=(-S stretch) && export BL_STRETCHED=1 - # -r is what gamescope advertises as the refresh rate, and what a game reads as the display's. - # Left unset it defaults to 60, so a 120 Hz panel offers only 60 Hz in game settings. The FPS - # limiter wins when it is on (it is a deliberate cap); otherwise use the panel's own rate. if [ "${BL_FPS:-0}" -gt 0 ]; then args+=(-r "$BL_FPS") elif [ "${BL_REFRESH:-0}" -gt 0 ]; then args+=(-r "$BL_REFRESH") fi - # Steam integration advertises a Deck session and makes the client select Big Picture even - # without -gamepadui. The desktop UI uses ordinary gamescope window management instead. [ "$mode" = steam ] && [ "${BL_STEAM_UI:-}" != desktop ] && args+=(-e) - # Two Xwayland servers, as SteamOS runs Steam: the client on the first, games on the second - # (STEAM_GAME_DISPLAY_0). The client then sizes the games' server to each game's Game Resolution - # (GAMESCOPE_XWAYLAND_MODE_CONTROL; native unless one is set), and gamescope scales it to the - # output. With one server the client and its games share the output's size, so a game's own Game - # Resolution never reaches its display (docs/development/steam-display-scaling.md). The client's - # overlay is also on another server than the game, so opening it no longer takes the game's X - # keyboard focus, which made games like FlatOut shrink their window. - # BL_XWAYLAND_COUNT=1 in Download/droiddeck-env goes back to one. if [ "$mode" = steam ] && [ "${BL_XWAYLAND_COUNT:-2}" -gt 1 ]; then args+=(--xwayland-count "${BL_XWAYLAND_COUNT:-2}") export STEAM_MULTIPLE_XWAYLANDS=1 fi - # Touch passes through to the client as touch until the client says otherwise, as SteamOS's - # gamescope-session starts it (TOUCH_MODE=4). The client sets the mode itself (STEAM_TOUCH_CLICK_MODE) - # only once it has a controller; until then gamescope's own default made a finger a left-click - # mouse, so the Steam interface took taps but would not scroll on a phone with no pad attached. [ "$mode" = steam ] && args+=(--default-touch-mode 4) - # No --force-windows-fullscreen: it was for games that shrink their window when the Steam menu - # opens over them (FlatOut), which no longer happens with the games on their own Xwayland server - # (above), and it made any game drawing at another size than its display fight it. An experiment - # can still pass it with BL_GAMESCOPE_ARGS. - # HDR10: the app opened its compositor's colour-management gate for this session (BL_HDR=1); - # gamescope then offers HDR to its clients through the same wp_color_manager_v1 it binds on ours. [ "${BL_HDR:-0}" = 1 ] && args+=(--hdr-enabled) - # Extra gamescope arguments for an experiment (--debug-focus, say), from Download/droiddeck-env. [ -n "${BL_GAMESCOPE_ARGS:-}" ] && read -r -a _bl_extra <<<"$BL_GAMESCOPE_ARGS" && args+=("${_bl_extra[@]}") - # The performance overlay (Quick Access Menu → Performance → Overlay Level) is drawn by - # mangoapp, which gamescope starts beside the client; the client sets the level by rewriting - # MANGOHUD_CONFIGFILE. The apk stages Valve's aarch64 build (tools/mangoapp) - without it the menu - # still offers the levels and nothing appears. Starts hidden until the client writes a level. - # BL_MANGOAPP=0 turns it off (one community build found Valve's mangoapp crashing on Turnip). if [ "$mode" = steam ] && [ "${BL_MANGOAPP:-1}" = 1 ] \ && [ "${BL_STEAM_UI:-}" != desktop ] && command -v mangoapp >/dev/null 2>&1; then - # One fixed file, rewritten each session: a mktemp name per session was never removed. MANGOHUD_CONFIGFILE=/tmp/droiddeck-mangohud.conf echo no_display > "$MANGOHUD_CONFIGFILE" export MANGOHUD_CONFIGFILE STEAM_USE_MANGOAPP=1 STEAM_MANGOAPP_PRESETS_SUPPORTED=1 STEAM_MANGOAPP_HORIZONTAL_SUPPORTED=1 @@ -333,10 +223,6 @@ if [ -z "${BL_INSIDE:-}" ]; then exec gamescope "${args[@]}" -- "$0" "$mode" "$@" fi -# gamescope manages X windows: toolkits go through Xwayland, not gamescope's own Wayland socket. -# Root-window requests for the game (restore) go to every display gamescope gave us: -# the client's ($DISPLAY) and, with two servers, the games' (STEAM_GAME_DISPLAY_0); gamescope reads -# them per server. bl_displays() { printf '%s\n' "$DISPLAY" [ -n "${STEAM_GAME_DISPLAY_0:-}" ] && [ "$STEAM_GAME_DISPLAY_0" != "$DISPLAY" ] && printf '%s\n' "$STEAM_GAME_DISPLAY_0" @@ -350,10 +236,6 @@ bl_xprop_set_all() { [ "$ok" = 1 ] } export GDK_BACKEND=x11 QT_QPA_PLATFORM=xcb SDL_VIDEODRIVER=x11 -# gamescope's reaper waits for every process left under it before the compositor comes down. A -# process a game left behind - a Wine service blocked on a server that is already gone - is -# reparented there when the client exits, and the screen stays black on it for good. Nothing of -# the session outlives the program it was started for. reap_orphans() { local round p stat pid left for round in 1 2 3 4 5; do @@ -373,13 +255,10 @@ reap_orphans() { done } -# Gamescope's Wayland backend does not forward the outer clipboard into Xwayland. -# This helper owns only Android text selections and exits with this guest session. if [ "${BL_DESKTOP:-}" != 1 ] && [ -n "${BL_ANDROID_CLIPBOARD:-}" ] && [ -x /usr/local/bin/droiddeck-clipboard ]; then /usr/local/bin/droiddeck-clipboard "$BL_ANDROID_CLIPBOARD" & fi -# KDE already has a session agent; starting another would race its request files. [ "${BL_DESKTOP:-}" = 1 ] || start_agent case $mode in @@ -394,7 +273,6 @@ case $mode in echo "== program cores: $cores" taskset -cp "$cores" $$ >/dev/null 2>&1 || true fi - # Player 1 on the controller in an emulator that has no profile of its own yet. python3 /usr/local/bin/droiddeck-pad-defaults 2>&1 || true fex=() if [ -x /usr/local/bin/droiddeck-fex ]; then @@ -425,13 +303,6 @@ case $mode in droiddeck-steam-install || exit $? step "Steam client ready" steam_root=$XDG_DATA_HOME/Steam - # Register the card library (bound at /mnt/droiddeck-sd) with the client - internal storage is - # its main library already - and the ARM64 Proton that lets it launch the Windows ones. - # A request left in ~/.bl-proton-extra installs a third-party Proton before the client starts, - # so it is in the Compatibility dropdown the first time the user looks. One per line, each the - # arguments to droiddeck-proton-extra: "ge", "cachyos", "droiddeck", "ge --tag GE-Proton11-7", - # or a URL. Hundreds of megabytes each, so it is only ever done on request, and the line is - # dropped once it has succeeded - a failed download stays and is retried next session. if [ -s "$HOME/.bl-proton-extra" ]; then _bl_left=$HOME/.bl-proton-extra.left : > "$_bl_left" @@ -447,11 +318,6 @@ case $mode in done < "$HOME/.bl-proton-extra" mv -f "$_bl_left" "$HOME/.bl-proton-extra" fi - # steam:// URLs left in ~/.bl-steam-urls are handed to the client on its command line. The app - # writes one when the user asks for a Proton that is one of Valve's own depots rather than a - # tarball: those are not files anywhere, the client fetches them like any other app. One per - # line, and the file is cleared once they have been passed on - the client either acts on a URL - # or it does not, and repeating it every session would re-ask forever. if [ -s "$HOME/.bl-steam-urls" ]; then while read -r _bl_url || [ -n "$_bl_url" ]; do case ${_bl_url# } in steam://*) ;; *) continue ;; esac @@ -460,15 +326,6 @@ case $mode in done < "$HOME/.bl-steam-urls" rm -f "$HOME/.bl-steam-urls" fi - # Cores for the client itself. Steam pins its own interface renderer to a subset - on the - # device it took 5 of 8, leaving out both little cores and the fastest one - which is a - # reasonable thing to do while a game is running and the wrong thing when the client is all - # there is. BL_CLIENT_CPUS overrides it. Re-applied on a beat because steamwebhelper spawns - # children continuously and each one inherits Steam's choice, not ours. The beat reads /proc - # itself instead of running pgrep, and runs taskset for new threads or a single-core mask - # that has drifted since it was last pinned: every fork is a round of traced syscalls, and a - # beat that forked a taskset per process every 5 s did that for an hour. taskset -a, because - # without it only a process's main thread moves and the renderer's workers stay where they were. if [ -n "${BL_CLIENT_CPUS:-}" ]; then step "client cores: $BL_CLIENT_CPUS" ( @@ -489,9 +346,6 @@ case $mode in done ) & fi - # zRAM compression (BL_ZRAM): 20 s into a game the client and its web helpers page their memory - # out to the device's zram (libblsession zram.c), again every 5 minutes, and read it back once - # the game has exited. A game is running while Steam's reaper is. if [ "${BL_ZRAM:-0}" = 1 ]; then printf 0 > /tmp/.bl-zram step "zRAM compression: on" @@ -543,9 +397,6 @@ case $mode in else rm -f /tmp/.bl-zram fi - # Rockstar's launcher keeps drawing its hidden web interface for as long as a game of theirs - # runs; droiddeck-launcher-cores holds it on one slow core while it is out of sight, from - # gamescope's focus as the agent publishes it. BL_LAUNCHER_CORES=0 leaves launchers alone. if [ "${BL_LAUNCHER_CORES:-1}" != 0 ] && [ -n "${BL_LAUNCH_DIR:-}" ] && [ -z "${BL_DESKTOP:-}" ] \ && [ "${BL_STEAM_UI:-}" != desktop ] && [ -x /usr/local/bin/droiddeck-launcher-cores ]; then /usr/local/bin/droiddeck-launcher-cores "$BL_LAUNCH_DIR/agent/focus.json" 2>&1 & @@ -558,32 +409,18 @@ case $mode in if [ -z "${BL_DESKTOP:-}" ] && [ "${BL_STEAM_UI:-}" != desktop ]; then droiddeck-steam-ui-scale "$steam_root" "${BL_WIDTH:-1280}" "${BL_HEIGHT:-720}" || true fi - # The user's own games, as non-Steam shortcuts under that tool; the list is written by the app - # each session (empty = the app's earlier entries are removed, the client's own are kept). if [ -n "${BL_ADDED_GAMES:-}" ]; then step "adding the app's own games to the client's library" droiddeck-steam-shortcuts "$steam_root" "$BL_ADDED_GAMES" || true fi - # Steam runs a game's shared redistributables inside its prefix on first launch; marking - # them as already run keeps those x86 installers from starting under emulation at all. droiddeck-seed-redists "$steam_root" || true - # The compatibility tool runs Valve's arm64 Proton, which the client fetches like any depot. - # Asked for on the command line the first time, so a Windows title has it before it is needed. if [ ! -f "$steam_root/steamapps/appmanifest_4427310.acf" ]; then step "asking the client to fetch the ARM64 Proton" set -- "$@" steam://install/4427310 - # Ported from Bannerlator's session script (device-proven 2026-09-22). The client decides - # a title's compatibility tool when the title is installed and keeps that decision for the - # rest of its run, and the tool is set up before the client comes up. So once the depot has - # fully landed, restart the client once - its own -shutdown, then the loop below, which knows - # the marker - and every title installed from then on runs through the tool, with the - # refreshed tool list and defaults, and no second launch by the user. A first run only: with - # the depot present at session start nothing here is armed. ( _bl_acf="$steam_root/steamapps/appmanifest_4427310.acf" for _ in $(seq 1 360); do sleep 10 - # StateFlags 4 = fully installed; a moment more for the client to finish its bookkeeping. if grep -q '"StateFlags"[[:space:]]*"4"' "$_bl_acf" 2>/dev/null; then sleep 10 steam-compatibility "$steam_root" >/dev/null 2>&1 || true @@ -594,17 +431,6 @@ case $mode in fi done ) & - # A client that starts signed out drops that request - on a fresh install nobody has signed - # in yet - so the install dialog only appeared after the user restarted the client by hand. - # With no remembered sign-in, ask again once the sign-in has happened: the client binary - # started a second time with a URL hands it to the running instance and exits. - # - # The request is also dropped while the client is still in its post-logon compat phase (it - # probes every registered tool then): with publicbeta 1790380355 that phase took 27 s and a - # request sent 15 s after sign-in vanished (2026-09-26, no dialog; 1790121765 was done in - # time). So wait for the client to say the phase is over ("Waiting for compat in post-logon - # took" in its console log; at most 90 s if it never does), then ask - and ask again, twice - # at most, if no install has started a minute later. if ! grep -q '"RememberPassword"[[:space:]]*"1"' "$steam_root/config/loginusers.vdf" 2>/dev/null; then ( _bl_acf="$steam_root/steamapps/appmanifest_4427310.acf" @@ -633,26 +459,6 @@ case $mode in ) & fi fi - # The tool is registered before the client starts and finds the depot at launch time, so - # nothing here waits for a download. What can still change under a running client is which - # titles are installed: a title installed this session gets the client's own per-app Proton - # choice, which it cannot start. Refresh the per-app entries every so often; the registrar is - # idempotent, so a repeat costs nothing. - # The seeder has the same problem and a worse consequence. It marks the shared redistributables - # as already run in each prefix that exists, and a game's prefix is not created until the - # client launches it - so a title played for the first time this session was never covered, and - # its install scripts run for real. That is exactly the hang the seeder exists to prevent: Need - # for Speed Payback sat on "Running install script" with vcredist_x86.exe blocked in pipe_read, - # holding both ends of a pipe nobody would write to, while the client waited for it to exit. - # Seeding again on the same beat covers a prefix the moment it appears. - # The seeder runs on a shorter beat than the registrar because it is racing something. The - # client creates a prefix when it launches a title and runs that title's install scripts - # moments later, so the seeder has only that gap to mark the redistributables as already run. - # It stamps each prefix it has covered and skips it thereafter, so the extra passes cost - # almost nothing. - # With the 16:9 stretch on, the client's own scaler must not undo it: in Deck mode it sets - # GAMESCOPE_NEW_SCALING_SCALER to Auto (0), which fits the 16:9 picture with bars and outranks -S. - # Auto is put back to Stretch (4); a mode picked in the QAM (Integer, Fit, Fill) is kept. [ -z "${BL_DESKTOP:-}" ] && [ "${BL_STRETCHED:-0}" = 1 ] && ( while :; do nap 1 @@ -663,13 +469,6 @@ case $mode in && echo "== stretch 16:9 to panel: the client's scaler was Auto, set to Stretch" done ) & - # Resume from the Steam menu. The menu is an overlay that takes input, and a fullscreen game that - # loses input minimizes itself; gamescope only takes a window out of that state when the focused - # window changes, which the menu never does, so the game stayed minimized after Resume (a black - # screen with its small caption in the top-left corner). gamescope-3.16.29-p4 restores it itself; - # this asks it again a moment after input comes back from the client (app 769) to anything else, - # which covers a game that minimizes late. Harmless when nothing is minimized. - # BL_RESTORE_ON_RESUME=0 turns it off. [ -z "${BL_DESKTOP:-}" ] && [ "${BL_RESTORE_ON_RESUME:-1}" = 1 ] && ( prev= xprop -spy -root GAMESCOPE_FOCUSED_APP 2>/dev/null | while read -r line; do @@ -683,13 +482,7 @@ case $mode in prev=$app done ) & - # Evidence for touch reports: the touch mode the client asks gamescope for (STEAM_TOUCH_CLICK_MODE, - # set once it has a controller, per focused app) and which app has focus, logged as they change. - # A finger that clicks but won't scroll, or touch that stops once a pad is used, shows here as - # the mode in force at that moment. 4 is touch passed through, 1 a left-click mouse. [ -z "${BL_DESKTOP:-}" ] && ( - # What follows "=" in an xprop line, or $2 for a property that is missing or has no value - # yet ("NAME(CARDINAL) =", which read leaves with nothing after the "="). xprop_value() { case $1 in *'not found'*|*=) echo "$2"; return ;; esac local v=${1#*=} @@ -714,13 +507,6 @@ case $mode in esac done ) & - # The library beat. Both helpers are whole programs under proot (the seeder some forty of them, - # the compat one a python start and a parse of the client's config), and nearly every beat has - # nothing new for them: a burst of thousands of proot stops every quarter minute, game loads - # included. Each runs only when what it reads has changed, which bash answers without starting - # anything ([ -nt ], globs), and every five minutes regardless. The seeder acts on a prefix (or - # Proton's template for them) it has not marked, and on new install scripts, which arrive with a - # manifest; the compat pass on a new manifest, depot or tool, or the client rewriting config.vdf. ( beat=0 compat_stamp=$HOME/.bl-compat-beat @@ -762,8 +548,6 @@ case $mode in seed_due && seed=1 compat_due && compat=1 [ -n "$seed$compat" ] || continue - # Not beside a running game: the client rewrites its manifest at each launch, which made both - # due while it loaded. Its own launch seeds its prefix (droiddeck-game-env) regardless. pgrep -x wineserver >/dev/null 2>&1 && continue if [ -n "$seed" ]; then : > "$seed_stamp" @@ -775,12 +559,6 @@ case $mode in fi done ) & - # A game the app asked to launch on a cold start is not put on the client's command line: the - # client takes it before its interface is up, its launch stops at "waiting for user response to - # ShowInterstitials" with nobody to answer, and the game page reads Launching for ever (device, - # 2026-10-09, and 2026-10-08 before it). It is handed over once the interface has focus - # (gamescope's focused app 769) and has had a moment, through the same steam-game request an - # in-session launch uses; after two minutes it goes anyway. if [ -n "${BL_LAUNCH_DIR:-}" ] && [ -z "${BL_DESKTOP:-}" ]; then _bl_held= _bl_rest=() @@ -803,13 +581,8 @@ case $mode in ) & fi fi - # Debug aid while the runtime is being brought up: an app id dropped in ~/.bl-autolaunch is - # handed to the client as a rungameid URL, so a launch can be reproduced without touching the - # screen. Removed once the runtime is stable. if [ -s "$HOME/.bl-autolaunch" ]; then _bl_app=$(cat "$HOME/.bl-autolaunch") - # A bare app id runs the game; a full steam:// URL is passed through, which is how a - # validate or an install is driven without touching the screen. case $_bl_app in steam://*) _bl_url=$_bl_app ;; *) _bl_url=steam://rungameid/$_bl_app ;; @@ -817,13 +590,6 @@ case $mode in echo "== autolaunch $_bl_url" set -- "$@" "$_bl_url" fi - # The client builds Settings > Internet and its connection indicator on NetworkManager, which - # it looks for on the SYSTEM bus as it starts. This runtime has neither, so the page failed - # ("StartScanningForNetworks is not a function") under a header that showed no connection even - # while downloads ran. A system bus and a stand-in that reports Android's connection - and - # refuses what it cannot do, since the app has no say over Wi-Fi - answer what the client asks. - # The bus config is written here rather than shipped, so a runtime installed earlier gets this - # from the app's copy of the session scripts without a re-host. (WinNative, maxjivi05, e7a224ae.) if command -v dbus-daemon >/dev/null 2>&1 && command -v droiddeck-netmanager >/dev/null 2>&1; then mkdir -p /run/dbus /usr/local/share/droiddeck cat > /usr/local/share/droiddeck/system-bus.conf <<'BUSCONF' @@ -852,9 +618,6 @@ BUSCONF droiddeck-netmanager & netmanager=$! echo "== network: system bus up, NetworkManager stand-in running" - # Steam blanks its main UI before calling login1.Suspend. Without the - # matching resume notification it stays black although the menus work. - # Own the name before Steam constructs its login1 proxy at startup. rm -f "$BL_LAUNCH_DIR/steam-sleep-ready" droiddeck-login1 & login_manager=$! @@ -880,9 +643,6 @@ BUSCONF export STEAM_RUNTIME=1 export LD_LIBRARY_PATH=$steam_root/steamrtarm64:$steam_root/steamrtarm64/panorama${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} export SDL_VIDEO_X11_DGAMOUSE=0 - # Decky is optional and session scoped: the app's marker opts in, and this supervisor is - # reaped with the client session. Native ARM64 runs directly. A legacy x86_64 asset is used - # only when its recorded architecture says so and droiddeck-fex has FEX and its runtime ready. decky_loader=$HOME/homebrew/services/PluginLoader if [ -f "$HOME/.droiddeck-decky-enabled" ] && [ -x "$decky_loader" ]; then decky_arch=$(cat "$HOME/homebrew/services/.droiddeck-decky-arch" 2>/dev/null || echo unknown) @@ -929,47 +689,23 @@ BUSCONF export BL_CDP_GUARD=$BL_CDP_PORT echo "== steam: devtools port $BL_CDP_PORT, guest processes only" fi - # The agent bridge drives the client through it (droiddeck-agent's cdp requests). [ -n "${BL_LAUNCH_DIR:-}" ] && mkdir -p "$BL_LAUNCH_DIR/agent" && echo "$BL_CDP_PORT" > "$BL_LAUNCH_DIR/agent/cdp-port" droiddeck-steam-compat "$steam_root" 2>&1 & - # The client exits 42 when it has updated itself and wants a fresh start (steam.sh does this). - # BL_STEAM_UI=desktop = the client's desktop UI (a window, the library as a list), which the - # desktop's launchers ask for. KDE owns its windows when BL_DESKTOP=1. _bl_ui=(-gamepadui) [ "${BL_STEAM_UI:-}" = desktop ] && _bl_ui=() _bl_cef=(-cef-force-gpu -cef-use-gl=angle -cef-use-angle=vulkan) if [ "${BL_DESKTOP:-}" = 1 ] && [ "${BL_STEAM_UI:-}" = desktop ]; then - # KDE uses QPainter/shm on Android. CEF's Vulkan buffers cannot be - # presented there; its software path keeps the library and dialogs on KDE. _bl_cef=(-cef-disable-gpu) _bl_ui=(-nobigpicture) fi - # Deck mode, always outside the desktop UI: -steamdeck is the flag that puts - # the Quick Access Menu and the Deck settings pages on screen; the QAM's performance overlay - # controls stay greyed out without -steamos3, as SteamOS and Armada launch it. -steamos3 makes - # the client manage itself as SteamOS does - it calls steamos-select-branch and writes - # steamdeck_stable to package/beta, and a branch on disk that loses to the -clientbeta below - # is an update that never installs: exit 42, a restart, for ever (Bannerlator 38996ea1). The - # loop below puts package/beta back before every start. The SteamOS helpers the client calls - # in this mode are staged as no-op stubs under /usr/bin. if [ "${BL_STEAM_UI:-}" != desktop ]; then _bl_ui+=(-steamdeck -steamos3) - # The QAM's Scaling Filter lists NVIDIA Image Scaling only when told gamescope has it (it does). export STEAM_GAMESCOPE_NIS_SUPPORTED=1 - # And shows its Scaling Mode and Scaling Filter only when told gamescope takes them. export STEAM_GAMESCOPE_FANCY_SCALING_SUPPORT=1 echo "== steam: Deck mode on (-steamdeck -steamos3)" fi - # The power menu's Shut Down and Restart. Without these the client blanks its interface and - # stays up (device, 2026-10-12: System.ShutdownPC and nothing after it). With them it does what - # SteamOS's gamescope session expects: writes the sentinel and exits. The loop below ends the - # session for one and starts the client again for the other. export STEAMOS_STEAM_SHUTDOWN_SENTINEL="$HOME/.bl-steam-shutdown" export STEAMOS_STEAM_REBOOT_SENTINEL="$HOME/.bl-steam-reboot" - # What a killed session leaves behind. Android ends sessions without warning (backgrounding, the - # low-memory killer), and the client then finds .crash - opening its update UI on the spinner - - # and the web helper's Singleton* locks in htmlcache, owned by a process that is gone, which can - # leave the Gamepad UI blank. No client or helper is running at this point in a session. if ! pgrep -f 'steamwebhelper|steamrtarm64/steam' >/dev/null 2>&1; then _stale="" for _f in "$steam_root/.crash" "$steam_root/steam.pid" \ @@ -980,18 +716,7 @@ BUSCONF [ -n "$_stale" ] && echo "== steam: cleared what an earlier session left:$_stale" unset _stale _f fi - # The Deck's beta branch, the one the first-run download takes: with -steamos3 on publicbeta - # the client settled on steamdeck_stable, an older client it then offered as a "Software - # Update" every session (2026-09-30); on steamdeck_publicbeta it finds no update. Whatever - # wrote package/beta, the branch on disk has to agree with the one forced here - a - # disagreement is the restart loop above, and it is silent apart from rc=42 in this log. A - # client installed from publicbeta by an older app moves over this way at its next start. - # Checked before every start, not once: the client rewrites package/beta during its own run, - # and a restart below (rc=42, or the one after the ARM64 Proton lands) would otherwise start it - # again on the branch it just wrote. steam_channel=steamdeck_publicbeta - # The app's language (Setup, or the system's): the client's interface takes -language over its - # registry, and the registry is rewritten before each start below for its update window. _bl_lang=() [ -n "${BL_STEAM_LANGUAGE:-}" ] && _bl_lang=(-language "$BL_STEAM_LANGUAGE") steam_beta=$steam_root/package/beta @@ -999,16 +724,12 @@ BUSCONF droiddeck-steam-games watch "$steam_root" "$BL_ADDED_GAMES" & games_watcher=$! fi - # Lossless Scaling (Steam app 993090): whether this account owns it, asked of the signed-in - # client once a session until the answer is yes. From then on the app offers importing - # Lossless.dll and passes BL_LOSSLESS_OWNED=1, so the question is never asked again. if [ "${BL_LOSSLESS_OWNED:-0}" != 1 ]; then ( owned=$HOME/.config/droiddeck/lossless-owned for ((_i = 0; _i < 60; _i++)); do nap 30 [ -e "$owned" ] && exit 0 - # Loading steamclient.so under proot takes seconds of CPU: not while a game loads or runs. pgrep -x wineserver >/dev/null 2>&1 && continue answer=$(droiddeck-steam-games probe "$steam_root" 993090 2>/dev/null) || continue case $answer in @@ -1020,15 +741,8 @@ BUSCONF ) & fi echo "== steam args: ${_bl_ui[*]} ${_bl_cdp[*]} ${_bl_lang[*]} -clientbeta $steam_channel -no-cef-sandbox -cef-ozone-platform=x11 ${_bl_cef[*]} $*" - # An update that cannot install asks for a restart (rc=42) every time; the count is capped so a - # session can never again loop in silence, which reads as a menu opening and closing by itself. steam_updates=0 rm -f "$HOME/.bl-stop-requested" "$STEAMOS_STEAM_SHUTDOWN_SENTINEL" "$STEAMOS_STEAM_REBOOT_SENTINEL" - # The client brings a game it launched to the front only while it counts someone as present, - # which it reads from gamescope's input counter. A game the app launches after a few minutes - # with no touch starts behind the library page and stays there until the next input (device, - # 2026-10-09). The game's window turning up behind the interface is counted as one input, as - # gamescope counts a key. steam_bring_forward() { local end=$((SECONDS + 600)) behind=0 line name value focused other n while [ "$SECONDS" -lt "$end" ]; do @@ -1099,7 +813,6 @@ BUSCONF echo start >> "$steam_root/package/droiddeck-unconfirmed" _bl_counted=1 fi - # Between starts only: the client holds registry.vdf in memory and writes it back on exit. if [ -n "${BL_STEAM_LANGUAGE:-}" ]; then droiddeck-steam-language "$HOME" "$BL_STEAM_LANGUAGE" || true fi @@ -1140,8 +853,6 @@ BUSCONF set -- continue fi - # The session asked for this exit (the ARM64 Proton landed): start again, without the URL - # arguments, which were acted on. Not counted against the update cap. if [ -f "$HOME/.bl-restart-once" ]; then echo "== steam: restarting once so the new compatibility layer is in the client's lists" set -- @@ -1153,7 +864,6 @@ BUSCONF echo "== steam: asked to restart for an update $steam_updates times and it is not installing; the session stops here rather than loop" break fi - # The URL arguments were acted on the first time; a restart must not repeat them. set -- done if [ -n "${stop_watcher:-}" ]; then @@ -1165,10 +875,6 @@ BUSCONF wait "$games_watcher" 2>/dev/null || true fi if [ -n "${BL_DESKTOP:-}" ]; then - # Started from the desktop (droiddeck-steam-launch): the desktop's own programs share this - # script's parent, so reap_orphans - right under gamescope, where everything is the client's - - # is not for here. The helpers this run started (the cores and redistributables beats, the - # system bus and the NetworkManager stand-in) go with the client instead. if [ -n "${decky_supervisor:-}" ]; then kill "$decky_supervisor" 2>/dev/null || true wait "$decky_supervisor" 2>/dev/null || true diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat index 31c06358d..24b2ad88b 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat @@ -126,10 +126,6 @@ INSTALL_JS = r""" return "installed"; })() """.replace("__BINDING__", BINDING) -# Big Picture's "Switch to Desktop" asks the client's SteamOS manager, which the client has only when it -# runs as SteamOS (-steamos3, Deck mode); otherwise the request fails and the screen stays on "Switching -# to Desktop..." for good. The request is caught on its way to the client and answered as SteamOS's -# own path does, with steamos-session-select. SWITCH_BINDING = "droiddeckSwitchToDesktop" SWITCH_JS = r""" (() => { @@ -526,7 +522,6 @@ def main(): os.makedirs(COMPAT["COMPAT_DIR"], exist_ok=True) watcher = Watcher([COMPAT["state_path"]("request.json"), COMPAT["state_path"]("games.json")] + [os.path.join(lib, "steamapps", "libraryfolders.vdf") for lib in (steam_root, COMPAT["LIBRARY"] + "-sd")]) - # The client has its own SteamOS manager when started as SteamOS (droiddeck-session's Deck mode). steamos = os.environ.get("BL_STEAMDECK") == "1" and os.environ.get("BL_STEAM_UI") != "desktop" helper = Helper(steam_root, port, steamos) failures = 0 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-desktop-ui b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-desktop-ui index 6eda34131..68b0a2a3a 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-desktop-ui +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-desktop-ui @@ -16,7 +16,6 @@ def select_desktop(evaluate, timeout=90): if result.get('value') is True: return True except Exception: - # CEF and SharedJSContext are not ready during the client's initial startup/update. pass time.sleep(1) return False diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games index 5fe62eb1e..85ecce1f2 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-games @@ -113,7 +113,6 @@ def method(obj, slot, result, *args): def probe(steam_root, ids): - # Slots belong to Valve's SteamClient020, SteamUser021 and STEAMAPPS_INTERFACE_VERSION008. library = C.CDLL(str(Path(steam_root) / "steamrtarm64/steamclient.so")) library.CreateInterface.argtypes = [C.c_char_p, C.c_void_p] library.CreateInterface.restype = C.c_void_p @@ -185,12 +184,6 @@ def watch(steam_root, listing): candidates, sources = {}, {} next_lookup = 0 last_status = None - # The probe is a fresh python that loads steamclient.so, seconds of CPU under proot. Ownership - # only moves with a purchase or a new match, so it runs when the matches or their sources - # change, after a failed probe, and otherwise every five minutes - not on every 30 s beat. - # A failed probe is retried on a doubling delay up to those five minutes rather than on every - # beat (a client that is not logged on fails it for as long as it stays that way), and none - # starts while a game is running: the answer can wait, the game's cores cannot. probed, next_probe, failures = None, 0, 0 pending, requested = [], [] while True: diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-install b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-install index 0eae6d0d2..1ae379d94 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-install +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-install @@ -1,10 +1,6 @@ #!/bin/bash -# Fetches the native arm64 Steam client from Valve's client-update CDN into ~/.local/share/Steam. -# The client is not redistributable, so the runtime ships this script instead of the client. set -eu cdn=https://client-update.fastly.steamstatic.com -# The Deck's beta branch, which droiddeck-session forces on the command line too. A client already -# on disk from another branch is left for the client to move (droiddeck-session's package/beta). steam_channel=steamdeck_publicbeta manifest=steam_client_${steam_channel}_linuxarm64 steam_root=${XDG_DATA_HOME:-$HOME/.local/share}/Steam @@ -43,7 +39,6 @@ work=$(mktemp -d) trap 'rm -rf "$work"' EXIT curl -fsSL "$cdn/$manifest" -o "$work/manifest" version=$(awk -F'"' '$2 == "version" { print $4; exit }' "$work/manifest") -# Every component block: its name, then the plain zip's file name and sha256. awk -F'"' ' /^\t"/ && NF >= 3 && $4 == "" { block = $2 } $2 == "file" { file[block] = $4 } @@ -62,7 +57,6 @@ while read -r block file sum; do unzip -qo "$work/$file" -d "$steam_root" rm -f "$work/$file" done < "$work/parts" -# Some entries are zipped with Windows separators; unzip keeps the backslash in the file name. find "$steam_root" -depth -name '*\\*' | while read -r odd; do fixed=${odd//\\//} mkdir -p "$(dirname "$fixed")" @@ -75,17 +69,11 @@ done echo "$steam_channel" > "$steam_root/package/beta" ln -sfn "$steam_root/steamrtarm64" "$steam_root/steamrtarm32" chmod +x "$steam_root/steamrtarm64/steam" 2>/dev/null || true -# What Valve's steam.sh sets up before it runs the client (it has no arm64 branch itself). mkdir -p "$HOME/.steam" ln -sfn "$steam_root" "$HOME/.steam/root" ln -sfn "$steam_root" "$HOME/.steam/steam" ln -sfn "$steam_root/steamrtarm64" "$HOME/.steam/bin64" ln -sfn "$steam_root/linuxarm64" "$HOME/.steam/sdk64" -# The arm64 client runs every game through $HOME/.steam/sdkarm64/steam-launch-wrapper. Valve's -# steam.sh has no arm64 branch to create it, and without it a launch dies in /bin/sh before Proton -# is ever invoked - "No such file or directory", no Proton log, and the only other clue is an -# unrelated-looking installscript assertion. Same target as sdk64; the client picks the name by -# architecture. ln -sfn "$steam_root/linuxarm64" "$HOME/.steam/sdkarm64" ln -sfn "$steam_root/steamrtarm64" "$HOME/.steam/binarm64" ln -sfn "$steam_root/linux32" "$HOME/.steam/sdk32" diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-language b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-language index bde57b06f..f892e8e22 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-language +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-language @@ -122,7 +122,6 @@ def set_value(tokens, opening, key, value): def with_language(text, language): tokens = tokenize(text) if child(tokens, 0, len(tokens), "Registry") is None: - # Not a registry the client wrote (or none at all): start one it will read. tokens = [] for path in KEYS: set_value(tokens, ensure_block(tokens, path), "language", language) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-launch b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-launch index b24dffe05..71da571dd 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-launch +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-launch @@ -1,11 +1,4 @@ #!/bin/bash -# Starts the Steam client from inside a session (the desktop's menu entries and the per-game -# launchers droiddeck-desktop-games writes): -# droiddeck-steam-launch desktop [appid|steam-url] Steam's desktop UI, optionally a game -# droiddeck-steam-launch bigpicture [appid|steam-url] Big Picture -# -# The desktop client stays on KDE's Xwayland. Big Picture is the explicit handoff -# to the app's gaming session. set -u ui=${1:-desktop} target=${2:-} @@ -21,13 +14,9 @@ esac if [ "$ui" = desktop ] && [ "${BL_DESKTOP:-}" = 1 ]; then steam_root=${XDG_DATA_HOME:-${HOME:-/root}/.local/share}/Steam export LD_LIBRARY_PATH=$steam_root/steamrtarm64:$steam_root/steamrtarm64/panorama${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} - # A second menu click forwards its URL to Steam's existing instance. Keep only - # one session supervisor, including while the client restarts for an update. mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}/droiddeck" exec 9>"${XDG_CACHE_HOME:-$HOME/.cache}/droiddeck/steam-desktop.lock" if ! flock -n 9; then - # The supervisor may still be preparing Proton or updating Steam. Invoking - # the binary before its IPC pipe exists would start a competing client. for ((attempt = 0; attempt < 180; attempt++)); do pid=$(cat "$HOME/.steam/steam.pid" 2>/dev/null || true) if [ -p "$HOME/.steam/steam.pipe" ] && [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null; then @@ -43,12 +32,10 @@ if [ "$ui" = desktop ] && [ "${BL_DESKTOP:-}" = 1 ]; then exec /usr/local/bin/droiddeck-session steam "${url:-steam://open/library}" fi -# Ask the app for a Steam session. dir=${BL_LAUNCH_DIR:-} if [ -z "$dir" ] || [ ! -d "$dir" ]; then echo "droiddeck-steam-launch: not inside a DroidDeck session (BL_LAUNCH_DIR unset)" >&2 exit 69 fi -# Written whole, then renamed: the app reads it on close, never half-written. printf 'ui=%s\nurl=%s\n' "$ui" "$url" > "$dir/steam-launch.tmp" && mv -f "$dir/steam-launch.tmp" "$dir/steam-launch" echo "droiddeck-steam-launch: asked the app for Steam ($ui${url:+, $url})" diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-library b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-library index 0a961c87a..171400b31 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-library +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-library @@ -1,23 +1,10 @@ #!/usr/bin/python3 -# Registers the card the app installs to (bound at /mnt/droiddeck-sd) as a library folder of the -# native Steam client, so they show as installed there instead of being downloaded a second time. -# -# droiddeck-steam-library <steam root> -# -# Ported from WinNative's gamescope runtime (GPL-3.0). import json import os import re import sys -# The client's own library is internal storage (its steamapps/common is bound to the app's root); -# the card is the one extra library folder. -# The app binds the storage the user chose (Steam cog -> Game storage) at this path and names it -# in BL_LIBRARY_LABEL; nothing bound = internal only, and the entry is removed so the client does -# not keep offering a place that is not there. LIBRARIES = (("/mnt/droiddeck-sd", os.environ.get("BL_LIBRARY_LABEL", "SD Card")),) -# The client calls its own library "Local Drive (/)", which says nothing about where that is. -# It is the phone's internal storage, so say so. MAIN_LABEL = "Internal Storage" @@ -153,10 +140,6 @@ def relabel(steam_root, target, label): with open(path, "w", encoding="utf-8") as f: f.write("\n".join(out)) print("droiddeck-steam-library: labelled %s %r" % (target, label)) - # A library also carries its own libraryfolder.vdf, and the client takes the label from there - # back into the list above - so editing the list alone is undone on the next start. The - # client's own library has no such file, which is why that one kept the new name and the card - # went back to the name it was first registered with. marker = os.path.join(target, "libraryfolder.vdf") if not os.path.isfile(marker): return diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shim b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shim index 9f97bb791..3ecbfceae 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shim +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shim @@ -1,8 +1,4 @@ #!/bin/bash -# `steam` on the desktop. The client writes a menu entry per installed game that runs -# `steam steam://rungameid/<id>`, and its own launchers say `steam`. On the desktop that goes -# through droiddeck-steam-launch, which keeps the desktop client on KDE and -# hands URLs to a client already running there. Anywhere else it is the client itself. set -u if [ "${BL_DESKTOP:-}" = 1 ]; then url= diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shortcuts b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shortcuts index 3fa78c900..d8e24069e 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shortcuts +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-shortcuts @@ -29,7 +29,6 @@ def account_dir(steam_root): pass userdata = os.path.join(steam_root, "userdata") if recent is not None: - # loginusers holds the 64-bit id; userdata folders use the 32-bit account id. acct = recent - 76561197960265728 return os.path.join(userdata, str(acct)) try: @@ -39,7 +38,6 @@ def account_dir(steam_root): return os.path.join(userdata, dirs[0]) if len(dirs) == 1 else None -# ---- binary VDF: 0x00 map, 0x01 string, 0x02 int32, 0x08 end ---- def parse(data): pos = [0] @@ -123,8 +121,6 @@ def shortcut(game, launch_options=None): RECORD = ".droiddeck-shortcuts.json" AWAY_LIMIT = 256 FIELDS = ("AppName", "Exe", "StartDir", "icon", "LaunchOptions") -# What the app may change after the shortcut exists. Each follows the app only while Steam still -# has what the app last asked for; a value the user set in Steam's Properties stays. FOLLOWED = ("StartDir", "LaunchOptions") @@ -164,9 +160,6 @@ def merge(fresh, current, state, seen): for field in ("AppName", "icon"): if current.get(field) == base.get(field): entry[field] = fresh[field] - # Start in and launch options the app sets: only once there is a record of what it asked for - # before (a first run keeps whatever the shortcut already had), and an older record that did - # not keep launch options means the app asked for none. if isinstance(state.get("app"), dict): for field in FOLLOWED: was = base.get(field, "" if field == "LaunchOptions" else current.get(field)) @@ -240,8 +233,6 @@ def save_record(path, record): ART_MANIFEST = ".droiddeck-art.json" -# listing key -> the client's grid file suffix: <appid>p = portrait capsule, <appid> = header, -# <appid>_hero, <appid>_logo. ART_SLOTS = (("p", "p"), ("header", ""), ("hero", "_hero"), ("logo", "_logo")) @@ -273,7 +264,7 @@ def install_art(acct, games): dst = os.path.join(grid, name) size = os.path.getsize(src) if os.path.exists(dst) and name not in manifest: - continue # the user's own choice inside the client; never ours to touch + continue wanted[name] = size if os.path.exists(dst) and manifest.get(name) == size: continue @@ -344,9 +335,7 @@ def main(): save_record(record_path, record) print("droiddeck-steam-shortcuts: %d added game(s) written, %d of the client's own kept" % (len(ours), len(kept))) install_art(acct, [g for g in games if g.get("exe") and g.get("name")]) - # Each added game runs under the ARM64 Proton: the compat registrar's own mapping writer. try: - # No .py suffix on the registrar, so the loader has to be named. compat_path = "/usr/local/bin/steam-compatibility" spec = importlib.util.spec_from_file_location("compat", compat_path, loader=importlib.machinery.SourceFileLoader("compat", compat_path)) compat = importlib.util.module_from_spec(spec) @@ -356,7 +345,7 @@ def main(): compat.register_default(cfg, apps=[str(int(g["appid"]) & 0xFFFFFFFF) for g in games if g.get("exe")] + [str(appid) for appid in set(routes.values())], protect=compat.adopt_extras(os.path.join(steam_root, "compatibilitytools.d"))) print("droiddeck-steam-shortcuts: mapped them to %s" % compat.TOOL) - except Exception as e: # noqa: BLE001 - the games still show; the client falls back to the default tool + except Exception as e: print("droiddeck-steam-shortcuts: compat mapping skipped (%s)" % e) return 0 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-ui-scale b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-ui-scale index b3e009e60..4a5997684 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-ui-scale +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-ui-scale @@ -21,7 +21,6 @@ import tempfile DISPLAY = "External: gamescope|||Windowed" NATIVE_RESOLUTION = "EnableSteamNativeExternalResolution" -# screensaver_while_ingame: kept in config.vdf like the Gamescope settings, under this path. SCREENSAVER_IN_GAME = ("Customization", "Screensaver", "WhileInGame") TOKEN = re.compile(r'\s+|//[^\n]*|"(?:\\.|[^"\\])*"|[{}]') @@ -79,8 +78,6 @@ def parse(text): def initial_scale(width, height): - # The Thor setting chosen on device at 1280x720 was 140% of its automatic scale. - # Adjust with canvas area so choosing a different session resolution keeps the UI usable. if not 320 <= width <= 16384 or not 320 <= height <= 16384: raise ValueError("invalid session size") return 1.3281567096710205 * math.sqrt(width * height / (1280 * 720)) @@ -97,8 +94,6 @@ def seed_text(text, scale): if display is not None and not isinstance(display, Block): raise ValueError("invalid display settings") if display is not None: - # Current describes the active display and is written even with automatic scaling. - # Named display entries are the user's saved choices, including a return to Auto. if any(name.lower() != 'current' and isinstance(value, Block) and value.get('ScaleFactor') is not None for name, value in display.pairs): return text, False @@ -111,7 +106,6 @@ def seed_text(text, scale): else: entry = '"UI"\n{\n"display"\n{\n' + entry + '}\n}\n' at, indent = root.end, 1 - # Insert only the new block; leave every existing byte, comment and credential untouched. insertion = '\n' + '\n'.join('\t' * indent + line for line in entry.splitlines()) + '\n' return text[:at] + insertion + text[at:], True @@ -138,7 +132,6 @@ def seed_value(text, path, value): for level, section in reversed(list(enumerate(sections))): lines = [(level, f'"{section}"'), (level, '{')] + lines + [(level, '}')] insertion = ''.join('\t' * (depth + level) + line + '\n' for level, line in lines) - # Before the line the block closes on, when it has one of its own; else beside the brace. at = text.rfind('\n', 0, block.end) + 1 if text[at:block.end].strip(): at, insertion = block.end, '\n' + insertion diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr index 162e471da..722769699 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steamvr @@ -1,14 +1,4 @@ #!/bin/sh -# SteamVR for Cardboard mode (docs/cardboard-vr.md). Experimental. -# -# droiddeck-steamvr status one line: installed=0|1 running=0|1 installing=0|1 driver=0|1 -# droiddeck-steamvr install fetch Valve's ARM64 SteamVR (the Steam Frame's package) into /opt/steamvr -# droiddeck-steamvr start start SteamVR with the DroidDeck Cardboard driver -# droiddeck-steamvr stop stop it -# -# SteamVR renders through the driver (tools/vr/src/driver_droiddeck.cpp) into an overlay window that -# gamescope shows over everything; the app's Cardboard pass, with a side-by-side source, puts the lens -# on it. vrcompositor needs the Vulkan layer (tools/vr/src/vk_layer_droiddeck_vr.c) on Turnip/KGSL. set -u steamvr=${STEAMVR:-/opt/steamvr} @@ -19,7 +9,6 @@ logs=$HOME/.local/share/Steam/logs state=${XDG_RUNTIME_DIR:-/tmp}/droiddeck-steamvr mkdir -p "$state" -# The Steam Frame's SteamVR, from Valve's package server; the same build the Frame's SteamOS ships. PKG_URL=https://steamdeck-packages.steamos.cloud/archlinux-deckard-hotfixes/release/0.3.x/deckard-steamvr-rel-r25358740%2B28b72a4f-1-aarch64.pkg.tar.zst PKG_SHA256=90a600775eb3e8856dc475f83166a93a290a0b1f3b4e82a69c8e2a8f06b9ddc9 @@ -31,9 +20,7 @@ stop() { for n in vrcompositor vrserver vrmonitor vrdashboard vrstartup vrwebhelper; do for p in $(pgrep -x "$n"); do kill "$p" 2>/dev/null; done done - # pgrep -x matches 15 characters of the name at most. for p in $(pgrep -f "$steamvr/bin/linuxarm64/vrcompositor-launcher"); do kill "$p" 2>/dev/null; done - # A process that exited on its own meanwhile is not a failure: the app reads this exit code. return 0 } @@ -41,9 +28,6 @@ case "${1:-status}" in status) i=0; installed && i=1 r=0; running && r=1 - # A session that died with SteamVR up leaves the marker behind; it would keep the layer active. - # Only an old one: vrserver takes a while to appear after start, and Steam's web helper needs the - # marker from the start. if [ "$r" = 0 ] && [ -n "$(find "$state/active" -mmin +2 2>/dev/null)" ]; then rm -f "$state/active"; fi n=0; [ -f "$state/installing" ] && n=1 d=0; [ -f "$driver/bin/linuxarm64/driver_droiddeck.so" ] && [ -f "$layer/libVkLayer_droiddeck_vr.so" ] && d=1 @@ -88,13 +72,10 @@ start) JSON ln -sf "$steamvr/steamxr_linuxarm64.json" "$HOME/.config/openxr/1/active_runtime.json" - # vrcompositor asks for VK_KHR_display, which makes Turnip/KGSL report no GPU at all; the layer stands - # in for a display. gamescope's implicit WSI layer stays out of SteamVR's processes. export VK_LAYER_PATH="$layer${VK_LAYER_PATH:+:$VK_LAYER_PATH}" export VK_INSTANCE_LAYERS="VK_LAYER_DROIDDECK_vr${VK_INSTANCE_LAYERS:+:$VK_INSTANCE_LAYERS}" export VK_LOADER_LAYERS_DISABLE="${VK_LOADER_LAYERS_DISABLE:-~implicit~}" - # SteamVR's web helper (the dashboard) checks the X server with xset, which the runtime lacks. shims=$state/shims mkdir -p "$shims" if ! command -v xset >/dev/null 2>&1; then @@ -103,8 +84,6 @@ JSON export PATH="$shims:$PATH" fi - # While this file exists, the layer is active in every Vulkan instance of the session, not only - # SteamVR's: Steam's web helper shares its in-VR overlay with SteamVR and needs it too. : > "$state/active" cd "$steamvr/bin/linuxarm64" || exit 1 setsid ./vrstartup > "$state/vrstartup.log" 2>&1 < /dev/null & diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch b/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch index 710c58dd6..ba1415112 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch @@ -116,7 +116,6 @@ def ask(channel, request, timeout=TIMEOUT): return value except (OSError, ValueError): time.sleep(0.05) - # Not answered: the request is withdrawn so a late app does not mint a code nobody reads. try: (req_dir / (rid + ".json")).unlink() except OSError: @@ -229,10 +228,7 @@ def provision_prefix(compat_data, wait=SERVER_WAIT): reg = pfx / "user.reg" if not reg.is_file(): return "prefix-not-created" - # A wineserver still up for this prefix would write its own copy of user.reg over these lines. busy = not wait_for_server(pfx, wait) - # winebrowser starts the first entry it can; an absolute path does not depend on the PATH Wine - # hands it. set_value(reg, BROWSER_KEY, "Browsers", BROWSER + ",xdg-open") removed = remove_value(reg, EOS_KEY, "OverlayPath") state = "browser-set" if has_value(reg, BROWSER_KEY, "Browsers", BROWSER + ",xdg-open") else "browser-not-written" @@ -243,8 +239,6 @@ def provision_prefix(compat_data, wait=SERVER_WAIT): CLOUD_TIMEOUT = 15.0 CLOUD_STORES = ("gog", "epic") -# Exit status that tells droiddeck-proton this was a store game with cloud saves: it then waits for -# Proton instead of exec'ing it, and runs `droiddeck-store-launch --exited` after. STORE_GAME = 10 @@ -283,8 +277,6 @@ def provision_galaxy(compat_data, proton, timeout=90): stamp = version + " " + " ".join(hashlib.sha256(p.read_bytes()).hexdigest() for p in tools) if marker.is_file() and marker.read_text() == stamp and (data / "pfx/system.reg").is_file(): return "ready" - # cmd/sc run through this title's selected Proton. The run verb bypasses the managed - # wrapper, so this cannot recursively request credentials or sync cloud saves. with tempfile.TemporaryDirectory(prefix="droiddeck-galaxy-") as tmp: folder = Path(tmp) result = folder / "result" @@ -352,7 +344,6 @@ def main(argv): drop_stale(folder) choice = options(sidecar) if not (choice["eos"] and not choice["offline"]): - # Sign-in off, or offline: no code, and none left over from before. try: (folder / EPIC_CODE).unlink() except OSError: @@ -373,7 +364,6 @@ def main(argv): log("cloud %s %s down result=skipped reason=off" % (store, game)) return 0 if launch_dir: - # The cloud copy where it is newer, before the game reads its saves; bounded, then on regardless. answer = ask(Path(launch_dir) / "stores", {"op": "cloud-down", "store": store, "id": game}, timeout=CLOUD_TIMEOUT) if answer is None: log("cloud %s %s down result=timeout" % (store, game)) @@ -387,6 +377,6 @@ def main(argv): if __name__ == "__main__": try: sys.exit(main(sys.argv[1:])) - except Exception as e: # never in the way of the launch + except Exception as e: log("skipped: %s" % type(e).__name__) sys.exit(0) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-wincomponents b/tools/linuxfs/overlay/usr/local/bin/droiddeck-wincomponents index 533f6674c..7349e6784 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-wincomponents +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-wincomponents @@ -32,20 +32,13 @@ from pathlib import Path STORE = Path("/opt/droiddeck/wincomponents") SELECTION = Path.home() / ".config/droiddeck/wincomponents.json" -# Components the app is still downloading for a game Steam installed during this session: -# {"games": {"<appid>": ["vcredist2019_dll", ...]}}. A launch waits a little for them. PENDING = Path.home() / ".config/droiddeck/wincomponents-pending.json" PENDING_WAIT = 10.0 -# What this prefix holds from us, so a component the user turns off is taken out again. STATE = ".droiddeck-wincomponents.json" -# A file ours replaced that was not Proton's (a real installer's, the user's own), put back when the -# component is turned off. Proton's builtins need no copy: it restores those itself. BACKUP = ".droiddeck-wincomponents-backup" BUILTIN_MARKS = (b"Wine builtin DLL", b"Wine placeholder DLL") ARCH_DIRS = ("system32", "syswow64") ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}") -# The registry file of each hive in a prefix, and the kinds of value registry.json holds: "key" -# makes a key, "append"/"prepend" add to a list such as PATH unless it is already there. HIVES = {"HKLM": "system.reg", "HKCU": "user.reg"} VALUE_TYPES = {"key", "sz", "expand_sz", "multi_sz", "dword", "binary", "append", "prepend"} @@ -80,7 +73,6 @@ def component(store, cid): for arch in ARCH_DIRS: folder = root / arch if folder.is_dir(): - # A few go one level down (gmdls: system32/drivers/gm.dls). for entry in sorted(folder.rglob("*")): if entry.is_file() and not entry.is_symlink(): rel = entry.relative_to(folder).as_posix().lower() @@ -195,12 +187,10 @@ def place(src, target, backup=None): final = found or target temp = final.with_name(".%s.droiddeck" % final.name) shutil.copy2(src, temp) - # Over a Proton builtin this swaps its symlink for our file. os.replace(temp, final) return True -# ---------------------------------------------------------------- the prefix's registry files def reg_escape(text, extra=""): """A string as Wine's registry files write it.""" @@ -556,7 +546,6 @@ def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING """Brings the prefix in line with the game's picks. Returns (overrides, notes).""" compat = Path(compat_path) pfx = compat / "pfx" - # A game Steam installed during this session may have components still on their way. waited = wait(app_id, pending, store) if wait else None ids = wanted(app_id, selection) state = load_state(compat) @@ -576,7 +565,6 @@ def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING overrides += names values += [(cid, value) for value in registry] for rel, src in files: - # A file this prefix already has from us is ours to replace; anything else is kept. if place(src, pfx / rel, None if rel in state["files"] else compat / BACKUP / rel): copied += 1 keep[rel] = cid @@ -594,8 +582,6 @@ def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING path.unlink() removed += 1 if removed: - # Proton refreshes its builtins only when its config changes; forgetting the config puts - # its own DLLs back where ours were on the next launch - this one. try: (compat / "config_info").unlink() except FileNotFoundError: @@ -627,7 +613,6 @@ def overrides_value(names): def main(): - # droiddeck-wincomponents <compatdata dir> <appid>: what a launch would do, for a shell. if len(sys.argv) != 3: print("usage: droiddeck-wincomponents <compatdata dir> <appid>", file=sys.stderr) return 2 diff --git a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility index b64d721b7..71557cf00 100755 --- a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility +++ b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility @@ -1,20 +1,4 @@ #!/usr/bin/python3 -# Makes the ARM64 Proton the client already downloaded usable here, and sets it as the default tool -# for Windows titles (CompatToolMapping "0" in config.vdf) when the user has not chosen one. Run -# before the client starts; it rewrites config.vdf on exit. -# -# steam-compatibility <steam root> -# -# Valve ships a native aarch64 Proton (arm64ec Wine with FEX as the x86 layer) as an ordinary depot, -# which is exactly what this runtime wants. Its toolmanifest declares require_tool_appid 4185400 - -# Steam Linux Runtime 4 for arm64 - so Steam stacks pressure-vessel underneath it, and -# pressure-vessel needs unprivileged user namespaces, which an Android app does not get. Steam then -# drops the launch on the floor: no window, no log, straight back to the library. -# -# So the depot is re-registered here as a compatibility tool of our own: every entry symlinked so -# nothing is copied and Steam still updates the real depot, but with a toolmanifest that leaves out -# require_tool_appid. Proton takes its base directory from dirname(sys.argv[0]) without resolving -# it, so it reads files/ through those symlinks and runs directly on this rootfs. import json import os import re @@ -26,22 +10,10 @@ TOOL = "droiddeck-proton-arm64" TOOL_11 = "droiddeck-proton-11-arm64" LEGACY_TOOL = "bannerlator-proton-arm64" LABEL_FILE = "/etc/steam-compatibility-label" -# The ARM64 Proton depot the client downloads for itself, as a compatibility-tool appid. -# The session asks the client to install it on the command line the first time (steam://install), -# so a new client has it before a Windows title is first launched. It is deliberately NOT named as -# this tool's dependency: a dependency is composed with its own dependencies, and this depot -# requires 4185400, the arm64 Steam Linux Runtime, whose container needs user namespaces Android -# does not give - the launch would be wrapped in it and die before this tool ever ran. PROTON_ARM64_APPID = "4427310" LAUNCHER = "droiddeck-proton" LEGACY_LAUNCHER = "bannerlator-proton" -# The extra Steam library the app mounts the installed games into (see droiddeck-steam-library). -# installed_apps() scans it too; it lives only in the sibling script, so define it here as well. -LIBRARY = "/mnt/droiddeck" # the card library is LIBRARY + "-sd"; internal storage is steam_root itself -# The client lists its own "Proton Experimental (ARM64)" and "Proton 11.0 (ARM64)" in the same -# Compatibility dropdown, and those are the two that cannot start a game here. Ours has to be -# unmistakable at a glance, or a user picks a neighbour and gets a launch that fails silently. -# Newest first: whichever the client has downloaded is used. +LIBRARY = "/mnt/droiddeck" SOURCES = ("Proton Experimental (ARM64)", "Proton 11.0 (ARM64)") OWN_TOOLS = ((TOOL, SOURCES), (TOOL_11, ("Proton 11.0 (ARM64)",))) FEX_TOOL = "droiddeck-fex" @@ -53,13 +25,7 @@ FEX_DEPENDENCIES = (FEX_APPID, SNIPER_APPID) REQUEST_HANDOFF_SECONDS = 30 PLAYABLE_TYPES = frozenset(("game", "demo", "application", "beta")) COMPAT_DIR = os.path.join(os.environ.get("HOME", "/root"), ".local", "share", "droiddeck-compat") -# Written by us; everything else in the depot is symlinked through. OWN_FILES = ("toolmanifest.vdf", "compatibilitytool.vdf", LAUNCHER) -# The client assigns a Proton to a Windows title by itself, as a per-app CompatToolMapping entry at -# priority 250 - which outranks the "0" default, so setting that default alone changes nothing. Every -# Proton it can pick is one this runtime cannot start: the ARM64 ones want pressure-vessel (see -# above) and the x86_64 ones want pressure-vessel and FEX on top. Ours is the only one that runs -# here, so every Proton mapping is repointed at it. Tools that are not Proton are left alone. COMPAT_TOOL_VDF = """"compatibilitytools" { "compat_tools" @@ -84,49 +50,25 @@ TOOL_MANIFEST_VDF = """"manifest" """ % LAUNCHER -# Steam runs its own graphics probe through EVERY registered tool at once, ~40 s after logging in, -# all with STEAM_COMPAT_DATA_PATH=.../compatdata/0, and those concurrent Protons can write their -# builtin DLLs through each other's prefix links into each other's trees (seen: GE's FEX DLLs became -# Proton Experimental's). Moving those runs to a prefix per tool was tried and crashed the Steam -# client right after the probe started (segfault, rc 139, first ever in 25 sessions), so the client -# relies on compatdata/0 itself. The launch-time check below is the protection instead. - -# The Components page's choices, enforced in the one place every game launch passes: the app keeps -# desired.tsv (scope, tool dir, Proton build, component, file, sha256, source copy) and an unpacked -# copy of each package in use. A game's rows replace General rows for that component. A `-` source -# removes a managed file absent from the selected package, so one game's package cannot leak into -# the next game. A file that no longer matches - whatever changed it - is put back before Proton -# starts. Only the matching Proton build is changed; after a Steam update the build differs and the -# Proton's own new files are left alone until the user chooses again. -# A store game (GOG, Epic, Amazon) is started through the launcher .bat DroidDeck wrote beside it. -# An Epic game needs a fresh sign-in code, which only the app can mint: droiddeck-store-launch asks -# for it and the app writes it into the game's folder, so a launch from the client's own Play -# button signs in just as one from the app does. A real launch only; never in the way of it. BL_STORE_SETUP = """ bl_store_launch() { [ "${verb:-}" = waitforexitandrun ] || return 0 - # A web address the game opens (Wine's winebrowser runs xdg-open) goes to Android's browser: - # droiddeck-open-url as xdg-open, first on the game's PATH, with the session channel left in a - # file in case Wine does not pass BL_LAUNCH_DIR on. (The prefix also names it as Wine's browser.) if [ -x /usr/local/bin/droiddeck-open-url ]; then mkdir -p /tmp/droiddeck-browser ln -sfn /usr/local/bin/droiddeck-open-url /tmp/droiddeck-browser/xdg-open - [ -n "${BL_LAUNCH_DIR:-}" ] && printf '%s\n' "$BL_LAUNCH_DIR" > /tmp/droiddeck-browser/launch-dir + [ -n "${BL_LAUNCH_DIR:-}" ] && printf '%s +' "$BL_LAUNCH_DIR" > /tmp/droiddeck-browser/launch-dir PATH="/tmp/droiddeck-browser:$PATH"; export PATH fi - # BL_DEBUG_BROWSER=1 (droiddeck-env): Wine traces how the game opens a web address. if [ "${BL_DEBUG_BROWSER:-}" = 1 ]; then WINEDEBUG="${WINEDEBUG:+$WINEDEBUG,}+shell,+winebrowser"; export WINEDEBUG fi [ -x /usr/local/bin/droiddeck-store-launch ] || return 0 /usr/local/bin/droiddeck-store-launch "$@" - # 10: a store game with cloud saves - the launch waits for Proton and uploads after (bl_run). [ $? -eq 10 ] && BL_STORE_EXIT=1 return 0 } -# Runs Proton: exec'd as always, except for a store game with cloud saves, where the wrapper stays -# to tell the app the game has exited so it can upload what changed. bl_run() { if [ -n "${BL_STORE_EXIT:-}" ]; then "$@" @@ -143,11 +85,6 @@ bl_components() { base=$1 cm_dir="${HOME:-/root}/.local/share/droiddeck-components" cm_list="$cm_dir/desired.tsv" - # A real game launch only: the verb the client starts a game with and the game's own prefix. Every - # other run - above all the client's own probes right after login, which all share compatdata/0 - - # is left exactly as it was: with this check running through them, the client segfaulted (rc 139) - # within seconds of starting its probes, four sessions out of four. The prefix decides rather than - # the app id because a non-Steam shortcut launches with app id 0 but still gets a prefix of its own. [ "${verb:-}" = waitforexitandrun ] || return 0 case "${STEAM_COMPAT_DATA_PATH:-}" in ''|*/compatdata/0|*/compatdata/0/|*/compatdata/0-*) return 0 ;; esac [ -f "$cm_list" ] || return 0 @@ -192,8 +129,6 @@ bl_components() { if (scopes[i] == app || (scopes[i] == "*" && !specific[comps[i]])) print rows[i] } ' "$cm_list") - # One sha256sum for every file: each program started here pays a dozen traced library lookups - # under the game's search path before it does anything. local cm_rows=() cm_files=() row h p local -A cm_sums=() while IFS=$'\\t' read -r scope t want comp rel sum src; do @@ -230,36 +165,17 @@ bl_components() { } """ -# Resolves the depot at launch time, so the tool can be registered before the depot exists and -# survives the client updating or replacing it. Proton takes its base directory from -# dirname(sys.argv[0]) without resolving it, so it is run from the depot itself. -# Cores for a game the client launches, kept separate from the client's own. One mask suits the -# client's menus and another suits a game, and they are wanted at the same time - so the game's is -# applied here, in the launch path every game goes through, rather than to the session as a whole. BL_GAME_CPUS_SETUP = """ bl_game_cpus() { [ -n "${BL_GAME_CPUS:-}" ] || return 0 command -v taskset >/dev/null 2>&1 || return 0 - # exec'd through taskset rather than set afterwards: a mask applied to a pid races the process - # into spawning its own threads, and this way every thread inherits it from the first instruction. BL_TASKSET="taskset -c $BL_GAME_CPUS" echo "droiddeck-proton: game cores $BL_GAME_CPUS" >&2 } """ -# Quake III engine games (id Tech 3) set their own resolution and switch the display to it. Under -# gamescope with the ARM64 Proton that switch is refused ("calling CDS: failed, bad mode") and the -# game never gets OpenGL; left at its own size in a window, it draws small in a corner. So these -# titles are told to stay windowed at the session's own size - unless the user's launch options -# already say something about the video mode, which are left as they are. -# The session puts the client's own libraries (steamrtarm64, its panorama/) ahead of everything for -# the client, and a game inherits that: Wine then resolved gnutls, SDL, FFmpeg and the rest against -# the client's private copies instead of the rootfs it was built for. Inside Steam's runtime -# container that path never reaches Proton either. (WinNative's winnative-proton-launch.) BL_LIBPATH_SETUP = """ bl_libpath() { - # A Proton built to prefer PipeWire (CachyOS's among the adopted ones) hung its audio thread - # here, where only PulseAudio answers. (WinNative, winnative-proton-launch.) export PROTON_USE_PIPEWIRE=${PROTON_USE_PIPEWIRE:-0} [ -n "${LD_LIBRARY_PATH:-}" ] || return 0 local kept= dir old_ifs=$IFS @@ -279,7 +195,6 @@ BL_IDTECH3_SETUP = """ bl_idtech3() { BL_EXTRA_ARGS=() case "${SteamAppId:-${STEAM_COMPAT_APP_ID:-}}" in - # Quake III Arena, Quake III: Team Arena, Return to Castle Wolfenstein, Jedi Academy. 2200|2350|9010|6020) ;; *) return 0 ;; esac @@ -307,10 +222,6 @@ bl_directaudio() { echo "droiddeck-proton: DirectAudio left off - it is built for Wine 11 and this Proton is '${version:-unknown}'. Pairing them would be silent rather than broken, so it is not worth guessing." >&2 return 0 fi - # Two builds of the driver ship, one per Wine audio interface, and both Wines call themselves - # 11.0: the private mmdevapi table is not versioned, and the wrong set plays silence, not an - # error. Proton's own winepulse.so tells them apart - it imports PsCreateSystemThread only on - # the system-thread table (Proton-CachyOS), never on the classic one (Valve, GE-Proton). winepulse="$base/files/lib/wine/aarch64-unix/winepulse.so" if [ ! -f "$winepulse" ]; then echo "droiddeck-proton: DirectAudio left off - no winepulse.so to read this Proton's audio interface from" >&2 @@ -322,20 +233,10 @@ bl_directaudio() { echo "droiddeck-proton: DirectAudio left off - no driver set for the $set interface is staged" >&2 return 0 fi - # Wine appends the per-architecture subdirectory to each entry, so the directory holding - # aarch64-unix/ and aarch64-windows/ is what goes here. WINEDLLPATH="$da/lib/wine${WINEDLLPATH:+:$WINEDLLPATH}" export WINEDLLPATH - # Proton-CachyOS's launcher sets WINE_AUDIO_DRIVER=pulse,alsa (setdefault, so an existing value - # wins), and its mmdevapi reads that variable BEFORE the registry - so the selection below was - # being overridden to pulse on every CachyOS game. Valve's and GE's mmdevapi do not read the - # variable at all and take the registry, which is why both are done. A value the user set for - # the game still wins: the per-game environment is applied after this. WINE_AUDIO_DRIVER="directaudio,pulse" export WINE_AUDIO_DRIVER - # mmdevapi only tries the drivers named in the registry; its built-in list is pulse, alsa, oss and - # coreaudio and knows nothing about this one. The key is appended to the prefix's user.reg rather - # than written with `wine reg`, which would start a wineserver here just to set one value. pfx="${STEAM_COMPAT_DATA_PATH:-}/pfx" reg="$pfx/user.reg" if [ -f "$reg" ]; then @@ -352,46 +253,21 @@ bl_directaudio() { elif ! BL_KEY="$key" awk 'index($0, "[") == 1 { inside = index($0, ENVIRON["BL_KEY"] " ") == 1 } inside && index($0, "\\"Audio\\"=") == 1 { last = $0 } END { exit last != "\\"Audio\\"=\\"directaudio,pulse\\"" }' "$reg" 2>/dev/null; then printf '\\n%s %s\\n"Audio"="directaudio,pulse"\\n' "$key" "$(date +%s)" >> "$reg" echo "droiddeck-proton: DirectAudio selected in the prefix" >&2 - # Steam runs this tool several times per launch (getcompatpath, run, waitforexitandrun); a - # wineserver from an earlier verb that still holds the registry saves its own copy on exit - # and drops this line. Said out loud so a game that stays on pulse once is explained; it - # takes on the next launch. (Writing through the live server with a bare `wine reg add` was - # tried and withdrawn: a second Wine invocation inside Steam's launch sequence is not safe.) srv="${BL_WINE_SERVER_BASE:-/tmp/.wine-$(id -u)}/server-$(printf '%x-%x' $(stat -c '%d %i' "$pfx" 2>/dev/null || echo 0 0))/socket" if [ -S "$srv" ]; then echo "droiddeck-proton: a wineserver is already running for this prefix; DirectAudio may only engage on the next launch" >&2 fi fi else - # A prefix that does not exist yet is created by Proton moments from now, after this runs, so - # the value lands on the NEXT launch of that game. Said out loud so a first launch on Proton's - # own audio does not look like a fault. (Having Proton create the prefix from here first was - # tried and withdrawn: a second Proton run inside Steam's launch sequence got the launch - # aborted - Steam games bounced back to their page with a half-made prefix.) echo "droiddeck-proton: no prefix yet - DirectAudio takes effect the next time this game starts" >&2 fi echo "droiddeck-proton: DirectAudio ready (Wine $major, $set)" >&2 } """ -# Resolves the depot at launch time, so the tool can be registered before the depot exists and -# survives the client updating or replacing it. Proton takes its base directory from -# dirname(sys.argv[0]) without resolving it, so it is run from the depot itself. LAUNCHER_SH = """#!/bin/bash -# DroidDeck's Proton ARM64: runs the ARM64 Proton the Steam client downloaded, without the -# Steam Linux Runtime container its own manifest asks for (no user namespaces on Android). verb=$1; shift steam_root=${STEAM_COMPAT_CLIENT_INSTALL_PATH:-${XDG_DATA_HOME:-$HOME/.local/share}/Steam} -# Steam's overlay is preloaded into every game Valve launches, and used to be dropped here: it was -# landing in front of open() and read() on /dev/input, letting Wine open the session's pad and -# answering every read of it with nothing - a controller that never reports, and Wine's HID thread -# spinning a core on it. -# -# Removing it was the wrong cure for an ordering problem. glibc processes LD_PRELOAD BEFORE -# /etc/ld.so.preload, and the interposer lives in the latter, so the overlay was simply ahead of it. -# Naming the interposer first in LD_PRELOAD puts it back in front and the overlay loads as Valve -# intends. That matters beyond the overlay itself: every deviation from the launch Valve ships is a -# difference this runtime would otherwise have to justify. for fake in /usr/local/lib/libfakeinput.so /usr/local/lib/libblsession.so; do if [ -f "$fake" ]; then case ":${LD_PRELOAD:-}:" in @@ -400,9 +276,6 @@ for fake in /usr/local/lib/libfakeinput.so /usr/local/lib/libblsession.so; do esac fi done -# ld.so looks every library up along the game's search path (its own directory, Steam's runtime -# and video directories, Proton's), and under proot each miss is a round trip to the tracer. With -# the fast path, the audit module skips the misses it can prove (tools/proot/fastpath/audit.c). audit=/usr/local/lib/libblaudit.so if [ -n "${PROOT_FP_KEY:-}" ] && [ -f "$audit" ]; then case ":${LD_AUDIT:-}:" in @@ -410,8 +283,6 @@ if [ -n "${PROOT_FP_KEY:-}" ] && [ -f "$audit" ]; then *) LD_AUDIT="$audit${LD_AUDIT:+:$LD_AUDIT}"; export LD_AUDIT ;; esac fi -# The depot this tool is named after, newest across the libraries that hold it; the others in the -# list stand in only when it is missing. depot= libraries=$(printf '%%s\\n' "$steam_root" /mnt/droiddeck-sd; sed -n 's/^[[:space:]]*"path"[[:space:]]*"\\(.*\\)"[[:space:]]*$/\\1/p' "$steam_root/steamapps/libraryfolders.vdf" 2>/dev/null) for name in %s; do @@ -447,9 +318,6 @@ LAUNCHER_TEMPLATE = LAUNCHER_SH def launcher_sh(sources=SOURCES): - # Spliced in after the formatting, never through it: the function body contains ${major%%.*} - # and a % format would eat that into a single %, leaving a shell expansion that silently does - # something else. return (LAUNCHER_TEMPLATE % " ".join('"%s"' % n for n in sources)).replace( "verb=$1; shift", "verb=$1; shift\n" + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP + BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP + BL_STORE_SETUP, 1) @@ -457,10 +325,6 @@ def launcher_sh(sources=SOURCES): LAUNCHER_SH = launcher_sh() -# A third-party Proton dropped into compatibilitytools.d - GE-Proton, proton-cachyos, anything of -# that shape - is adopted rather than used as shipped, for the same two reasons Valve's ARM64 depot -# is: its manifest declares require_tool_appid, and the client's overlay library has to come out of -# LD_PRELOAD. Its own entry point is left untouched and called through this wrapper. EXTRA_WRAPPER = "droiddeck-proton-wrap" LEGACY_EXTRA_WRAPPER = "bannerlator-proton-wrap" ORIGINAL_SUFFIX = ".droiddeck-orig" @@ -473,16 +337,9 @@ EXTRA_MANIFEST_VDF = """"manifest" "compatmanager_layer_name" "proton" } """ % EXTRA_WRAPPER -# %s is the tool's own entry point, read out of the manifest it shipped with. EXTRA_WRAPPER_SH = """#!/bin/bash -# DroidDeck: runs a Proton installed into compatibilitytools.d without the Steam Linux Runtime -# container its own manifest asks for - pressure-vessel needs unprivileged user namespaces, which -# an Android app does not get, and the launch dies before Proton is reached. verb=$1; shift here=$(dirname "$0") -# See droiddeck-proton: the interposer goes first in LD_PRELOAD rather than the overlay being -# removed, because glibc reads LD_PRELOAD before /etc/ld.so.preload and the overlay was simply -# ahead of it on /dev/input. for fake in /usr/local/lib/libfakeinput.so /usr/local/lib/libblsession.so; do if [ -f "$fake" ]; then case ":${LD_PRELOAD:-}:" in @@ -491,7 +348,6 @@ for fake in /usr/local/lib/libfakeinput.so /usr/local/lib/libblsession.so; do esac fi done -# See droiddeck-proton: library lookups that cannot succeed are skipped with the fast path. audit=/usr/local/lib/libblaudit.so if [ -n "${PROOT_FP_KEY:-}" ] && [ -f "$audit" ]; then case ":${LD_AUDIT:-}:" in @@ -507,8 +363,6 @@ bl_libpath BL_STORE_PROTON="$here/%s"; export BL_STORE_PROTON bl_store_launch "$@" BL_STORE_ARGS=("$@") -# Proton takes its base directory from dirname(sys.argv[0]) without resolving it, so it has to be -# started from its own tree. bl_run ${BL_TASKSET:-} /usr/local/bin/droiddeck-game-env "$BL_STORE_PROTON" "$verb" "$@" "${BL_EXTRA_ARGS[@]}" """ EXTRA_COMPAT_TOOL_VDF = """"compatibilitytools" @@ -707,8 +561,6 @@ def adopt_extras(tools): manifest = os.path.join(tool_dir, "toolmanifest.vdf") if not os.path.isdir(tool_dir) or not os.path.isfile(manifest): continue - # The shipped manifest is kept, once, and stays the source of the entry point: re-running - # must not read back our own rewritten commandline and wrap the wrapper. original = manifest + ORIGINAL_SUFFIX legacy = manifest + LEGACY_ORIGINAL_SUFFIX if not os.path.isfile(original) and os.path.isfile(legacy): @@ -731,8 +583,6 @@ def adopt_extras(tools): name = first_key(compat_vdf, ["compatibilitytools", "compat_tools"]) or entry wrapper = os.path.join(tool_dir, EXTRA_WRAPPER) try: - # Formatted first, spliced second: the function carries a printf %s and a - # ${major%%.*} of its own, and putting it through the format would eat both. body = (EXTRA_WRAPPER_SH % entry_point).replace( 'here=$(dirname "$0")', 'here=$(dirname "$0")\n' + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP @@ -750,19 +600,17 @@ def adopt_extras(tools): return adopted -# Steam's own tools and runtimes live in steamapps beside the games; they are not titles and must -# not be mapped to a compatibility tool. NOT_GAMES = frozenset(( - "228980", # Steamworks Common Redistributables - "1070560", # Steam Linux Runtime 1.0 (scout) - "1391110", # Steam Linux Runtime 2.0 (soldier) - "1628350", # Steam Linux Runtime 3.0 (sniper) - "1493710", # Proton Experimental - "3127680", # FEX - "4183110", # Steam Linux Runtime 4.0 - "4185400", # Steam Linux Runtime 4.0 - Arm64 - "4427310", # Proton Experimental (ARM64) - "4628740", # Proton 11.0 (ARM64) + "228980", + "1070560", + "1391110", + "1628350", + "1493710", + "3127680", + "4183110", + "4185400", + "4427310", + "4628740", )) @@ -1299,8 +1147,6 @@ def build_tool(tool_dir, depot, sources=SOURCES, name=TOOL): if os.path.islink(tool_dir): os.unlink(tool_dir) elif os.path.isdir(tool_dir) and set(os.listdir(tool_dir)) - set(OWN_FILES): - # Earlier versions mirrored the depot in here as symlinks; start clean. A directory holding - # only our own files is kept, and each file rewritten only when it changed. shutil.rmtree(tool_dir) os.makedirs(tool_dir, exist_ok=True) write_changed(os.path.join(tool_dir, "toolmanifest.vdf"), TOOL_MANIFEST_VDF) diff --git a/tools/linuxfs/preload/binfmt.c b/tools/linuxfs/preload/binfmt.c index 6592a8d5e..7aee2ee2f 100644 --- a/tools/linuxfs/preload/binfmt.c +++ b/tools/linuxfs/preload/binfmt.c @@ -1,11 +1,3 @@ -/* - * binfmt_misc for the desktop, which proot does not have: an exec of an x86 Linux program, a Windows - * program, or a program or script on noexec shared storage goes to droiddeck-open --exec, which runs - * it under FEX, Proton or its interpreter. x86 and Windows programs are caught before the exec: proot - * loads a static x86 program as a native one, which dies on its first instruction, and execvp runs a - * Windows program as a shell script. A program on noexec storage also passes access(X_OK), which KIO - * and Qt ask before they start one. Only in the desktop session. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <elf.h> @@ -27,7 +19,6 @@ typedef int (*spawn_fn)(pid_t *, const char *, const posix_spawn_file_actions_t *, const posix_spawnattr_t *, char *const[], char *const[]); -/* BINFMT_TEST: tools/tests builds it for the host, whose own x86 programs stand in for foreign ones. */ #if defined(__aarch64__) || defined(BINFMT_TEST) static int desktop(void) { @@ -35,7 +26,6 @@ static int desktop(void) { return value && value[0] == '1' && value[1] == 0; } -/* 1: x86 program or noexec storage, 2: Windows program, 3: script; 0 for anything else. */ static int foreign(const char *path, int error) { if (!path || !strcmp(path, BINFMT_HANDLER)) return 0; int fd = open(path, O_RDONLY | O_CLOEXEC); @@ -52,7 +42,6 @@ static int foreign(const char *path, int error) { } else if (n >= 2 && head[0] == '#' && head[1] == '!' && error == EACCES) { kind = 3; } - /* What the kernel refuses for its permissions stays refused; noexec storage has no execute bits to give. */ struct statvfs vfs; int allowed = kind && (((st.st_mode & 0111) && error != EACCES) || (fstatvfs(fd, &vfs) == 0 && (vfs.f_flag & ST_NOEXEC))); close(fd); @@ -63,7 +52,6 @@ static int wanted(const char *path, int error) { return (error == ENOEXEC || error == ENOENT || error == EACCES) && desktop() && foreign(path, error); } -/* The program on PATH as execvp would find it, into [out]. */ static int lookup(const char *file, char *out, size_t size) { if (!file || !*file) return 0; if (strchr(file, '/')) return snprintf(out, size, "%s", file) < (int)size; @@ -111,19 +99,16 @@ static int spawn_handler(pid_t *pid, const char *path, const posix_spawn_file_ac return real(pid, BINFMT_HANDLER, fa, attr, args, envp); } -/* Before an exec of [path]: an x86 or Windows program goes to the handler instead. */ __attribute__((visibility("hidden"))) void bl_binfmt_before(const char *path, char *const argv[], char *const envp[]) { if (desktop() && foreign(path, ENOEXEC)) hand_off(path, argv, envp); } -/* After a failed exec of [path]: the handler takes it over, or the exec's own error stands. */ __attribute__((visibility("hidden"))) int bl_binfmt_retry(const char *path, char *const argv[], char *const envp[], int error) { if (wanted(path, error)) hand_off(path, argv, envp); errno = error; return -1; } -/* The same around execvp, for the program it finds on PATH. */ __attribute__((visibility("hidden"))) void bl_binfmt_before_search(const char *file, char *const argv[], char *const envp[]) { char path[PATH_MAX]; if (desktop() && lookup(file, path, sizeof path)) bl_binfmt_before(path, argv, envp); @@ -136,7 +121,6 @@ __attribute__((visibility("hidden"))) int bl_binfmt_after_search(const char *fil return -1; } -/* After access(X_OK) of [path] was refused (pathcache.c): a program on noexec storage runs all the same. */ __attribute__((visibility("hidden"))) int bl_binfmt_runnable(const char *path, int mode) { if (mode != X_OK || !desktop()) return 0; int runs = foreign(path, EACCES) != 0; diff --git a/tools/linuxfs/preload/dircache.c b/tools/linuxfs/preload/dircache.c index baf0ada12..bdfec14da 100644 --- a/tools/linuxfs/preload/dircache.c +++ b/tools/linuxfs/preload/dircache.c @@ -1,10 +1,3 @@ -/* - * The Steam client lists a whole content directory about ten times per file it creates, moves or - * deletes, to match names case-insensitively. On Android's FUSE storage that made reserving space - * quadratic. Listings under steamapps/ on FUSE are kept for at most DIR_TTL_NS and dropped on any - * inotify event the client's own patched changes don't account for; directory timestamps alone - * can't be trusted, as NTFS volumes report whole seconds. BL_NO_DIR_CACHE=1 turns it off. - */ #define _GNU_SOURCE #include <dirent.h> #include <dlfcn.h> @@ -32,7 +25,6 @@ #define FUSE_MAGIC 0x65735546 #define WATCHED (IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE_SELF | IN_MOVE_SELF | IN_ONLYDIR) -/* The client is 64-bit; 32-bit builds only pass creates through. */ #if __SIZEOF_POINTER__ == 8 _Static_assert(sizeof(struct dirent) == sizeof(struct dirent64), "dirent layouts differ"); @@ -88,7 +80,6 @@ static long long now_ns(void) { return (long long)t.tv_sec * 1000000000LL + t.tv_nsec; } -/* A child shares the parent's inotify queue, so it stops keeping listings. */ static void after_fork(void) { pthread_mutex_init(&lock, NULL); pthread_mutex_init(&oplock, NULL); @@ -162,7 +153,6 @@ static void unref(struct listing *l) { if (--l->refs == 0 && l->detached) destroy(l); } -/* The rest is called with lock held. */ static void release(int wd) { if (wd < 0 || notify < 0) return; @@ -179,7 +169,6 @@ static void detach(struct listing *l) { if (l->refs == 0) destroy(l); } -/* Takes the listing out of the table; freed now or when its last reader closes. */ static void drop(int slot) { struct listing *l = table[slot]; int wd = l->wd; @@ -195,7 +184,6 @@ static void changed(int wd) { if (handles[i] && (handles[i]->wd == wd || wd < 0)) __atomic_store_n(&handles[i]->dirty, 1, __ATOMIC_RELAXED); } -/* Applies queued events; the expected ones are the client's own changes, already patched in. */ static void pump(struct expect *x, int nx) { char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event)))); if (notify < 0 || forked) return; @@ -302,7 +290,6 @@ static DIR *wrap(struct handle *h) { #define RECLEN(namelen) ((offsetof(struct dirent64, d_name) + (namelen) + 1 + 7) & ~(size_t)7) -/* The listing as packed dirent64 records, built once per version. */ static int records(struct listing *l) { if (l->recs) return 0; size_t len = 0; @@ -326,7 +313,6 @@ static int records(struct listing *l) { return 0; } -/* Lock-free from here on unless noted: a stream is only ever used by one thread at a time. */ static struct handle *lookup(DIR *d) { return registered(d) >= 0 ? (struct handle *)d : NULL; @@ -356,7 +342,6 @@ REAL(symlink); REAL(readdir64_r); REAL(readdir_r); -/* Resolved before the client starts threads; the calls below only fill gaps for earlier callers. */ __attribute__((constructor)) static void resolve(void) { NEXT(opendir); NEXT(readdir64); @@ -378,7 +363,6 @@ __attribute__((constructor)) static void resolve(void) { } #pragma GCC diagnostic pop -/* A reader of the kept listing when it is still current. */ static DIR *hit(const struct stat *st, struct handle *h, long long now) { pthread_mutex_lock(&lock); if (!changing) pump(NULL, 0); @@ -401,7 +385,6 @@ static DIR *hit(const struct stat *st, struct handle *h, long long now) { return NULL; } -/* Registers the stream and its watch before anything is read, so no change goes unseen. */ static int track(struct handle *h, const char *path) { pthread_mutex_lock(&lock); h->wd = watch(path); @@ -411,7 +394,6 @@ static int track(struct handle *h, const char *path) { return d != NULL; } -/* Reads through the real stream, keeping what it returns. */ static void filling(struct handle *h, DIR *real, const struct stat *st, long long now) { h->real = real; h->fill = calloc(1, sizeof(*h->fill)); @@ -625,7 +607,6 @@ static int watching(const char *path) { return eligible(path) && __atomic_load_n(&cached_listings, __ATOMIC_ACQUIRE) > 0; } -/* Called with oplock held: the client's tracked changes happen one at a time. */ static void begin(void) { pthread_mutex_lock(&lock); pump(NULL, 0); @@ -639,7 +620,6 @@ static void before(struct side *s) { enum { ADDED, REMOVED }; -/* Patches the listing after the client's own change, or drops it when unsure. Called with lock held. */ static int patch(struct side *s, int what, uint32_t event, int ok, int error, const char *target, struct expect *x) { struct stat now; if (!s->known) return 0; @@ -795,7 +775,6 @@ ONE_PATH(remove, REMOVED, IN_DELETE, next_remove(path)) return r; \ } -/* A refused hard link to a lock file is taken as a symlink (links.c). */ int bl_link_lock(const char *oldpath, const char *newpath, int result) __attribute__((visibility("hidden"))); static int link_or_lock(const char *from, const char *to) { return bl_link_lock(from, to, next_link(from, to)); } diff --git a/tools/linuxfs/preload/drm.c b/tools/linuxfs/preload/drm.c index bb4374be0..101512387 100644 --- a/tools/linuxfs/preload/drm.c +++ b/tools/linuxfs/preload/drm.c @@ -1,10 +1,3 @@ -/* - * GEM handle emulation for the KGSL device presented as a DRM render node. - * - * Compositors and Mesa validate or convert dma-bufs with PRIME ioctls on the render node; KGSL - * has no GEM. Handles only serve as tokens to these callers, so a table of duplicated dma-buf - * descriptors stands in for the kernel's, and every other device is left to libdrm. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -19,7 +12,6 @@ #define MAX_HANDLES 4096 -/* DRM_IOCTL_GEM_CLOSE from drm.h: _IOW('d', 0x09, struct drm_gem_close { u32 handle, pad; }). */ struct gem_close { uint32_t handle; uint32_t pad; @@ -36,7 +28,7 @@ static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; static struct handle_entry handle_entries[MAX_HANDLES]; static uint32_t next_handle = 1; static dev_t kgsl_dev; -static int kgsl_state; /* 0 unknown, 1 known, -1 absent */ +static int kgsl_state; static int is_kgsl(int fd) { struct stat st; @@ -63,7 +55,7 @@ static int same_file_description(int a, int b) { } #ifdef SYS_kcmp int saved_errno = errno; - long ret = syscall(SYS_kcmp, getpid(), getpid(), 0 /* KCMP_FILE */, a, b); + long ret = syscall(SYS_kcmp, getpid(), getpid(), 0 , a, b); errno = saved_errno; return ret == 0; #else @@ -81,11 +73,6 @@ static int find_handle_locked(int fd, uint32_t handle) { return -1; } -/* - * The old table used its slot index as the public handle. Once #275 started freeing slots, - * rapidly recreated swapchains immediately reused those numbers. Keep storage slots reusable but - * make the public token monotonic so a stale handle can fail instead of aliasing a newer buffer. - */ static uint32_t alloc_handle_locked(void) { for (int tries = 0; tries <= MAX_HANDLES; tries++) { uint32_t candidate = next_handle++; @@ -157,7 +144,6 @@ int drmPrimeHandleToFD(int fd, uint32_t handle, uint32_t flags, int *prime_fd) { return ret; } -/* Drops only a handle belonging to this KGSL file description. */ static int release_handle(int fd, uint32_t handle) { int found = 0; pthread_mutex_lock(&lock); @@ -179,16 +165,6 @@ int drmCloseBufferHandle(int fd, uint32_t handle) { return release_handle(fd, handle) ? 0 : -EINVAL; } -/* - * Zink gives its handles back with the GEM_CLOSE ioctl itself, not drmCloseBufferHandle - * (zink_bo.c, bo_destroy). KGSL refuses the ioctl, so the duplicated descriptor stayed open and - * kept the whole buffer alive. Preserve #275's reclamation without letting another KGSL file - * description release the token or making a new buffer immediately inherit a retired token. - * - * Caught at ioctl() (netif.c), which libdrm's drmIoctl calls, rather than by exporting drmIoctl: - * every session process preloads this library, and exporting drmIoctl from it brought on a - * judder replaying old frames in No Man's Sky on an Adreno 840 even with no handle released. - */ __attribute__((visibility("hidden"))) int bl_drm_gem_close(int fd, unsigned long request, void *arg, int *rc) { if (request != GEM_CLOSE || !arg || !is_kgsl(fd)) { return 0; @@ -202,12 +178,6 @@ __attribute__((visibility("hidden"))) int bl_drm_gem_close(int fd, unsigned long return 1; } -/* - * fork() carries over only the calling thread, so a lock another thread was holding at that - * instant stays held in the child by a thread that is not there to release it. Taking it before - * the fork makes the copy consistent; the parent then unlocks it and the child, whose one thread - * never locked it, gets a fresh one. (WinNative 79aa7f68.) - */ static void drm_lock_before_fork(void) { pthread_mutex_lock(&lock); } static void drm_unlock_after_fork(void) { pthread_mutex_unlock(&lock); } static void drm_reset_after_fork(void) { pthread_mutex_init(&lock, NULL); } diff --git a/tools/linuxfs/preload/fsync.c b/tools/linuxfs/preload/fsync.c index 844af92b8..dfcb06979 100644 --- a/tools/linuxfs/preload/fsync.c +++ b/tools/linuxfs/preload/fsync.c @@ -1,6 +1,3 @@ -/* droiddeck-fsync: futex_waitv(2) in userspace, so Proton's own fsync runs where the app sandbox refuses the syscall. - * Credit: fsync, the futex-based synchronization in Proton, by Elizabeth Figura and Paul Gofman; - * futex_waitv(2) by André Almeida. */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> diff --git a/tools/linuxfs/preload/inputudev.c b/tools/linuxfs/preload/inputudev.c index b4ff0b3ef..2de0ef908 100644 --- a/tools/linuxfs/preload/inputudev.c +++ b/tools/linuxfs/preload/inputudev.c @@ -1,34 +1,7 @@ /* - * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0, with names - * changed to this project's. The reasoning below is the original author's. - * - * The session's controllers, described to libudev's callers. - * - * The pads are evdev nodes the app backs with its own input rings, bound over /dev/input. Wine's - * HID bus finds them there, but it identifies a node by resolving /sys/class/input/<name> and - * asking libudev about the result: on Android that resolves to whatever real input device the - * host numbered the same, whose device node libudev cannot read, so every pad is discarded before - * its identity is ever looked at. The fallback is Wine's SDL bus, and SDL enumerates through udev - * too, over a netlink socket the app sandbox refuses. A game was left with no controller at all - * while the Steam client, which reads the nodes itself, had one. - * - * So the pads are described here instead: the resolved path is answered for the session's own - * nodes, and libudev's accessors answer for that path from what the app already knows. They carry - * the identity of the pad Steam Input would present if it could - it has no /dev/uinput here to - * make one - which is the one identity Wine's own bus keeps rather than handing on to SDL. - * - * Only Wine's device host is answered, and only for the session's own nodes: the Steam client - * enumerates the same nodes for itself and would take a pad wearing Steam Input's identity for one - * of its own and ignore it. Every other caller, path and device object goes straight through. - * - * With libfakeinput's /dev/uinput stand-in (FAKE_EVDEV_UINPUT=1) Steam Input's own virtual pads - * exist too, as event16 and up, and those are what Wine is meant to read: each is described with - * the identity the client gave it, from the .uevent file libfakeinput writes beside the node. - * While one exists the app's pads are described as what they are, an Xbox 360 controller, which - * Wine's HID bus leaves alone - the game reads Steam Input's output, not the pad under it, as on - * a Steam Deck. With none (Steam Input off for the game) they keep the virtual identity, so the - * game still has a controller. + * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0. */ + #define _GNU_SOURCE #include <dlfcn.h> #include <limits.h> @@ -39,20 +12,16 @@ #include <sys/sysmacros.h> #include <sys/types.h> -/* The app opens one ring per configured controller; four is what its launcher offers. Nodes from - * UINPUT_BASE up are the virtual pads made through libfakeinput's /dev/uinput stand-in. */ #define RING_MAX 4 #define UINPUT_BASE 16 #define PAD_MAX 64 #define EVENT_MINOR_BASE 64 -/* BUS_VIRTUAL, and the identity Steam Input gives the gamepad it presents to a game. */ #define PAD_UEVENT_FORMAT \ "PRODUCT=6/28de/11ff/110\n" \ "NAME=\"Generic HID Gamepad %d\"\n" \ "PHYS=\"usb-fakeinput/input%d\"\n" -/* The identity libfakeinput gives the pad itself (FAKE_EVDEV_IDENTITY=xbox360). */ #define PAD_PHYSICAL_UEVENT_FORMAT \ "PRODUCT=3/45e/28e/110\n" \ "NAME=\"Xbox 360 Controller (%d)\"\n" \ @@ -70,11 +39,6 @@ static struct pad pads[PAD_MAX]; static int pad_count; static pthread_mutex_t virtual_lock = PTHREAD_MUTEX_INITIALIZER; -/* - * The slots the app published rings for, named in FAKE_EVDEV_MEMFD_PATHS as "<slot>=<path>" - * separated by semicolons. Without it the process has no pads of the session's and every call - * here is a passthrough. - */ static void discover_pads(void) { const char *spec = getenv("FAKE_EVDEV_MEMFD_PATHS"); const char *entry; @@ -113,7 +77,6 @@ static int uinput_enabled(void) { return enabled != NULL && atoi(enabled) != 0; } -/* Reads the .uevent libfakeinput wrote for virtual pad <node> into buf; 0 when there is none. */ static int read_virtual_uevent(int node, char *buf, size_t size) { const char *dir = getenv("FAKE_EVDEV_DIR"); char path[PATH_MAX]; @@ -138,10 +101,6 @@ static int any_virtual_pad(void) { return 0; } -/* - * Brings the entry for event<node> up to date before Wine resolves it: a virtual pad comes and - * goes with the client, and the app's pads change identity with it. - */ static void refresh_pad(int node) { struct pad *pad; @@ -168,7 +127,6 @@ static void refresh_pad(int node) { pthread_mutex_unlock(&virtual_lock); } -/* The pad a synthetic syspath names, or NULL when the path is not one of ours. */ static struct pad *pad_for_syspath(const char *syspath) { int i; @@ -180,7 +138,6 @@ static struct pad *pad_for_syspath(const char *syspath) { return NULL; } -/* A device object is ours exactly when it is one of the pads. */ static struct pad *pad_for_device(void *device) { int i; @@ -192,7 +149,6 @@ static struct pad *pad_for_device(void *device) { return NULL; } -/* The pad /sys/class/input/<name> stands for, or NULL when the path names something else. */ static struct pad *pad_for_syslink(const char *path) { static const char prefix[] = "/sys/class/input/"; const char *name; @@ -213,7 +169,6 @@ static void *next_symbol(const char *name) { return dlsym(RTLD_NEXT, name); } char *bl_cached_realpath(const char *path, char *resolved, char *(*real)(const char *, char *)) __attribute__((visibility("hidden"))); -/* Wine reaches libudev from its HID bus driver, which is the only caller these pads are for. */ static int caller_is_winebus(void *caller) { Dl_info info; @@ -228,7 +183,6 @@ char *realpath(const char *path, char *resolved) { if (pad != NULL) { if (resolved == NULL) return strdup(pad->syspath); - /* PATH_MAX is what the caller promises when it passes a buffer. */ memcpy(resolved, pad->syspath, strlen(pad->syspath) + 1); return resolved; } @@ -336,10 +290,6 @@ dev_t udev_device_get_devnum(void *device) { return real_get(device); } -/* - * A pad has no bus behind it, so it stands in for its own input device and has no other parent. - * The reference is a borrowed one either way. - */ void *udev_device_get_parent_with_subsystem_devtype(void *device, const char *subsystem, const char *devtype) { static void *(*real_get)(void *, const char *, const char *); diff --git a/tools/linuxfs/preload/kgslid.c b/tools/linuxfs/preload/kgslid.c index 3dee629d1..c909f4618 100644 --- a/tools/linuxfs/preload/kgslid.c +++ b/tools/linuxfs/preload/kgslid.c @@ -1,16 +1,3 @@ -/* - * Adreno revisions the runtime's Turnip does not know, reported as the revision it does. - * - * Retail SM8850 phones report their A840 as chip_id 0x44050A21 (KGSL gpu_model "Adreno840v2"). - * Mesa only lists 0x44050A31, so the runtime's glibc Turnip refuses the device outright - * ("device (chip_id = 44050A21, gpu_id = 6860) is unsupported") and gamescope exits before Steam - * starts. Banners-Turnip fixes its own builds by registering the v2 id on the same A840 GPU info - * (patches/a840v2.py); answering KGSL's device-info query with the known id has the same effect - * on any Turnip in the session, including the one the runtime was built with. A driver that - * already knows the v2 id resolves the rewritten one to the same entry. - * - * Only the KGSL device-info property is touched. Its layout is the kernel's uapi (msm_kgsl.h). - */ #define _GNU_SOURCE 1 #include <stddef.h> #include <sys/ioctl.h> @@ -38,7 +25,7 @@ static const struct { unsigned int reported; unsigned int known; } CHIP_IDS[] = { - {0x44050A21, 0x44050A31}, /* Adreno 840v2 -> Adreno 840 */ + {0x44050A21, 0x44050A31}, }; void bl_kgsl_chip_id_fixup(unsigned long request, void *arg, int rc) { diff --git a/tools/linuxfs/preload/kgslpoll.c b/tools/linuxfs/preload/kgslpoll.c index 00eb8f01f..841c049ee 100644 --- a/tools/linuxfs/preload/kgslpoll.c +++ b/tools/linuxfs/preload/kgslpoll.c @@ -1,19 +1,3 @@ -/* - * A zero-timeout KGSL timestamp wait answered as the poll it was meant to be. - * - * Turnip's KGSL backend emulates timeline semaphores with timestamps, and asks "has this one - * retired yet?" by waiting on it with IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID and a timeout of 0. - * KGSL reads a timeout of 0 as no timeout at all, so the poll blocks until the GPU gets there. - * Mesa's timeline bookkeeping polls like that under a lock on every signalling submit, which put - * vkd3d-proton, DXVK, gamescope and Zink in lock-step with the GPU: one frame queued, never two. - * Found and measured by Bannerlator (PROGRESS_LOG 2026-09-29: a D3D12 demo went from 614 to - * 4335 fps with the same answer given in its Vulkan adapter); the driver-side fix is on - * Banners-Turnip, and this covers the drivers that do not carry it yet. - * - * The answer comes from the context's retired timestamp, compared the way KGSL compares them - * (with wrap-around). Anything the read does not answer - an unknown context, a device that is not - * KGSL - goes to the kernel as before. BL_NO_KGSL_POLL_FIX=1 turns it off. - */ #define _GNU_SOURCE #include <errno.h> #include <stdint.h> @@ -50,7 +34,6 @@ static int enabled(void) { return cached; } -/* True when the call was answered here, with its result in *rc (and errno set on failure). */ __attribute__((visibility("hidden"))) int bl_kgsl_poll(int (*real)(int, unsigned long, void *), int fd, unsigned long request, void *arg, int *rc) { diff --git a/tools/linuxfs/preload/links.c b/tools/linuxfs/preload/links.c index 7a963205b..350990b61 100644 --- a/tools/linuxfs/preload/links.c +++ b/tools/linuxfs/preload/links.c @@ -1,22 +1,3 @@ -/* - * Lock files taken by hard link, which Android denies apps. - * - * libICE and libXau lock an authority file the classic way: create "<file>-c", then link() it to - * "<file>-l" - the link either appears or fails because the other holder's is there. Android's - * SELinux policy refuses hard links in an app's data, so the link fails for everyone, the lock is - * never taken, and Plasma's session manager gives up at "Could not lock ICEAuthority file". With - * no session manager there is no Plasma desktop. - * - * Only that convention is answered here: when the real link is refused and the new name ends in - * "-l", a symlink stands in. Creating a symlink fails on an existing name just as a link does, so - * the lock excludes as before, and the holder removes both names when it is done. Every other - * link keeps the real answer: a program that links a file into place and then deletes the - * original would be left with a dangling symlink. - * - * That pattern itself is answered for one caller that asks: with DROIDDECK_LINK_RENAME=1 (the - * session sets it for localedef, which names a finished locale archive that way) a refused link - * becomes a rename that will not replace an existing name, which is all the link promised there. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -38,9 +19,7 @@ static int lock_name(const char *path) { return n > 2 && strcmp(path + n - 2, "-l") == 0; } -/* The answer to link(oldpath, newpath), given the real call's: a refused lock link becomes a symlink. */ __attribute__((visibility("hidden"))) int bl_link_lock(const char *oldpath, const char *newpath, int result) { - /* libc's own symlink: dircache.c's wrapper may be holding its lock around this call. */ static int (*real_symlink)(const char *, const char *); if (result == 0 || !refused(errno)) return result; if (moves_instead()) return renameat2(AT_FDCWD, oldpath, AT_FDCWD, newpath, RENAME_NOREPLACE); @@ -49,7 +28,6 @@ __attribute__((visibility("hidden"))) int bl_link_lock(const char *oldpath, cons return real_symlink(oldpath, newpath); } -/* 64-bit builds wrap link() in dircache.c, which hands its result to bl_link_lock. */ #if __SIZEOF_POINTER__ != 8 int link(const char *oldpath, const char *newpath) { static int (*real_link)(const char *, const char *); @@ -64,7 +42,6 @@ int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, int result = real_linkat(olddirfd, oldpath, newdirfd, newpath, flags); if (result != 0 && refused(errno) && flags == 0 && moves_instead()) return renameat2(olddirfd, oldpath, newdirfd, newpath, RENAME_NOREPLACE); - /* A relative old name would resolve against the new name's directory as a symlink's target. */ if (result != 0 && refused(errno) && lock_name(newpath) && oldpath[0] == '/' && flags == 0) return symlinkat(oldpath, newdirfd, newpath); return result; diff --git a/tools/linuxfs/preload/lock.c b/tools/linuxfs/preload/lock.c index a11d024e2..b802fded6 100644 --- a/tools/linuxfs/preload/lock.c +++ b/tools/linuxfs/preload/lock.c @@ -1,24 +1,7 @@ /* - * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0, with names - * changed to this project's. The reasoning below is the original author's. - * - * File locking for the game library on external storage. - * - * The user's games are bound in from /storage/emulated, which Android serves over FUSE, and that - * filesystem implements no locking at all: flock() and fcntl()'s lock commands answer ENOSYS for - * every file under it. The Steam client takes a shared lock on each content file as it validates - * or updates a title and treats the refusal as a failure, so a download walks its progress back - * and forth instead of finishing; a game reading its own data hits the same wall. - * - * Where the kernel can lock, it does: the real call runs first and its answer stands, so files on - * the app's own f2fs storage keep genuine cross-process exclusion. Only the filesystem that cannot - * lock at all is answered here, and granting is the honest reading of it - a filesystem with no - * lock state has nothing to contend over, and the one other party that could contend is inside - * this same session. Reporting failure instead buys no safety and costs the download. - * - * F_GETLK is the exception: it asks who holds a lock, and on a filesystem holding none the true - * answer is "nobody", so it is reported unlocked rather than granted. + * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0. */ + #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -28,7 +11,6 @@ #include <sys/file.h> #include <unistd.h> -/* The filesystem refuses the whole operation, rather than declining this particular lock. */ static int unsupported(int error) { return error == ENOSYS || error == ENOTSUP || error == EOPNOTSUPP; } @@ -44,12 +26,6 @@ int flock(int fd, int operation) { return result; } -/* - * fcntl()'s third argument is an int, a pointer, or absent depending on the command; only the - * lock commands are of interest here and all of those take a struct flock *. Anything else is - * passed straight through as a void * - wide enough for both an int and a pointer on every ABI - * the runtime is built for. - */ static int is_lock_command(int cmd) { switch (cmd) { case F_SETLK: @@ -104,6 +80,5 @@ int fcntl(int fd, int cmd, ...) { } #if defined(__GLIBC__) && defined(F_SETLK64) -/* glibc's large-file build redirects both names; the runtime is 64-bit, so they are the same call. */ int fcntl64(int fd, int cmd, ...) __attribute__((alias("fcntl"))); #endif diff --git a/tools/linuxfs/preload/net.c b/tools/linuxfs/preload/net.c index 636ba1aea..bc9b25afe 100644 --- a/tools/linuxfs/preload/net.c +++ b/tools/linuxfs/preload/net.c @@ -1,11 +1,3 @@ -/* - * Loopback socket ownership for Steam's IPC peer check. - * - * Steam validates a websocket peer by running lsof to find which process owns a 127.0.0.1 port. - * The Android sandbox denies both /proc/net/tcp and sock_diag, so no tool can answer. Every Steam - * process is preloaded with this library, so each records the ports it binds and connects, and the - * lsof Steam spawns is answered from that shared registry instead of being run. - */ #define _GNU_SOURCE #include <arpa/inet.h> #include <spawn.h> @@ -54,8 +46,6 @@ static int loopback_family(const struct sockaddr *address, socklen_t length) { return 0; } -/* Record that this process owns a loopback socket on its local port; peer is the far end's port, - * 0 for a listening socket. */ static void record_port(int fd, unsigned peer) { struct sockaddr_storage ss; socklen_t len = sizeof(ss); @@ -119,7 +109,6 @@ typedef int (*connect_fn)(int, const struct sockaddr *, socklen_t); typedef int (*accept4_fn)(int, struct sockaddr *, socklen_t *, int); static unsigned guarded_port(void); -/* udevmon.c: a stand-in for the netlink socket the sandbox refuses, bound here without the kernel. */ __attribute__((visibility("hidden"))) int bl_udevmon_stand_in(int fd); int bind(int fd, const struct sockaddr *addr, socklen_t len) { @@ -239,7 +228,6 @@ int connect(int fd, const struct sockaddr *addr, socklen_t len) { if (peer) record_port(fd, peer); int ret = real(fd, addr, len); if (ret == 0 || errno == EINPROGRESS) { - /* The caller goes on to test for EINPROGRESS, which the bookkeeping must not disturb. */ int saved = errno; record_port(fd, peer); errno = saved; @@ -267,7 +255,6 @@ int accept(int fd, struct sockaddr *addr, socklen_t *len) { return accept4(fd, addr, len, 0); } -/* binfmt.c: an exec of a program the kernel cannot run, handed to droiddeck-open in the desktop. */ extern char **environ; typedef int (*spawn_fn)(pid_t *, const char *, const posix_spawn_file_actions_t *, const posix_spawnattr_t *, char *const[], char *const[]); @@ -279,8 +266,6 @@ __attribute__((visibility("hidden"))) int bl_binfmt_spawn(pid_t *pid, const char const posix_spawnattr_t *attr, char *const argv[], char *const envp[], spawn_fn real); -/* lsof would read the denied /proc/net/tcp; answer its -i TCP@host:port query from the registry - * instead of executing it. Recognised only when invoked as lsof with that query. */ static int maybe_answer_lsof(const char *path, char *const argv[]) { const char *base = strrchr(path, '/'); base = base ? base + 1 : path; @@ -302,7 +287,7 @@ static int maybe_answer_lsof(const char *path, char *const argv[]) { snprintf(path_buf, sizeof(path_buf), "%s/p%u", net_dir(), (unsigned)atoi(port_str)); int in = open(path_buf, O_RDONLY); if (in < 0) { - _exit(1); /* lsof exit code for "nothing found" */ + _exit(1); } char buf[64]; int n = read(in, buf, sizeof(buf) - 1); @@ -314,14 +299,12 @@ static int maybe_answer_lsof(const char *path, char *const argv[]) { int pid = 0, ppid = 0, uid = 0; unsigned peer = 0; sscanf(buf, "%d %d %d %u", &pid, &ppid, &uid, &peer); - /* Steam splits the address on "->" and wants exactly two halves, taking the port it asked - * about from the left one; only a listening socket has no far end. */ char out[160]; int m = peer ? snprintf(out, sizeof(out), "p%d\nR%d\nu%d\nn127.0.0.1:%u->127.0.0.1:%u\n", pid, ppid, uid, (unsigned)atoi(port_str), peer) : snprintf(out, sizeof(out), "p%d\nR%d\nu%d\nn127.0.0.1:%u\n", pid, ppid, uid, (unsigned)atoi(port_str)); - if (write(1, out, m) != m) { /* best effort */ } + if (write(1, out, m) != m) { } _exit(0); } @@ -334,8 +317,6 @@ int execve(const char *path, char *const argv[], char *const envp[]) { return bl_binfmt_retry(path, argv, envp, errno); } -/* popen(3) and posix_spawn(3) reach lsof without going through the execve symbol above. The - * check is cheap, so a spawned lsof is answered here too by handling it in a short-lived child. */ static int answer_lsof_via_spawn(const char *path, char *const argv[], pid_t *pid) { pid_t child = fork(); if (child < 0) { diff --git a/tools/linuxfs/preload/netif.c b/tools/linuxfs/preload/netif.c index acdc6e66d..bcd12e924 100644 --- a/tools/linuxfs/preload/netif.c +++ b/tools/linuxfs/preload/netif.c @@ -1,16 +1,7 @@ /* - * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0, with names - * changed to this project's. The reasoning below is the original author's. - * - * Network interfaces for a sandbox that hides them. - * - * Android denies an app the rtnetlink dump behind getifaddrs() and if_nameindex(), the hardware - * address ioctl, and every table under /proc/net. Wine builds its adapter, address, route and - * neighbour tables from exactly those, so a game saw a machine with no adapter, no address, no - * gateway and no MAC, and one that checks its adapters before going online waited for good. - * The app knows the real link from ConnectivityManager and writes it to /etc/droiddeck-net; - * whatever the kernel refuses is answered from that file. Anything the kernel does answer wins. + * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0. */ + #define _GNU_SOURCE 1 #include <arpa/inet.h> #include <dlfcn.h> @@ -108,9 +99,6 @@ static int fresh(long long *at) { return then != 0 && now_ns() - then < FRESH_NS; } -/* Wine's network driver reads the link several times for each table it builds, and some games - * have it build them all hundreds of times a second. The app rewrites the file when the device - * changes network, so a reading stands for a second. */ static pthread_mutex_t link_lock = PTHREAD_MUTEX_INITIALIZER; static struct link link_read; static int link_ok; @@ -128,7 +116,6 @@ static int load_link(struct link *link) { return ok; } -/* See drm.c: a fork must not leave the lock held by a thread the child does not have. */ static void link_lock_before_fork(void) { pthread_mutex_lock(&link_lock); } static void link_unlock_after_fork(void) { pthread_mutex_unlock(&link_lock); } static void link_reset_after_fork(void) { pthread_mutex_init(&link_lock, NULL); } @@ -137,8 +124,6 @@ __attribute__((constructor)) static void install_link_fork_handlers(void) { pthread_atfork(link_lock_before_fork, link_unlock_after_fork, link_reset_after_fork); } -/* When the kernel's own list was of no use, it is not asked again for a second: each asking binds - * a netlink socket, a stop in the tracer, only to be refused again. */ static long long ifaddrs_unusable_at, nameindex_unusable_at; static void prefix_mask(int family, unsigned prefix, unsigned char *out) { @@ -152,8 +137,6 @@ static int is_link_local(const struct link_addr *addr) { return addr->family == AF_INET6 && addr->bytes[0] == 0xfe && (addr->bytes[1] & 0xc0) == 0x80; } -/* One allocation per list. ifa_data of the head points at the head itself, which glibc never - * does, so freeifaddrs() can tell the two kinds of list apart without keeping any state. */ struct entry { struct ifaddrs ifa; struct sockaddr_storage addr, mask, extra; @@ -246,7 +229,6 @@ int getifaddrs(struct ifaddrs **out) { if (asked) __atomic_store_n(&ifaddrs_unusable_at, now_ns(), __ATOMIC_RELAXED); if (rc == 0 && list) real_freeifaddrs(list); - /* A leading slot keeps fill_entry()'s back link in bounds; the list starts after it. */ struct entry *block = calloc((size_t) link.addr_count + 5, sizeof(*block)); if (block == NULL) { errno = ENOMEM; @@ -277,7 +259,6 @@ void freeifaddrs(struct ifaddrs *list) { real_freeifaddrs(list); } -/* Names live in the same allocation as the array, right behind it; glibc strdup()s each one. */ struct name_block { struct if_nameindex index[3]; char names[2][IFNAMSIZ]; @@ -353,7 +334,6 @@ char *if_indextoname(unsigned int index, char name[IF_NAMESIZE]) { static int mac_unusable(const struct ifreq *ifr) { const unsigned char *a = (const unsigned char *) ifr->ifr_hwaddr.sa_data; if (ifr->ifr_hwaddr.sa_family != ARPHRD_ETHER) return 1; - /* Android hands out 02:00:00:00:00:00 where it hides the real address. */ int rest = a[1] | a[2] | a[3] | a[4] | a[5]; return (a[0] & 0x03) || (rest == 0 && (a[0] == 0 || a[0] == 0x02)); } @@ -420,15 +400,13 @@ static int is_link_query(unsigned long request) { || request == SIOCGIFBRDADDR; } -void bl_kgsl_chip_id_fixup(unsigned long request, void *arg, int rc); /* kgslid.c */ +void bl_kgsl_chip_id_fixup(unsigned long request, void *arg, int rc); #include "../../quest/kgsl_compat.h" int bl_kgsl_poll(int (*real)(int, unsigned long, void *), int fd, unsigned long request, void *arg, int *rc) __attribute__((visibility("hidden"))); int bl_ntsync_ioctl(int fd, unsigned long request, void *arg, int *rc) __attribute__((visibility("hidden"))); int bl_drm_gem_close(int fd, unsigned long request, void *arg, int *rc) __attribute__((visibility("hidden"))); -/* Every DRM and evdev call comes through here, so the lookup is done once. Two threads racing - * to store it store the same pointer. */ int ioctl(int fd, unsigned long request, ...) { typedef int (*ioctl_fn)(int, unsigned long, void *); static ioctl_fn real; @@ -538,7 +516,6 @@ static const char *denied_table(const char *path) { return NULL; } -/* A memfd rather than fmemopen(): the stream owns its contents, so fclose() frees everything. */ static FILE *synthesize(const char *table) { struct link link; if (!load_link(&link)) return NULL; @@ -558,7 +535,6 @@ static FILE *synthesize(const char *table) { return out; } -/* tracer.c */ int bl_status_without_tracer(const char *path, int flags) __attribute__((visibility("hidden"))); static FILE *open_stream(fopen_fn real, const char *path, const char *mode) { diff --git a/tools/linuxfs/preload/noexec.c b/tools/linuxfs/preload/noexec.c index 26aa539b7..0b0326bda 100644 --- a/tools/linuxfs/preload/noexec.c +++ b/tools/linuxfs/preload/noexec.c @@ -1,9 +1,3 @@ -/* - * noexec.c: FEX runs an x86 program and its libraries where they lie, on noexec shared storage too. - * FEX maps them with the guest's PROT_EXEC, which the kernel refuses for a file on a noexec mount, - * but it never runs those pages itself - it runs its own translation of them - so a mapping or an - * mprotect refused for that alone is made without PROT_EXEC. Only in FEX's own processes. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -12,7 +6,6 @@ #include <sys/statvfs.h> #include <unistd.h> -/* NOEXEC_TEST: tools/tests builds it for the host. */ #if defined(__aarch64__) || defined(NOEXEC_TEST) static int in_fex(void) { @@ -28,7 +21,6 @@ static int in_fex(void) { return known; } -/* After an mmap failed (fsync.c's mmap asks): whether to map it again without PROT_EXEC. */ __attribute__((visibility("hidden"))) int bl_noexec_retry(int prot, int flags, int fd) { int error = errno; struct statvfs vfs; @@ -40,7 +32,6 @@ __attribute__((visibility("hidden"))) int bl_noexec_retry(int prot, int flags, i static int (*real_mprotect)(void *, size_t, int); -/* Looked up before anything runs: Wine calls mprotect from its signal handlers, where dlsym is unsafe. */ __attribute__((constructor)) static void noexec_init(void) { real_mprotect = (int (*)(void *, size_t, int))dlsym(RTLD_NEXT, "mprotect"); } diff --git a/tools/linuxfs/preload/ntsync.c b/tools/linuxfs/preload/ntsync.c index dc21b117f..69f40a145 100644 --- a/tools/linuxfs/preload/ntsync.c +++ b/tools/linuxfs/preload/ntsync.c @@ -1,5 +1,3 @@ -/* droiddeck-ntsync: a userspace implementation of the /dev/ntsync interface for kernels without the driver. - * Credit: ntsync, the kernel driver and its Wine client, by Elizabeth Figura. */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -1243,7 +1241,6 @@ int bl_udevmon_recvmsg(int fd, struct msghdr *msg, int flags, ssize_t (*real_rec ssize_t recvmsg(int fd, struct msghdr *msg, int flags) { static ssize_t (*real)(int, struct msghdr *, int); if (!real) real = (ssize_t (*)(int, struct msghdr *, int))dlsym(RTLD_NEXT, "recvmsg"); - /* udevmon.c's stand-in netlink sockets first: their messages are dressed as udevd's. */ ssize_t answered; if (msg && bl_udevmon_recvmsg(fd, msg, flags, real, &answered)) return answered; ssize_t r = real(fd, msg, flags); diff --git a/tools/linuxfs/preload/opens.c b/tools/linuxfs/preload/opens.c index 2e4a687e8..7c0e766d2 100644 --- a/tools/linuxfs/preload/opens.c +++ b/tools/linuxfs/preload/opens.c @@ -1,29 +1,7 @@ /* - * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0, with names - * changed to this project's. The reasoning below is the original author's. - * - * The adjustments a session's opens need. - * - * O_NOATIME on the game library: the user's games are bound in from shared storage, which Android - * serves over FUSE with every file owned by its media provider rather than by the app. O_NOATIME - * is reserved for a file's owner, so the kernel refuses such an open outright with EPERM. The - * Steam client opens every content file it reads that way, and treats the refusal as the file - * being unreadable or missing: a validation reports corrupt or missing files and an update is - * cancelled. The flag only asks the filesystem to skip an access-time update; the open is retried - * without it. Every other refusal is passed through untouched, and a file the process may open - * with the flag still is. - * - * The kernel's trace marker: tracefs is world-writable here, so the client's tier0 opens - * /sys/kernel/tracing/trace_marker at startup - three times over, once per module that profiles - - * and writes a "[%s] tgid=%d begin_ctx=%lld" line for every scope it enters. Nothing in the - * session reads those markers, and they are one more thing that can land in the wrong file when - * a descriptor number changes hands (tracer.c has the cause). They have been found appended to an - * app manifest, which the client then cannot parse, to a download's state file, and to content - * chunks, which it quarantines as corrupt. Refused the descriptor, tier0 writes nothing at all and - * has nothing to lose. - * - * A process status is served without the container's tracer in it; see tracer.c. + * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0. */ + #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -55,7 +33,6 @@ static int retry_without_noatime(int fd, int flags) { return fd < 0 && errno == EPERM && (flags & O_NOATIME); } -/* Both spellings tier0 tries, the debugfs one first. */ static int is_trace_marker(const char *path) { return path != NULL && (strcmp(path, "/sys/kernel/tracing/trace_marker") == 0 @@ -83,7 +60,6 @@ int open(const char *path, int flags, ...) { } fd = bl_dircache_create(AT_FDCWD, path, flags, mode, via_open); if (retry_without_noatime(fd, flags)) fd = real_open(path, flags & ~O_NOATIME, mode); - /* A read-only file the Steam client rewrites (perms.c). */ if (fd < 0 && bl_writable_retry(AT_FDCWD, path, flags)) fd = real_open(path, flags & ~O_NOATIME, mode); return fd; } @@ -113,6 +89,5 @@ int openat(int dirfd, const char *path, int flags, ...) { return fd; } -/* The runtime is 64-bit, so the large-file names are the same calls. */ int open64(const char *path, int flags, ...) __attribute__((alias("open"))); int openat64(int dirfd, const char *path, int flags, ...) __attribute__((alias("openat"))); diff --git a/tools/linuxfs/preload/pathcache.c b/tools/linuxfs/preload/pathcache.c index 0aab91bd7..c183eba84 100644 --- a/tools/linuxfs/preload/pathcache.c +++ b/tools/linuxfs/preload/pathcache.c @@ -1,18 +1,3 @@ -/* - * Path lookups the Steam client repeats, answered from a short-lived cache. - * - * While it registers its compatibility tools after logon, the client resolves the same few - * directories - its local tools under compatibilitytools.d and its own platform directory - with - * realpath() and access() thousands of times a second. Each realpath() is one readlinkat() per - * path component, and under proot every one of those is a trapped syscall with a round trip - * through the tracer. Measured on an AYN Thor, that loop was 23 of the client's 31 seconds of - * startup, with proot busy for 16 of them. - * - * Only the client itself uses the cache, never a game or a helper. Only successful answers are - * kept, and only for a moment: a path that stops existing is noticed within PATH_CACHE_TTL_NS, and - * one that starts existing is never hidden, since failures always go to the filesystem. - * BL_NO_PATH_CACHE=1 turns it off. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -152,7 +137,6 @@ static int (*next_faccessat(void))(int, const char *, int, int) { return real; } -/* binfmt.c: a program on noexec storage, which the desktop runs all the same. */ __attribute__((visibility("hidden"))) int bl_binfmt_runnable(const char *path, int mode); static int check_cwd(const char *path, int mode, int flags) { diff --git a/tools/linuxfs/preload/perms.c b/tools/linuxfs/preload/perms.c index 96f1540c2..4ca79d3d4 100644 --- a/tools/linuxfs/preload/perms.c +++ b/tools/linuxfs/preload/perms.c @@ -1,20 +1,3 @@ -/* - * Root's way past a read-only file, for the Steam client. - * - * The client runs as "root" inside the session - the rootfs names the app's own uid root, and - * proot (-i) shows it that uid - but the kernel treats it as the ordinary user it is. The client - * leaves many content files read-only (r-x------, - * ~290 files of Proton Experimental (ARM64) alone), and when an update later rewrites one of them - * it opens the file for writing expecting root's permission to override the mode. The kernel - * refuses (EACCES), the client reports "Missing file permissions" and cancels the update - * (2026-09-26: Proton Experimental (ARM64), "CFileWriter: errno: 13 ... wineopenxr.dll"). - * - * So for the client's own processes, a refused write-open of a file this user owns is answered the - * way root's would be: the owner's write bit is added - to the file, or to the directory a new - * file is being created in - and the open is tried once more. chmod only succeeds on what the app - * owns, so nothing it could not already change is touched. Games, Wine and everything else keep - * ordinary permissions: only the processes started from the client's own directories take this. - */ #define _GNU_SOURCE #include <errno.h> #include <fcntl.h> @@ -28,11 +11,6 @@ static int in_steam_dirs(const char *path) { || strstr(path, "/Steam/ubuntu12_64/") != NULL || strstr(path, "/Steam/linuxarm64/") != NULL); } -/* - * The client and its helpers run from the Steam root's platform directories. The kernel's - * /proc/self/exe names proot's loader for every process in the session, so the program's own - * name - the path it was started by - is asked as well. - */ static int is_steam_client(void) { static int cached = -1; char exe[PATH_MAX]; @@ -52,10 +30,6 @@ static int grant_owner_write(int dirfd, const char *path) { return fchmodat(dirfd, path, (st.st_mode & 07777) | S_IWUSR, 0) == 0; } -/* - * After an open refused with EACCES: returns 1 when a write bit was added and the open is worth - * one more try. errno is left as the open set it. - */ __attribute__((visibility("hidden"))) int bl_writable_retry(int dirfd, const char *path, int flags) { int saved = errno; @@ -67,7 +41,6 @@ int bl_writable_retry(int dirfd, const char *path, int flags) { if (faccessat(dirfd, path, F_OK, 0) == 0) { granted = grant_owner_write(dirfd, path); } else if (flags & O_CREAT) { - /* A new file in a read-only directory. */ slash = strrchr(path, '/'); if (slash == NULL) { granted = grant_owner_write(dirfd, "."); diff --git a/tools/linuxfs/preload/quest_kgsl.c b/tools/linuxfs/preload/quest_kgsl.c index 09037fafe..da5ef79fd 100644 --- a/tools/linuxfs/preload/quest_kgsl.c +++ b/tools/linuxfs/preload/quest_kgsl.c @@ -1,3 +1,2 @@ /* SPDX-License-Identifier: GPL-3.0-or-later */ -/* Shared with the Android driver wrapper so the two translation paths cannot drift. */ #include "../../quest/kgsl_compat.c" diff --git a/tools/linuxfs/preload/robust.c b/tools/linuxfs/preload/robust.c index 97bb9eec9..6bbb6f91e 100644 --- a/tools/linuxfs/preload/robust.c +++ b/tools/linuxfs/preload/robust.c @@ -1,11 +1,3 @@ -/* - * get_robust_list(2) for the session's own threads. - * - * Android's app policy refuses the call, and Steam's cross-process mutex insists on seeing the - * head glibc registered. The kernel's list protocol gives it away: a robust mutex this thread - * holds is linked directly behind the registered head, so locking a private one and following - * the link back finds the head without depending on libc's layout. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -48,8 +40,6 @@ static struct robust_list_head *thread_robust_head(void) { if (pthread_mutex_lock(&mutex) != 0) { return NULL; } - /* The mutex holds tids and lock words beside the link, so a word is only followed - * through a read that fails instead of faulting on a non-pointer. */ uintptr_t lo = (uintptr_t)&mutex, hi = lo + sizeof(mutex); for (uintptr_t p = lo; p + sizeof(void *) <= hi && !head; p += sizeof(void *)) { struct robust_list_head *candidate = *(struct robust_list_head **)p; diff --git a/tools/linuxfs/preload/space.c b/tools/linuxfs/preload/space.c index 5edc0bd35..950197f65 100644 --- a/tools/linuxfs/preload/space.c +++ b/tools/linuxfs/preload/space.c @@ -1,23 +1,3 @@ -/* - * The free space a library reports. - * - * A library's games can live somewhere the folder that names the library does not. The card - * library is exactly that: its steamapps/common is bound to the card, but the directory above it - * belongs to the runtime image in the phone's own storage, and that directory is what the client - * measures. So it offers both install locations with the phone's free space against each, and the - * card's real figure - seventeen gigabytes more, on the device this was found on - never appears. - * - * The number is not only cosmetic: the client refuses an install it believes will not fit. Once - * the phone fills up it would turn down a card install with room to spare, and say nothing about - * why. - * - * Moving the whole library onto the card would fix the number and break something worse. The - * prefixes under steamapps/compatdata want symlinks and file locks, and the card is served over - * FUSE, which gives us neither - that is why they are kept in the runtime image in the first - * place. So the question is answered where the content actually is: a query about a library root - * named in BL_LIBRARY_SPACE is served from its steamapps/common, the bind that points at the - * card. Every other path is passed through untouched. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <stddef.h> @@ -26,8 +6,6 @@ #include <sys/statfs.h> #include <sys/statvfs.h> -/* A library root, or its steamapps, resolved to the directory that holds the games. Returns the - * rewritten path in `buf`, or NULL when `path` is not one of ours and should be left alone. */ static const char *content_of(const char *path, char *buf, size_t len) { const char *roots = getenv("BL_LIBRARY_SPACE"); if (!path || !roots || !*roots) @@ -38,8 +16,6 @@ static const char *content_of(const char *path, char *buf, size_t len) { const char *end = strchr(roots, ':'); size_t root_len = end ? (size_t)(end - roots) : strlen(roots); if (root_len > 0) { - /* The root itself, or the steamapps directly beneath it. Anything deeper is already inside - * a bind and answers for itself. */ int match = (path_len == root_len && !strncmp(path, roots, root_len)) || (path_len == root_len + 10 && !strncmp(path, roots, root_len) && !strcmp(path + root_len, "/steamapps")); diff --git a/tools/linuxfs/preload/storage.c b/tools/linuxfs/preload/storage.c index 201f93cd4..4b9c44e66 100644 --- a/tools/linuxfs/preload/storage.c +++ b/tools/linuxfs/preload/storage.c @@ -1,13 +1,3 @@ -/* - * Opt-in observations for file allocation and writes to the selected Steam library. - * - * The session enables them only when storage diagnostics are turned on. Counters are local to - * each process, allocation details are capped, writes are sampled, and storage.log has a global - * size limit because every Steam child shares it. - * - * LD_PRELOAD cannot see direct syscalls or libc-internal calls that bypass public symbols. A - * missing entry therefore does not prove the operation did not happen. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -126,7 +116,6 @@ static void append_line(const char *line, size_t length) { syscall(SYS_close, (long)fd, 0L, 0L, 0L, 0L, 0L); goto done; } - /* One small append write per complete line; no per-write log flood. */ (void)syscall(SYS_write, (long)fd, (long)line, (long)length, 0L, 0L, 0L); syscall(SYS_close, (long)fd, 0L, 0L, 0L, 0L, 0L); done: @@ -223,7 +212,7 @@ int posix_fallocate(int fd, off_t offset, off_t length) { int call_errno = errno; uint64_t elapsed = now_ns() - start; allocation_line("posix_fallocate", result, call_errno, 0, offset, length, elapsed); - errno = entry_errno; /* POSIX requires the error to be returned, leaving errno unchanged. */ + errno = entry_errno; return result; } @@ -262,8 +251,6 @@ int ftruncate(int fd, off_t length) { int known = 0; if (steam_client()) { if (next_fstat == NULL) next_fstat = dlsym(RTLD_NEXT, "fstat"); - /* staging_file() reads /proc/self/fd, a round trip through proot: skip it for a shrink, which - * is passed through untouched either way. */ known = next_fstat != NULL && next_fstat(fd, &st) == 0 && S_ISREG(st.st_mode) && length >= st.st_size && staging_file(fd); } diff --git a/tools/linuxfs/preload/sysv.c b/tools/linuxfs/preload/sysv.c index 5c1416b2b..dc349c232 100644 --- a/tools/linuxfs/preload/sysv.c +++ b/tools/linuxfs/preload/sysv.c @@ -1,14 +1,3 @@ -/* System V semaphores and shared memory for programs in the Linux runtime. - * - * Android kernels are built without System V IPC, so semget/shmget fail with ENOSYS. Steam's tier0 - * creates its client-instance semaphore with semget and gives up when that fails. This library is - * LD_PRELOADed into the session and keeps each object in a file under /dev/shm (a bound directory - * the whole session shares): a semaphore set is an array of counters waited on with futexes, a - * segment is a plain file that shmat maps. Objects are found by the inode of their file, which is - * what the id is. semop applies its operations one by one (no all-or-nothing across a set) and - * SEM_UNDO is accepted but not performed - enough for the client, which uses single-counter sets. - * A message queue is a table of slots in its file: gamescope hands mangoapp (Deck mode's performance - * overlay) each frame's timings through one. */ #define _GNU_SOURCE #include <dirent.h> #include <errno.h> @@ -34,7 +23,7 @@ #include <unistd.h> #define DIR_DEFAULT "/dev/shm/wnsysv" -#define SEM_MAGIC 0x574e5345u /* "WNSE" */ +#define SEM_MAGIC 0x574e5345u #define MAX_OBJECTS 64 struct sem_header { @@ -43,15 +32,11 @@ struct sem_header { uint32_t key; uint32_t mode; int32_t creator_pid; - /* What IPC_STAT reports as sem_otime / sem_ctime (seconds). The usual race-free init has the - * creator semop the new set once, and every other process wait until sem_otime is non-zero; - * SteamVR's web helper spins on exactly that. */ _Atomic uint32_t otime; _Atomic uint32_t ctime; int32_t pad[9]; }; _Static_assert(sizeof(struct sem_header) == 64, "semaphore files written by older sessions keep their layout"); -/* The counters follow the header, one 32-bit word each (a futex word). */ struct sem_map { int id; struct sem_header *hdr; size_t len; }; struct shm_map { void *addr; size_t len; int id; }; @@ -76,7 +61,6 @@ static int object_id(int fd) { return (int)(st.st_ino & 0x7fffffff); } -/* Open the object file of kind ("sem"/"shm") for key, creating it as flags ask. */ static int open_object(const char *kind, key_t key, int flags, int *created) { char path[PATH_MAX]; int oflags = O_RDWR | O_CLOEXEC; @@ -98,7 +82,6 @@ static int open_object(const char *kind, key_t key, int flags, int *created) { return fd; } -/* The object file for an id, found by inode. */ static int open_by_id(const char *kind, int id) { DIR *d = opendir(object_dir()); if (!d) return -1; @@ -141,7 +124,6 @@ static long futex(void *addr, int op, int val, const struct timespec *timeout) { return syscall(SYS_futex, addr, op, val, timeout, NULL, 0); } -/* ------------------------------------------------------------------ semaphores */ static struct sem_map *sem_lookup(int id) { for (int i = 0; i < MAX_OBJECTS; i++) @@ -227,7 +209,7 @@ int semtimedop(int semid, struct sembuf *sops, size_t nsops, const struct timesp } else if (sem_wait_op(w, sops[i].sem_op, sops[i].sem_flg, timeout) != 0) { return -1; } - if (sops[i].sem_op < 0) futex(w, FUTEX_WAKE, INT_MAX, NULL); /* zero-waiters see the change */ + if (sops[i].sem_op < 0) futex(w, FUTEX_WAKE, INT_MAX, NULL); } atomic_store(&h->otime, (uint32_t)time(NULL)); return 0; @@ -293,11 +275,7 @@ int semctl(int semid, int semnum, int cmd, ...) { } } -/* ------------------------------------------------------------------ shared memory */ -/* Linux keeps a removed segment attachable until the last detach, which X clients rely on: they - * remove the segment right after asking the server to attach it. A removed segment is renamed out - * of key lookup instead, and removed for good once it has been out of use for a while. */ static int remove_shm(int id) { DIR *d = opendir(object_dir()); if (!d) return -1; @@ -411,18 +389,18 @@ int shmctl(int shmid, int cmd, struct shmid_ds *buf) { } } -/* ------------------------------------------------------------------ message queues */ -/* Only what gamescope and mangoapp use: msgtyp 0 or > 0, IPC_NOWAIT and IPC_RMID. */ -#define MSG_MAGIC 0x574e4d51u /* "WNMQ" */ + + +#define MSG_MAGIC 0x574e4d51u #define MSG_SLOTS 32 struct msg_queue { uint32_t magic; - _Atomic int32_t lock; /* 0 free, else the holder's pid */ - _Atomic int32_t changed; /* bumped on every send and receive; waiters sleep on it */ + _Atomic int32_t lock; + _Atomic int32_t changed; uint64_t next_seq; - struct { uint64_t seq; long mtype; size_t len; char data[2048]; } slot[MSG_SLOTS]; /* seq 0: free */ + struct { uint64_t seq; long mtype; size_t len; char data[2048]; } slot[MSG_SLOTS]; }; static struct msg_queue *msg_map(int id) { @@ -437,7 +415,6 @@ static struct msg_queue *msg_map(int id) { return NULL; } -/* A process killed while holding the lock (Android ends sessions without warning) loses it. */ static void msg_lock(struct msg_queue *q) { for (int32_t owner = 0; !atomic_compare_exchange_strong(&q->lock, &owner, getpid()); owner = 0) { if (kill(owner, 0) != 0 && errno == ESRCH) atomic_compare_exchange_strong(&q->lock, &owner, 0); @@ -531,12 +508,6 @@ int msgctl(int msqid, int cmd, struct msqid_ds *buf) { return rc; } -/* - * fork() carries over only the calling thread, so a lock another thread was holding at that - * instant stays held in the child by a thread that is not there to release it. Taking it before - * the fork makes the copy consistent; the parent then unlocks it and the child, whose one thread - * never locked it, gets a fresh one. (WinNative 79aa7f68.) - */ static void sysv_lock_before_fork(void) { pthread_mutex_lock(&g_lock); } static void sysv_unlock_after_fork(void) { pthread_mutex_unlock(&g_lock); } static void sysv_reset_after_fork(void) { pthread_mutex_init(&g_lock, NULL); } diff --git a/tools/linuxfs/preload/tracefs.c b/tools/linuxfs/preload/tracefs.c index 17d45a01a..8c59f2963 100644 --- a/tools/linuxfs/preload/tracefs.c +++ b/tools/linuxfs/preload/tracefs.c @@ -1,15 +1,3 @@ -/* - * A trace instance for mangoapp where the kernel's tracefs is closed to the app. - * - * Valve's mangoapp (Deck mode's performance overlay) reads the GPU memory total on a Qualcomm - * display driver (msm_dpu / msm_drm) from the gpu_mem_total event, through a tracefs instance of - * its own - and aborts when tracefs_instance_create() fails, taking the whole overlay with it. - * Android mounts tracefs for root and the readtracefs group only, so it always fails here. - * - * The real calls run first. Only when they fail is the instance placed in an empty directory of - * the session's own, and reading its events answers with one gpu_mem_total record carrying the - * total KGSL - the GPU driver of every Adreno under Android - reports to the app. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <limits.h> @@ -25,7 +13,6 @@ struct tracefs_instance; struct tep_handle; struct tep_event; -/* The head of libtraceevent 1.8's struct tep_record (the version tools/mangoapp/packages.txt pins). */ struct tep_record { unsigned long long ts, offset; long long missed_events; @@ -35,7 +22,6 @@ struct tep_record { void *priv; }; -/* The event as Android kernels declare it (trace/events/gpu_mem.h). */ static const char gpu_mem_total_format[] = "name: gpu_mem_total\n" "ID: 1\n" diff --git a/tools/linuxfs/preload/tracer.c b/tools/linuxfs/preload/tracer.c index 326a8f353..f713cc254 100644 --- a/tools/linuxfs/preload/tracer.c +++ b/tools/linuxfs/preload/tracer.c @@ -1,21 +1,7 @@ /* - * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0, with names - * changed to this project's. The reasoning below is the original author's. - * - * A process status without the container's tracer in it. - * - * proot runs the session under ptrace, so every process here reads a TracerPid in its - * /proc/<pid>/status. The Steam client takes that to mean a debugger is attached, and its arm64 - * build breaks into the supposed debugger with a bare "svc #0" after every failed assertion. That - * is no breakpoint: the registers still hold the call Plat_IsInDebugSession() just made, close(), - * with its own result, 1, as the argument - so each assertion closes descriptor 1. The number then - * goes to whatever the client opens next, and its standard output is written into that: app - * manifests and content chunks, its own connections, and the socket a game's Steam API talks - * over, which is how a game that stayed silent while loading found its pipe dead. - * - * The tracer is part of the container, not something a program should react to, so the status - * files are served with TracerPid 0. + * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0. */ + #define _GNU_SOURCE #include <ctype.h> #include <errno.h> @@ -34,7 +20,6 @@ static const char *skip_digits(const char *s) { return end == s ? NULL : end; } -/* /proc/{self,thread-self,<pid>}[/task/<tid>]/status */ static int is_status_path(const char *path) { const char *rest; if (path == NULL || strncmp(path, "/proc/", 6) != 0) return 0; @@ -46,9 +31,6 @@ static int is_status_path(const char *path) { return strcmp(rest, "/status") == 0; } -/* Chromium stands its hang watchdogs down when it sees a tracer, and the client's web helper does - * not come up within their patience here: it is left reading the truth. SteamVR's web helper (the - * dashboard, Cardboard mode's SteamVR) is the same Chromium and stalls the same way. */ static int is_web_helper(void) { return strcmp(program_invocation_short_name, "steamwebhelper") == 0 || strcmp(program_invocation_short_name, "vrwebhelper") == 0; @@ -65,8 +47,6 @@ static int write_all(int fd, const char *data, size_t len) { return 0; } -/* The descriptor to hand back for a read-only open of a status file, or -1 with errno untouched - * when the path is something else or the copy cannot be made, and the real open should run. */ __attribute__((visibility("hidden"))) int bl_status_without_tracer(const char *path, int flags) { char text[STATUS_MAX]; size_t len = 0; diff --git a/tools/linuxfs/preload/udevmon.c b/tools/linuxfs/preload/udevmon.c index 86baed110..33baf2bda 100644 --- a/tools/linuxfs/preload/udevmon.c +++ b/tools/linuxfs/preload/udevmon.c @@ -1,29 +1,7 @@ /* - * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0, with the - * symbol prefix changed to match this library. The reasoning below is the original author's. - * - * udev's netlink monitor, stood in for where the sandbox refuses the socket it is built on. - * - * The app sandbox denies a NETLINK_KOBJECT_UEVENT socket outright. libudev cannot create a - * monitor without one, and SDL's HID layer cannot start without a monitor: every SDL_hid_init() - * fails, the Steam client's controller code retries it hundreds of times a second, its main loop - * stalls, and the client asserts out from under the running game. Refusing SDL the library - * altogether (the earlier answer here) is what produced that failure. - * - * So the socket is answered instead: one end of a datagram socketpair stands in for the netlink - * socket, its peer held open so it never reads as hung up, and the few calls libudev makes on - * the socket that only a netlink one could answer are answered here. Nothing is ever written to - * it, so the monitor is live and silent, which is the most the kernel would deliver in a - * container anyway. The real socket is tried first, so this stays out of the way wherever a - * monitor could genuinely be created. - * - * The stand-in can also carry events after all (bl_udevmon_inject, below): libfakeinput plugs - * Steam's touch controller in and out of the client that way. - * - * Wine is left without one. Its HID bus reads the session's pads itself only when its SDL bus - * cannot start, and that is the path inputudev.c describes them on. Given a monitor the SDL bus - * comes up and takes the pads over from it. + * Ported from WinNative (maxjivi05, feature/wayland-gamescope, 2026-09-18/19), GPL-3.0. */ + #define _GNU_SOURCE #include <dlfcn.h> #include <errno.h> @@ -39,9 +17,9 @@ #include <unistd.h> struct stand_in { - int fd; /* the descriptor handed out, -1 when the slot is free */ - int peer; /* the other end, kept open */ - ino_t inode; /* identifies the socket after fd is closed and its number reused */ + int fd; + int peer; + ino_t inode; }; static struct stand_in stand_ins[16]; @@ -62,8 +40,6 @@ static ino_t socket_inode(int fd) { return fstat(fd, &st) == 0 ? st.st_ino : 0; } -/* Whether fd is one of the stand-in sockets. A slot whose descriptor was closed and reused by - * something else is retired on sight, so a later socket with the same number is not mistaken. */ static int is_stand_in(int fd) { int found = 0; if (fd < 0) @@ -108,7 +84,6 @@ static int make_stand_in(int type) { errno = saved; return -1; } - /* The peer is ours alone: it must not follow the socket into a child. */ fcntl(sv[1], F_SETFD, FD_CLOEXEC); pthread_mutex_lock(&lock); for (size_t i = 0; i < sizeof(stand_ins) / sizeof(stand_ins[0]); i++) { @@ -147,7 +122,6 @@ int socket(int domain, int type, int protocol) { return make_stand_in(type); } -/* For net.c's bind(): whether fd is a stand-in, which accepts any netlink address as bound. */ __attribute__((visibility("hidden"))) int bl_udevmon_stand_in(int fd) { return is_stand_in(fd); } @@ -180,8 +154,6 @@ int setsockopt(int fd, int level, int optname, const void *optval, socklen_t opt static int (*real_setsockopt)(int, int, int, const void *, socklen_t); if (is_stand_in(fd)) { - /* Group membership and packet filters only mean something on a socket that receives; this - * one never does, so they are accepted as set. The rest is genuine on a unix socket. */ if (level == SOL_NETLINK) return 0; if (level == SOL_SOCKET && (optname == SO_ATTACH_FILTER || optname == SO_DETACH_FILTER)) @@ -195,8 +167,6 @@ int setsockopt(int fd, int level, int optname, const void *optval, socklen_t opt return real_setsockopt(fd, level, optname, optval, optlen); } -/* udevd's message to the "udev" multicast group (systemd, device-monitor.c): this header, then the - * properties as NUL-separated KEY=VALUE strings. */ struct monitor_netlink_header { char prefix[8]; unsigned magic; @@ -209,7 +179,6 @@ struct monitor_netlink_header { unsigned filter_tag_bloom_lo; }; -/* systemd's string_hash32: MurmurHash2, seed 0. */ static unsigned murmur_hash2(const char *key, size_t len) { const unsigned m = 0x5bd1e995; unsigned h = (unsigned)len; @@ -236,8 +205,6 @@ static unsigned murmur_hash2(const char *key, size_t len) { return h; } -/* Sends a device event to every stand-in in this process, as udevd would; returns how many got it. - * For libfakeinput (found with dlsym), which knows when its devices come and go. */ __attribute__((visibility("default"))) int bl_udevmon_inject(const char *action, const char *devpath, const char *subsystem, const char *devname, unsigned major, unsigned minor) { @@ -247,7 +214,7 @@ __attribute__((visibility("default"))) int bl_udevmon_inject(const char *action, action, 0, devpath, 0, subsystem, 0, devname, 0, major, 0, minor, 0, __atomic_add_fetch(&seqnum, 1, __ATOMIC_RELAXED)); if (used <= 0 || (size_t)used >= sizeof(props)) return -1; - used++; /* the last string's NUL */ + used++; struct monitor_netlink_header header; memset(&header, 0, sizeof(header)); memcpy(header.prefix, "libudev", 8); @@ -269,10 +236,6 @@ __attribute__((visibility("default"))) int bl_udevmon_inject(const char *action, return sent; } -/* For ntsync.c's recvmsg(), which owns the symbol: 1 when fd is a stand-in, answered here (*result - * is what recvmsg returns), 0 otherwise. A message is dressed as udevd's: libudev drops one that - * does not come from the udev multicast group with root credentials. The sender address goes in - * the room msg_namelen had before the call; a unix socket sets it to 0. */ __attribute__((visibility("hidden"))) int bl_udevmon_recvmsg(int fd, struct msghdr *msg, int flags, ssize_t (*real_recvmsg)(int, struct msghdr *, int), ssize_t *result) { @@ -286,7 +249,7 @@ __attribute__((visibility("hidden"))) int bl_udevmon_recvmsg(int fd, struct msgh struct sockaddr_nl nl; memset(&nl, 0, sizeof(nl)); nl.nl_family = AF_NETLINK; - nl.nl_groups = 2; /* udev's group */ + nl.nl_groups = 2; memcpy(msg->msg_name, &nl, sizeof(nl)); msg->msg_namelen = sizeof(nl); } diff --git a/tools/linuxfs/preload/xmode.c b/tools/linuxfs/preload/xmode.c index 8579f70bc..e6842d73a 100644 --- a/tools/linuxfs/preload/xmode.c +++ b/tools/linuxfs/preload/xmode.c @@ -1,26 +1,9 @@ -/* - * The Steam client's own screen, in a Follow screen session. - * - * With two Xwayland servers the client sizes them through GAMESCOPE_XWAYLAND_MODE_CONTROL - * ({server, width, height, allow_super_res} on the root window): the games' server (1) to a game's - * Game Resolution, and its own (0) to what it recommends for the display - 1920x1080 on a Fold's - * 2448x1848 inner panel. From then on it reads that 16:9 size back as the display's "native" one, - * so its interface and every game it starts are 16:9 with bars on a near-square panel, and stay so - * when the phone folds onto the cover screen. - * - * gamescope already keeps server 0 at the output's size and resizes it when the output changes (a - * Steam Deck docking; here, the app resizing the output as the phone folds and unfolds). So in a - * Follow screen session (BL_FOLLOW_OUTPUT=1) a request for server 0 is dropped and the client's - * screen stays the panel's; requests for the games' servers pass through untouched. Any other - * session, and any other property, is passed straight on. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -/* Xlib's types, without needing its headers in the build image. */ typedef struct _XDisplay Display; typedef unsigned long Atom; typedef unsigned long Window; @@ -48,12 +31,10 @@ int XChangeProperty(Display *dpy, Window w, Atom property, Atom type, int format static Display *atom_dpy; static Atom mode_control; if (!intern) intern = (intern_atom_fn)dlsym(RTLD_NEXT, "XInternAtom"); - /* Atoms belong to a connection; the client's is the one that writes this. */ if (intern && atom_dpy != dpy) { mode_control = intern(dpy, "GAMESCOPE_XWAYLAND_MODE_CONTROL", 1); atom_dpy = dpy; } - /* Format-32 property data is an array of long in Xlib. */ const long *v = (const long *)data; if (mode_control && property == mode_control && v[0] == 0) { static int said; diff --git a/tools/linuxfs/preload/zram.c b/tools/linuxfs/preload/zram.c index 058b5bd4f..565e0f7dd 100644 --- a/tools/linuxfs/preload/zram.c +++ b/tools/linuxfs/preload/zram.c @@ -11,9 +11,6 @@ #include <sys/uio.h> #include <unistd.h> -/* zRAM for the Steam client while a game runs: on SIGURG the process pages its private anonymous - * memory out (ZR_CTL holds 1) or reads it back (0). Raw syscalls only, as this library wraps - * close(), openat() and syscall(). */ #ifndef MADV_PAGEOUT #define MADV_PAGEOUT 21 @@ -56,8 +53,6 @@ static unsigned long zr_hex(const char **p) { } } -/* Paged out: private writable anonymous memory (the heap, unnamed and [anon:name] mappings). - * Read back: every private readable mapping, as the kernel may have swapped others meanwhile. */ static int zr_eligible(const char *line, int compress, unsigned long *start, unsigned long *end) { const char *p = line; *start = zr_hex(&p); diff --git a/tools/linuxfs/ssbs/ssbs_adapter.c b/tools/linuxfs/ssbs/ssbs_adapter.c index 92b3000f6..a3125f070 100644 --- a/tools/linuxfs/ssbs/ssbs_adapter.c +++ b/tools/linuxfs/ssbs/ssbs_adapter.c @@ -1,30 +1,3 @@ -/* - * libssbs.so - keep PSTATE.SSBS set across signal returns, without rebuilding Wine. - * - * Wine's arm64 signal code writes PSTATE back from a Windows CONTEXT, whose Cpsr never carries - * SSBS (bit 12). Every signal return through that code (exception fixups, NtSetContextThread on - * another thread, the SIGUSR2 suspend path) therefore resumes the thread with SSBS clear, so the - * CPU runs it with speculative store bypass disabled - slower on cores such as Oryon. - * force_ssbs.diff (Sonicadvance1, FEX) fixes this inside ntdll; this library does the same from - * outside, for Protons that cannot be rebuilt: the Valve, GE and CachyOS ARM64 builds DroidDeck - * runs, which come prebuilt from Steam. Same source as Bannerlator's adapter (bionic), built here - * for glibc. - * - * How: interpose sigaction()/signal() via LD_PRELOAD. Every handler is installed behind one - * SA_SIGINFO trampoline that calls the real handler and then sets SSBS in the ucontext it is - * about to return through. The kernel accepts SSBS from userspace on sigreturn and context- - * switches it per thread, so the bit sticks until the next sigreturn. Handlers that never return - * (siglongjmp) are unaffected either way. - * - * Env: - * SSBS_ADAPTER_DISABLE=1 pass everything through untouched. - * SSBS_ADAPTER_OBSERVE=1 wrap and count, but leave SSBS as the handler left it (the baseline). - * SSBS_ADAPTER_DEBUG=1 log to stderr (implied by OBSERVE): - * - "returned with SSBS clear": a handler was about to resume a thread without SSBS. - * - "arrived with SSBS clear": a thread was RUNNING without SSBS when a signal reached it. - * Under the adapter this should stop after each thread's first fixed return; if it keeps - * counting, something other than a signal return is clearing the bit. - */ #define _GNU_SOURCE #include <dlfcn.h> #include <signal.h> @@ -41,7 +14,6 @@ typedef void (*sigact_fn)(int, siginfo_t *, void *); typedef void (*sighand_fn)(int); typedef int (*sigaction_fn)(int, const struct sigaction *, struct sigaction *); -/* Per signal: the handler the program asked for, and whether it wanted SA_SIGINFO. */ static _Atomic(void *) real_handler[MAX_SIG]; static atomic_bool real_siginfo[MAX_SIG]; @@ -69,7 +41,6 @@ __attribute__((constructor)) static void ctor(void) if (debug) fprintf(stderr, "libssbs: loaded (%s)\n", disabled ? "disabled" : observe ? "observing only" : "active"); } -/* 1, 2, 4, 8, ...: enough lines to see a trend without flooding the log. */ static int milestone(unsigned long n) { return (n & (n - 1)) == 0; } static void trampoline(int sig, siginfo_t *info, void *uc_v) @@ -130,7 +101,6 @@ int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact) void *prev_h = atomic_load(&real_handler[sig]); int prev_info = atomic_load(&real_siginfo[sig]); - /* Publish before installing so the trampoline never runs with a stale handler. */ if (act && wrapped(new_h)) { atomic_store(&real_siginfo[sig], new_info); atomic_store_explicit(&real_handler[sig], new_h, memory_order_release); @@ -142,7 +112,6 @@ int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact) atomic_store(&real_handler[sig], prev_h); } - /* Report the caller's own handler, not the trampoline, so handler chaining keeps working. */ if (ret == 0 && oldact && oldact->sa_sigaction == trampoline && prev_h) { if (prev_info) { oldact->sa_sigaction = (sigact_fn)prev_h; @@ -167,7 +136,6 @@ sighandler_t signal(int sig, sighandler_t handler) sighandler_t bsd_signal(int sig, sighandler_t handler) { return signal(sig, handler); } -/* glibc's System V flavour: a one-shot handler with no restart. */ sighandler_t sysv_signal(int sig, sighandler_t handler) { struct sigaction sa, old; diff --git a/tools/mangoapp/libtracefs-shim.c b/tools/mangoapp/libtracefs-shim.c index 58506cada..7095a961f 100644 --- a/tools/mangoapp/libtracefs-shim.c +++ b/tools/mangoapp/libtracefs-shim.c @@ -1,15 +1,3 @@ -// libtracefs.so.1 for mangoapp: the GPU memory Valve's overlay reads, without the kernel's tracing. -// -// Valve's mangoapp (msm.cpp, ftrace_gpumem.c) takes the GPU's memory in use - the VRAM and PVRAM -// lines - from the gpu_mem_total tracepoint (pid 0, the device's total) through libtracefs. An -// Android app may not use tracefs at all: the instance could not be made, and both lines read 0. -// This stands in for the libtracefs calls mangoapp makes, and hands its record walker one -// gpu_mem_total record carrying what the app writes to /run/droiddeck-hud/gpu-mem (bytes; the -// session's GpuMemComponent, from KGSL's own accounting). Only mangoapp loads this copy. -// -// The walker reads libtraceevent's structures directly, so the ones below are libtraceevent -// 1.8.2's (event-parse.h), the version tools/mangoapp/packages.txt pins; tep_find_event_by_record -// and tep_read_number are answered here too, ahead of the real libtraceevent in mangoapp's lookup. #include <sched.h> #include <stddef.h> @@ -76,7 +64,6 @@ struct tep_record { void *priv; }; -// gpu_mem_total's record: gpu_id, pid, size - as the kernel lays it out after the common fields. struct gpu_mem_total { unsigned int gpu_id; unsigned int pid; @@ -90,7 +77,6 @@ static struct tep_format_field pid_field = { static struct tep_event gpu_mem_event = { .name = "gpu_mem_total", .system = "gpu_mem", .format = {.nr_fields = 2, .fields = &pid_field}}; -// Never dereferenced by mangoapp: they only have to be non-null. static char handle; static char instance; diff --git a/tools/mangoapp/mangoapp b/tools/mangoapp/mangoapp index e912be1a8..0a208fef3 100644 --- a/tools/mangoapp/mangoapp +++ b/tools/mangoapp/mangoapp @@ -1,15 +1,5 @@ #!/bin/sh -# gamescope --mangoapp runs `mangoapp` from PATH; this puts Valve's build beside the libraries the -# runtime lacks (fmt 10, spdlog 1.13, glfw, libtracefs, libtraceevent), staged by the app. -# mangoapp reads its window through GLFW's X11 API. GLFW 3.4 takes XDG_SESSION_TYPE=wayland with a -# WAYLAND_DISPLAY as Wayland-only, and unset, libwayland falls back to wayland-0 - DroidDeck's own -# compositor, which shares XDG_RUNTIME_DIR - so both fail; x11 makes GLFW use gamescope's X server. export XDG_SESSION_TYPE=x11 -# gamescope's reaper restarts mangoapp the moment it exits, with no backoff. One that dies at start -# (a stale session preload without the System V queue, a driver it crashes on) was started about -# once a second for the whole session, each start a full exec under proot - the Steam menu itself -# lagged. More than 5 starts in 60 s leaves the overlay off: this waits instead of exiting, so the -# reaper has nothing to restart until the session ends. starts=/tmp/droiddeck-mangoapp-starts now=$(date +%s) recent=$(awk -v now="$now" '$1 > now - 60' "$starts" 2>/dev/null) diff --git a/tools/mangoapp/packages.txt b/tools/mangoapp/packages.txt index 9637e246a..c5131beee 100644 --- a/tools/mangoapp/packages.txt +++ b/tools/mangoapp/packages.txt @@ -1,12 +1,5 @@ -# Valve's aarch64 mangoapp (the Deck performance overlay) and the libraries it needs that the -# runtime does not have, at the versions it was built against (its .BUILDINFO: fmt 10.2.0, -# spdlog 1.13.0). CI fetches each, checks the sha256 and stages the files named below into the apk -# under usr/local/lib/mangoapp; SessionFiles copies them into the runtime at every launch. -# sha256 url 5b203a711e4e48f01fdffaa4f5a330f8fef22f1eab865f067a3e60bd99a28a5c https://holo-packages.steamos.cloud/archlinux-deckard-hotfixes/release/0.4.x/mangohud-20260902.1-1-aarch64.pkg.tar.zst ef7411229ea4e9b3ab4e36ac356a85b5072f362ee1de68b28d09c43f0b0ecc44 https://steamdeck-packages.steamos.cloud/archlinux-deckard/archlinux/mash-20240705.2/extra/os/aarch64/fmt-10.2.0-1-aarch64.pkg.tar.zst c9bfd2c3f15071329d381b9c2061d07503c4787ceb7001b9dfbf72a23c85cb04 https://steamdeck-packages.steamos.cloud/archlinux-deckard/archlinux/mash-20240705.2/extra/os/aarch64/spdlog-1.13.0-1-aarch64.pkg.tar.zst f9fae6c84a0382188f1cb20761424051593fd09b1456191cc250bd16e0a1d7a5 https://steamdeck-packages.steamos.cloud/archlinux-deckard/archlinux/mash-20240705.2/extra/os/aarch64/glfw-3.4-2-aarch64.pkg.tar.zst -# libtracefs is ours (libtracefs-shim.c), and reads libtraceevent's structures as 1.8.2 lays them -# out: moving libtraceevent off 1.8.2 means checking event-parse.h against the shim. 0cccdc9c92cb3cae1e37ebeb0ed20b93bb3b5000c8f955b63d0e07bd878c7018 https://steamdeck-packages.steamos.cloud/archlinux-deckard/archlinux/mash-20240705.2/extra/os/aarch64/libtraceevent-1%3A1.8.2-2-aarch64.pkg.tar.zst diff --git a/tools/msitools/packages.txt b/tools/msitools/packages.txt index 8329e68a7..69a9dedba 100644 --- a/tools/msitools/packages.txt +++ b/tools/msitools/packages.txt @@ -1,12 +1,3 @@ -# What droiddeck-msi-install runs: msitools' msiinfo reads a Windows Installer package's tables -# and streams, cabextract unpacks its cabinets (every compression a cabinet can use, which gcab -# alone cannot), 7-Zip (7z + its 7z.so format plugin) opens the installer .exe wrappers around -# packages (self-extracting 7-Zip and cabinet files, WiX bundles). Arch Linux ARM's aarch64 packages, hosted unchanged on this repo's -# msitools-0.106-r1 release because the mirror keeps only the newest of each. CI fetches each, -# checks the sha256 and stages the files below into the apk under usr/local/lib/droiddeck-msitools; -# SessionFiles copies them into the runtime at every launch. Everything else they need (glib, gio, -# libxml2.so.16, zlib, bzip2, libgcc_s) is in the runtime already. -# sha256 url 4ab93d800b320d19c6c1090e21b69e5764ee241b6370012459e0c5df03739a8d https://github.com/Droid-Deck/DroidDeck/releases/download/msitools-0.106-r1/msitools-0.106-3-aarch64.pkg.tar.xz 4028f44d83b2956ba2931d32aee8cceecbd6481e2febf86a3b6bfed69ef403d5 https://github.com/Droid-Deck/DroidDeck/releases/download/msitools-0.106-r1/libgsf-1.14.60-1-aarch64.pkg.tar.xz 3d7bd103313cb36d6cb703a5e583363e56ff255f105bcc0727916d195967976d https://github.com/Droid-Deck/DroidDeck/releases/download/msitools-0.106-r1/gcab-1.6-2-aarch64.pkg.tar.xz diff --git a/tools/proot/bench/equiv2.py b/tools/proot/bench/equiv2.py index 397f41b56..b267ad121 100644 --- a/tools/proot/bench/equiv2.py +++ b/tools/proot/bench/equiv2.py @@ -185,7 +185,6 @@ def sockets(base): srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) out.append(" bind " + call(lambda: srv.bind(path))) if not os.path.exists(path): - # adb's shell may not create sockets under /data/local/tmp; the app can. srv.close() return out out.append(" name " + call(lambda: srv.getsockname())) diff --git a/tools/proot/bench/fpbench.c b/tools/proot/bench/fpbench.c index c9b42d888..f64e909e9 100644 --- a/tools/proot/bench/fpbench.c +++ b/tools/proot/bench/fpbench.c @@ -1,5 +1,3 @@ -/* The calls the fast path took over from proot, timed: run under fpoff and fastpath and compare. - * Usage: fpbench <scratch dir in the guest> [iterations]. Builds a Wine-like prefix there. */ #define _GNU_SOURCE #include <fcntl.h> #include <limits.h> diff --git a/tools/proot/bench/mechbench.c b/tools/proot/bench/mechbench.c index ae4319a95..4b9e034b4 100644 --- a/tools/proot/bench/mechbench.c +++ b/tools/proot/bench/mechbench.c @@ -1,14 +1,3 @@ -/* Cost of the syscall-interception mechanisms available to an Android app, measured on the - * trapped call getppid() (the tracer/handler does no work, so this is pure mechanism cost). - * - * mechbench filter <n_linear> allowed syscall through a filter of n JEQs (linear, proot-style) - * mechbench tree allowed syscall through a binary-search filter (~150 entries) - * mechbench ptrace [spin] [regs] seccomp RET_TRACE + ptrace tracer (proot's mechanism) - * mechbench sigsys seccomp RET_TRAP + in-process SIGSYS handler - * mechbench notif [sync] [spin] seccomp RET_USER_NOTIF + supervisor thread - * mechbench openat2 <root> <path> openat2(RESOLVE_IN_ROOT) vs openat on the host path - * Env CPU_T / CPU_S pin the trapped thread / the tracer-supervisor to a cpu. - */ #define _GNU_SOURCE #include <errno.h> #include <fcntl.h> @@ -68,8 +57,6 @@ static double time_trapped(void) { return (now() - t0) / N; } -/* A filter: arch check, then `n` JEQs on numbers that never match (like proot's list of ~150 - * traced syscalls seen by an allowed syscall), then `action` for TRAPPED, else ALLOW. */ static int install(int n_linear, uint32_t action, unsigned flags) { struct sock_filter *f = calloc(n_linear + 16, sizeof *f); int k = 0; @@ -77,7 +64,7 @@ static int install(int n_linear, uint32_t action, unsigned flags) { f[k++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_AARCH64, 1, 0); f[k++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW); f[k++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)); - for (int i = 0; i < n_linear; i++) /* numbers far above any real syscall */ + for (int i = 0; i < n_linear; i++) f[k++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 5000 + i, 0, 0); f[k++] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, TRAPPED, 0, 1); f[k++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, action); @@ -87,7 +74,6 @@ static int install(int n_linear, uint32_t action, unsigned flags) { return syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, flags, &p); } -/* ~150 traced numbers as a balanced binary search (what libseccomp's optimize=2 emits). */ static int emit_tree(struct sock_filter *f, int *k, int lo, int hi) { if (lo > hi) { f[(*k)++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW); return 0; } int mid = (lo + hi) / 2; @@ -106,7 +92,7 @@ static void *sigsys_unused; static void on_sigsys(int sig, siginfo_t *si, void *ctx) { (void)sig; (void)si; ucontext_t *uc = ctx; - uc->uc_mcontext.regs[0] = 4242; /* the emulated result */ + uc->uc_mcontext.regs[0] = 4242; } struct notif_arg { int fd; int spin; }; @@ -184,22 +170,17 @@ int main(int argc, char **argv) { if (ioctl(notif_fd, SECCOMP_IOCTL_NOTIF_SET_FLAGS, SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP) < 0) perror("SYNC_WAKE_UP"); else printf("(sync wake-up on)\n"); } - /* The filter lives on thread w, which has exited; install again on a fresh worker that - * does the timing. */ struct notif_arg na = {.fd = -1}; (void)na; printf("note: user-notif is measured in 'notif2'\n"); return 0; } if (!strcmp(mode, "notif2")) { - /* Supervisor = child thread (unfiltered), timed thread = main after installing. */ int sync = argc > 2 && !strcmp(argv[2], "sync"); pin("CPU_T"); int fd = install(0, SECCOMP_RET_USER_NOTIF, SECCOMP_FILTER_FLAG_NEW_LISTENER); if (fd < 0) { perror("NEW_LISTENER"); return 1; } if (sync && ioctl(fd, SECCOMP_IOCTL_NOTIF_SET_FLAGS, SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP, 0) < 0) perror("SYNC_WAKE_UP"); - /* The supervisor thread is created AFTER the filter, so it inherits it - but it only - * makes ioctl calls, which the filter allows. */ static struct notif_arg na; na.fd = fd; pthread_t s; diff --git a/tools/proot/bench/parstat.c b/tools/proot/bench/parstat.c index 0b229f080..cb016b5b1 100644 --- a/tools/proot/bench/parstat.c +++ b/tools/proot/bench/parstat.c @@ -1,4 +1,3 @@ -/* T threads each stat()ing a path for D seconds: aggregate trapped-syscall throughput. */ #define _GNU_SOURCE #include <pthread.h> #include <stdio.h> diff --git a/tools/proot/bench/run-device.sh b/tools/proot/bench/run-device.sh index 20333318f..0feacc437 100644 --- a/tools/proot/bench/run-device.sh +++ b/tools/proot/bench/run-device.sh @@ -1,14 +1,4 @@ #!/system/bin/sh -# Runs a guest command on an adb-connected device the way a DroidDeck session runs proot (same -# options, a similar number of binds), as the shell user from /data/local/tmp/dd. Layout there: -# mini/ a glibc rootfs subset (glibc, bash, coreutils, python) + the bench binaries -# proot, loader this repo's proot (tools/proot/build.sh) -> variant "patched" -# proot-vanilla termux proot at source.env with no patches -> variant "vanilla" -# proot-fp this repo's proot incl. 0014 -> "fastpath" / "fpoff" -# mini/usr/lib/libfastpath.so tools/proot/fastpath/fastpath.c -# NOTE: adb shell is NOT under the app seccomp policy; see docs/development/proot-performance.md. -# -# run-device.sh <patched|vanilla|noseccomp|bare|fastpath|fpoff|direct> cmd... D=/data/local/tmp/dd V=$1; shift U=$(id -u) diff --git a/tools/proot/bench/sysbench.c b/tools/proot/bench/sysbench.c index 628e36e46..2d42ffdc5 100644 --- a/tools/proot/bench/sysbench.c +++ b/tools/proot/bench/sysbench.c @@ -1,6 +1,3 @@ -/* Per-syscall latency under whatever runs this program (native, proot, ...). - * Usage: sysbench <deep-file> [iterations] - * Prints "name ns/op" lines. */ #define _GNU_SOURCE #include <fcntl.h> #include <linux/futex.h> diff --git a/tools/proot/bench/workloads.sh b/tools/proot/bench/workloads.sh index f09c0b9e5..a727605c3 100644 --- a/tools/proot/bench/workloads.sh +++ b/tools/proot/bench/workloads.sh @@ -1,5 +1,4 @@ #!/system/bin/sh -# workloads: direct glibc (patched PT_INTERP) vs proot variants D=/data/local/tmp/dd; cd $D t() { local s=$(date +%s%N); "$@" >/dev/null 2>&1; local e=$(date +%s%N); echo $(( (e - s) / 1000000 )); } base() { t ./run.sh $1 /usr/bin/true; } diff --git a/tools/proot/fastpath/audit.c b/tools/proot/fastpath/audit.c index 01628712e..6913ffa6f 100644 --- a/tools/proot/fastpath/audit.c +++ b/tools/proot/fastpath/audit.c @@ -1,20 +1,3 @@ -/* - * libblaudit: an LD_AUDIT module that keeps ld.so from asking proot about libraries that are not - * there. ld.so finds a library by trying to open it in each directory of its search path in turn, - * and under proot every one of those opens is a round trip through the tracer. A game's environment - * puts the game's own directory, Steam's runtime and video directories and Proton's library - * directories ahead of /usr/lib, so most of the opens of every Wine process fail. - * - * la_objsearch() is asked about each candidate before ld.so opens it. One that provably does not - * exist - its parent directory is reached through no symlink, so proot would walk the same path, - * and the name itself is missing there - is skipped. Everything else is left to ld.so, which then - * behaves exactly as it would have. - * - * Built without libc: an audit module is loaded into a link namespace of its own, and a libc there - * would be searched for along that same path. Its calls go through the trampoline page of - * libblfastpath.so, mapped here first (libblfastpath finds it in place), and only in a process traced - * by the proot the environment describes (PROOT_FP_KEY, as libblfastpath checks it). - */ #define _GNU_SOURCE #include <errno.h> #include <fcntl.h> @@ -26,7 +9,7 @@ #include <sys/syscall.h> #include <time.h> -#define FP_STUB_ADDR 0xffff00000UL /* as in fastpath.c */ +#define FP_STUB_ADDR 0xffff00000UL #define FP_STUB_SIZE 4096UL #define MAX_BINDS 256 #define PATH 4096 @@ -38,8 +21,6 @@ static stub_fn sys; static long sc(long nr, long a, long b, long c, long d) { return sys(nr, a, b, c, d, 0, 0); } -/* A syscall of its own, before the trampoline exists: only mmap, mprotect and munmap, which proot - * does not stop. */ static long raw(long nr, long a, long b, long c, long d) { register long x8 __asm__("x8") = nr; register long x0 __asm__("x0") = a; @@ -52,7 +33,6 @@ static long raw(long nr, long a, long b, long c, long d) { return x0; } -/* What __builtin___clear_cache does on aarch64, without libgcc. */ static void sync_code(char *b, char *e) { for (char *p = b; p < e; p += 4) __asm__ volatile("dc cvau, %0" : : "r"(p) : "memory"); __asm__ volatile("dsb ish" : : : "memory"); @@ -80,7 +60,6 @@ static int under(const char *path, const char *dir, size_t dlen) { return same(path, dir, dlen) && (path[dlen] == 0 || path[dlen] == '/'); } -/* ---------------------------------------------------------------- setup */ static char env[65536], tracer_env[32768]; static size_t env_len; @@ -109,7 +88,6 @@ static const char *var(const char *buf, size_t len, const char *name) { return NULL; } -/* The canonical form of a host path, as proot's realpath() made it; 0 when it has none. */ static int canonical(const char *path, char *out) { long fd = sc(SYS_openat, AT_FDCWD, (long)path, O_PATH | O_CLOEXEC, 0); if (fd < 0) return 0; @@ -148,14 +126,13 @@ static int traced_by_match(const char *key) { static void setup(void) { long p = raw(SYS_mmap, FP_STUB_ADDR, FP_STUB_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE); - /* Loaded before anything else maps, so whatever is already there is not the trampoline. */ if (p != (long)FP_STUB_ADDR) { if (p > 0) raw(SYS_munmap, p, FP_STUB_SIZE, 0, 0); return; } static const uint32_t code[] = { 0xAA0003E8, 0xAA0103E0, 0xAA0203E1, 0xAA0303E2, 0xAA0403E3, 0xAA0503E4, 0xAA0603E5, - 0xD4000001, 0xD65F03C0, /* mov x8,x0; mov x0..x5,x1..x6; svc #0; ret */ + 0xD4000001, 0xD65F03C0, }; copy((char *)p, (const char *)code, sizeof code); sync_code((char *)p, (char *)p + sizeof code); @@ -174,7 +151,6 @@ static void setup(void) { root = root_canon; rootlen = len_of(root); if (rootlen <= 1) return; - /* "host[:guest]|..." taken apart in place; a list this cannot hold leaves everything to ld.so. */ for (char *s = (char *)b; s && *s;) { if (nbinds == MAX_BINDS) return; char *end = s; @@ -196,9 +172,7 @@ static void setup(void) { ready = 1; } -/* ---------------------------------------------------------------- lookups */ -/* The host path proot binds there, as it canonicalised it: NULL for one it dropped (or /proc/self). */ static const char *bind_host(struct bind *bd) { if (!bd->state) { bd->state = under(bd->host, "/proc/self", 10) || !canonical(bd->host, bd->canon) ? 2 : 1; @@ -212,8 +186,6 @@ static long long now(void) { return ts.tv_sec * 1000000000LL + ts.tv_nsec; } -/* The host path proot gives a canonical guest path: under the longest binding over it (the last of - * two for the same path), else the rootfs. 0 for a binding proot dropped, or no room. */ static int to_host(const char *g, char *host, int *bound) { size_t n = len_of(g); struct bind *bd = NULL; @@ -235,12 +207,10 @@ static int to_host(const char *g, char *host, int *bound) { return 1; } -/* Under a directory proot or the kernel answers for in its own way. */ static int special(const char *g) { return under(g, "/proc", 5) || under(g, "/dev", 4) || under(g, "/sys", 4); } -/* A guest path proot only shows as glue, or one with a binding below it. */ static int above_binding(const char *g) { size_t n = len_of(g); for (int i = 0; i < nbinds; i++) @@ -248,12 +218,6 @@ static int above_binding(const char *g) { return 0; } -/* - * The canonical guest path of the directory `g`, walked as canonicalize() walks it: a symlink in the - * rootfs is followed in guest terms, its text losing the rootfs prefix as detranslate_path() has it. - * -1 when a component is missing, so proot fails the path with ENOENT. Anything this cannot follow - * exactly (a link inside a binding, glue, a component not a directory) gives 0. - */ static int canonical_dir(const char *g, char *out) { char path[PATH], next[PATH], host[PATH], text[PATH]; char st_buf[256] __attribute__((aligned(16))); @@ -312,8 +276,6 @@ static int canonical_dir(const char *g, char *out) { static struct { char dir[PATH]; char canon[PATH]; long long at; } known[VERIFIED]; static int next_slot; -/* canonical_dir(), remembered for a while: ld.so looks in the same few directories for every - * library. */ static int known_dir(const char *g, char *out) { size_t n = len_of(g); long long t = now(); @@ -331,8 +293,6 @@ static int known_dir(const char *g, char *out) { return 1; } -/* The guest path certainly names nothing: its directory is one proot reaches the same way, and the - * name is missing there. */ static int missing(const char *g) { char dir[PATH], cand[PATH], host[PATH]; size_t n = len_of(g); @@ -363,13 +323,6 @@ unsigned int la_version(unsigned int version) { return LAV_CURRENT; } -/* - * A candidate skipped here is one whose open "failed" with ld.so's errno as it stands, and ld.so - * gives up on the whole search path unless that is ENOENT or EACCES. So each lookup's first - * candidate is always opened for real: once that has failed, errno says so, and nothing ld.so - * does between candidates changes it unless the search ends anyway. The default directories are - * left alone too: what ld.so meets there is what it reports when a library is nowhere. - */ static int tried; char *la_objsearch(const char *name, uintptr_t *cookie, unsigned int flag) { diff --git a/tools/proot/fastpath/fastpath.c b/tools/proot/fastpath/fastpath.c index 3914423a0..22bc38fe4 100644 --- a/tools/proot/fastpath/fastpath.c +++ b/tools/proot/fastpath/fastpath.c @@ -1,38 +1,3 @@ -/* - * proot fast path: path syscalls answered inside the process, without a ptrace round trip. - * - * proot traps every path syscall into its tracer: 15-60 us a call on a Snapdragon, most of it - * the two cross-core wake-ups between tracee and tracer. This library, preloaded into every guest - * process, answers the common calls itself and issues the translated syscall from a trampoline - * page at a fixed address, which proot's seccomp filter lets through without a stop - * (PROOT_FASTPATH, tools/proot/patches/0014). - * - * It decides nothing proot would decide differently. A guest path is resolved the way proot's - * canonicalize() does it, one component at a time from the root or the guest directory a - * relative path starts in: each component is mapped through the longest binding whose guest path - * is a prefix of it (else the rootfs) and looked at without following it; a symlink is read and - * its text resolved in its place, ".." drops the last resolved component, and a directory that - * only exists as proot's glue above a binding is walked through. The kernel then gets the host - * path that proot would have given it. In /proc, /proc/self and /proc/thread-self are followed as - * proot follows them, and a link proot emulates is read the way it answers readlink(); anything - * proot keeps state about (a renamed directory, sockets bound at a long path, glue itself), the - * /dev links into /proc, any flag not handled here and any failure to decide go to proot: the real - * libc call runs, proot traps it and answers as it always has. - * - * Only syscalls Android's app seccomp policy allows are used - not openat2 or faccessat2, which - * it answers with SIGSYS. - * - * Directories resolved once are remembered for PROOT_FP_TTL_MS (default 2000; 0 = resolve every - * call): a directory replaced by a symlink within that window would be missed, the same trade the - * session's pathcache.c makes for the Steam client. Renames, removals and new symlinks made here - * forget them at once. - * - * Config (the launcher sets it): PROOT_FP_ROOT=<host rootfs>, PROOT_FP_BINDS=<host:guest|...>; - * PROOT_FP_KEY=<key>, with PROOT_FASTPATH=<key> in proot's own environment: nothing is answered - * here unless the tracer is that proot (traced_by_match). - * PROOT_FP_OFF=1 disables it; PROOT_FP_STATS=1 reports hits and misses at exit, and - * PROOT_FP_LOG=<file> appends every call left to proot to that file. - */ #define _GNU_SOURCE #include <dirent.h> #include <dlfcn.h> @@ -66,14 +31,10 @@ #include <unistd.h> #include <utime.h> -#define FP_STUB_ADDR 0xffff00000UL /* must match FASTPATH_STUB_ADDR in proot's seccomp.c */ +#define FP_STUB_ADDR 0xffff00000UL #define FP_STUB_SIZE 4096UL -/* A session binds 80-odd paths (devices, the Steam libraries, each Games folder...). More than fit, - * or one that cannot be held, turns the fast path off: a bind it did not know would map that - * guest path into the rootfs instead (an empty mount point), which proot never does. */ #define MAX_BINDS 256 #define FP_SLOW (-100000L) -/* proot gives up at MAXSYMLINKS; a chain this long is left to it, whatever it decides. */ #define MAX_LINKS 16 typedef long (*stub_fn)(long nr, long a, long b, long c, long d, long e, long f); @@ -91,7 +52,6 @@ static unsigned long hits, misses; static long sc(long nr, long a, long b, long c, long d, long e) { return fp_sys(nr, a, b, c, d, e, 0); } static void fp_init(void); static pthread_once_t init_once = PTHREAD_ONCE_INIT; -/* Set up on the first call that could use it: a process that only execs on pays nothing. */ static inline int fp_ready(void) { pthread_once(&init_once, fp_init); return fp_on; @@ -102,7 +62,6 @@ static int under(const char *path, const char *dir, size_t dlen) { return strncmp(path, dir, dlen) == 0 && (path[dlen] == 0 || path[dlen] == '/'); } -/* ---------------------------------------------------------------- setup */ static void stats(void) { char line[160]; @@ -114,7 +73,6 @@ static void stats(void) { static const char *miss_log; static long fp_open(int dirfd, const char *path, int flags, mode_t mode); -/* A call left to proot. Opened for each line: a program may close or reuse any descriptor kept. */ static void missed(const char *call, int dirfd, const char *path) { misses++; if (!miss_log) return; @@ -133,8 +91,6 @@ static void missed(const char *call, int dirfd, const char *path) { sc(SYS_close, fd, 0, 0, 0, 0); } -/* The canonical form of a host path (bindings may name /sdcard, /data/user/0, ...), as realpath() - * gives it; -errno, leaving the path as it is, when it does not lead to one. */ static long canonical(char *path, size_t size) { long fd = sc(SYS_openat, AT_FDCWD, (long)path, O_PATH | O_CLOEXEC, 0, 0); if (fd < 0) return fd; @@ -155,15 +111,8 @@ static pthread_mutex_t cache_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t bind_lock; static void after_fork(void) { pthread_mutex_init(&cache_lock, NULL); pthread_mutex_init(&bind_lock, NULL); } -/* - * The process is traced by a proot that lets the trampoline through and was given the same - * rootfs and bindings: its own environment carries PROOT_FASTPATH=<PROOT_FP_KEY> (the launcher - * derives the key from them) and no PROOT_NO_SECCOMP. A process that inherited the variables but - * runs under another proot, or none, must not translate paths itself: a proot that traps the - * trampoline would translate the host path a second time. - */ static int traced_by_match(const char *key) { - static char buf[32768]; /* under init_once */ + static char buf[32768]; char path[48], want[160]; long fd = sc(SYS_openat, AT_FDCWD, (long)"/proc/self/status", O_RDONLY | O_CLOEXEC, 0, 0); if (fd < 0) return 0; @@ -191,11 +140,6 @@ static int traced_by_match(const char *key) { return found; } -/* - * proot canonicalised every binding's host path with realpath() in its own process, so a binding of - * /proc/self/fd/1 (/dev/stdout) names whatever proot's own descriptor 1 is - /dev/null in a session - - * and one onto a pipe was dropped. Read through the tracer's /proc; 0 when that cannot be seen. - */ static int in_tracer(struct bind *bd) { char h[PATH_MAX]; if (snprintf(h, sizeof h, "/proc/%ld%s", tracer, bd->host + 10) >= (int)sizeof h) return 0; @@ -216,12 +160,12 @@ static void fp_init(void) { MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); if (p != (void *)FP_STUB_ADDR) { if (p != MAP_FAILED) { munmap(p, FP_STUB_SIZE); return; } - if (errno != EEXIST) return; /* mapped already: this library loaded twice */ + if (errno != EEXIST) return; } else { static const uint32_t code[] = { - 0xAA0003E8, /* mov x8, x0 */ 0xAA0103E0, /* mov x0, x1 */ 0xAA0203E1, /* mov x1, x2 */ - 0xAA0303E2, /* mov x2, x3 */ 0xAA0403E3, /* mov x3, x4 */ 0xAA0503E4, /* mov x4, x5 */ - 0xAA0603E5, /* mov x5, x6 */ 0xD4000001, /* svc #0 */ 0xD65F03C0, /* ret */ + 0xAA0003E8, 0xAA0103E0, 0xAA0203E1, + 0xAA0303E2, 0xAA0403E3, 0xAA0503E4, + 0xAA0603E5, 0xD4000001, 0xD65F03C0, }; memcpy(p, code, sizeof code); __builtin___clear_cache((char *)p, (char *)p + sizeof code); @@ -246,8 +190,6 @@ static void fp_init(void) { char *colon = strchr(spec, ':'); if (colon) *colon = 0; struct bind *bd = &binds[nbinds]; - /* Canonicalised on first use: some (/sdcard) are FUSE, slow to look up at every exec. - * Static storage: malloc here would grow the heap with brk(2), which proot traps. */ static char hosts[MAX_BINDS][PATH_MAX], guests[MAX_BINDS][512]; const char *g = colon ? colon + 1 : spec; if (strlen(g) >= sizeof guests[0]) { unheld = 1; break; } @@ -267,15 +209,13 @@ static void fp_init(void) { if (unheld || (s && *s)) { fp_sys = NULL; return; } for (int i = 0; i < nbinds; i++) if (under(binds[i].host, "/proc/self", 10) && !in_tracer(&binds[i])) { fp_sys = NULL; return; } - pthread_atfork(NULL, NULL, after_fork); /* registered once per exec */ + pthread_atfork(NULL, NULL, after_fork); fp_on = 1; if (getenv("PROOT_FP_STATS")) atexit(stats); miss_log = getenv("PROOT_FP_LOG"); if (miss_log && miss_log[0] != '/') miss_log = NULL; } -/* Whether proot holds a binding: one whose host path did not resolve it dropped ("can't sanitize - * binding"). 1, 0, or -1 when that cannot be told now. */ static pthread_mutex_t bind_lock = PTHREAD_MUTEX_INITIALIZER; static int held(struct bind *bd) { if (!__atomic_load_n(&bd->canon, __ATOMIC_ACQUIRE)) { @@ -290,16 +230,12 @@ static int held(struct bind *bd) { return !bd->dropped; } -/* ---------------------------------------------------------------- guest <-> host */ -/* Guest paths never answered here: bindings onto /proc/self, which proot resolves as its own. */ static int reserved(const char *g) { return under(g, "/dev/fd", 7) || under(g, "/dev/stdin", 10) || under(g, "/dev/stdout", 11) || under(g, "/dev/stderr", 11); } -/* The binding a canonical guest path is under (the longest guest prefix; of two for the same guest - * path, proot keeps the last), or NULL for the rootfs. -1 when it cannot be told. */ static int bind_of(const char *g, struct bind **out) { struct bind *bd = NULL; for (int i = 0; i < nbinds; i++) { @@ -312,7 +248,6 @@ static int bind_of(const char *g, struct bind **out) { return 0; } -/* The host path proot gives the kernel for a canonical guest path; -1 when it cannot be told. */ static int to_host(const char *g, char *host, struct bind **through) { struct bind *bd; if (bind_of(g, &bd) < 0) return -1; @@ -322,8 +257,6 @@ static int to_host(const char *g, char *host, struct bind **through) { return n < PATH_MAX ? 0 : -1; } -/* A guest directory proot only shows as glue: missing in the rootfs, with a binding below it. - * -1 when it cannot be told. */ static int glue_above_binding(const char *g, size_t len) { int glue = 0; for (int i = 0; i < nbinds && glue == 0; i++) @@ -331,13 +264,6 @@ static int glue_above_binding(const char *g, size_t len) { return glue; } -/* - * The k-th guest path proot could name a canonical host path by (the cwd, a dirfd), as its - * detranslation does: inside the rootfs, the rootfs's view of it, whatever is bound; elsewhere the - * binding with the deepest host path - two bindings of the same host path are two names, and which - * one proot uses depends on its list order. A name must also map back to the path, i.e. no longer - * binding hides it. 0, or -1 when there is no k-th. - */ static int guest_name(const char *h, int k, char *g) { char back[PATH_MAX]; size_t deepest = 0; @@ -359,7 +285,6 @@ static int guest_name(const char *h, int k, char *g) { return -1; } -/* The one guest path that names a host path, or -1. */ static int to_guest(const char *h, char *g, size_t size) { char other[PATH_MAX]; if (size < PATH_MAX || guest_name(h, 0, g) < 0) return -1; @@ -368,8 +293,6 @@ static int to_guest(const char *h, char *g, size_t size) { return 0; } -/* A host path or link text proot shows for a host path it read from a /proc link: in guest terms - * when the rootfs or a binding holds it, else as it is (a pipe, a socket, a path outside the guest). */ static int guest_text(const char *h, char *g) { int in = h[0] == '/' && under(h, root, rootlen); for (int i = 0; h[0] == '/' && !in && i < nbinds; i++) { @@ -382,8 +305,6 @@ static int guest_text(const char *h, char *g) { return 0; } -/* The binding proot's get_binding(HOST) finds for a host path outside the rootfs: the deepest host - * prefix. -1 for none, -2 when it cannot be told (two at that depth with different guest paths). */ static int host_bind(const char *h) { int found = -1; for (int i = 0; i < nbinds; i++) { @@ -396,11 +317,6 @@ static int host_bind(const char *h) { return found == -3 ? -2 : found; } -/* - * proot's detranslate_path() for the text of a link outside /proc whose own host path is `at`: - * text into the binding the link itself is in shows under that binding's guest path, other text - * naming the rootfs loses the rootfs prefix. 1 when rewritten, 0 when not, or FP_SLOW. - */ static long detranslated(char *text, const char *at) { if (text[0] != '/') return 0; if (!under(at, root, rootlen) && !under(text, root, rootlen)) { @@ -421,10 +337,9 @@ static long detranslated(char *text, const char *at) { return 1; } -/* ---------------------------------------------------------------- resolved directories */ #define SLOTS 512 -#define SLOT_PATH 256 /* longer directories are resolved every time */ +#define SLOT_PATH 256 struct slot { long long stamp; unsigned hash; unsigned gen; char literal[SLOT_PATH]; char guest[SLOT_PATH]; }; static struct slot cache[SLOTS]; static unsigned generation; @@ -441,7 +356,6 @@ static unsigned hash_of(const char *s) { return h; } -/* Something here changed which paths name which directories. */ static void forget(void) { __atomic_add_fetch(&generation, 1, __ATOMIC_RELEASE); } static int cached(const char *literal, unsigned h, char *guest) { @@ -467,12 +381,6 @@ static void remember(const char *literal, unsigned h, const char *guest) { pthread_mutex_unlock(&cache_lock); } -/* - * A binding below a symlink: proot canonicalised its guest path when it set it up - * (initialize_binding), so -b X:/sys/class/thermal/thermal_zone1/type binds the link's target, - * /sys/devices/virtual/thermal/thermal_zone1/type, and a path through the link meets it there. - * Known here by its literal text only, it would be missed; such a path is left to proot. - */ static int binding_below(const char *g) { size_t len = strlen(g); for (int i = 0; i < nbinds; i++) @@ -480,14 +388,7 @@ static int binding_below(const char *g) { return 0; } -/* ---------------------------------------------------------------- resolution */ -/* - * Where a resolution ended: the canonical guest path, its host path and, when it exists, its lstat. - * `libc` asks for links to be read as glibc's realpath() reads them, through readlink; `moving` is - * set once the answer depends on the caller (a /proc link), so it is not remembered. `magic`: it - * ends on /proc/<pid>/fd/N, which proot leaves for the kernel to follow; st is what it leads to. - */ struct res { char guest[PATH_MAX]; char host[PATH_MAX]; @@ -499,12 +400,10 @@ struct res { int magic; }; -/* A directory removed while open: the kernel names it "<path> (deleted)". */ static int gone(const char *h, size_t n) { return n > 10 && strcmp(h + n - 10, " (deleted)") == 0; } -/* The guest cwd proot keeps for this process, as the kernel's cwd names it. */ static long own_cwd(char *g) { char h[PATH_MAX]; long n = sc(SYS_readlinkat, AT_FDCWD, (long)"/proc/self/cwd", (long)h, sizeof h - 1, 0); @@ -514,7 +413,6 @@ static long own_cwd(char *g) { return (long)strlen(g); } -/* The program proot started this process with, asked of proot once: it is its own record. */ static char exe_text[PATH_MAX]; static int exe_len; static long own_exe(char *g) { @@ -537,12 +435,6 @@ static long own_exe(char *g) { return t; } -/* - * What proot's readlink() answers for the link r names under /proc (readlink_proc2, then - * detranslate_path): exe, cwd and root of a process it traces from its own records, every other - * link as the kernel has it with a host path put in guest terms. `changed` is set when that differs - * from the kernel's text, which is cut to `size` (0: no limit) before proot reads it. - */ static long proc_readlink(const struct res *r, char *text, size_t size, int *changed) { const char *name = strrchr(r->guest, '/') + 1; size_t blen = name - 1 - r->guest; @@ -566,7 +458,6 @@ static long proc_readlink(const struct res *r, char *text, size_t size, int *cha return (long)strlen(text); } -/* /proc/<pid>/fd/N. */ static int fd_link(const char *g) { const char *p = g + 6; if (strncmp(g, "/proc/", 6) != 0 || *p < '1' || *p > '9') return 0; @@ -575,11 +466,6 @@ static int fd_link(const char *g) { return 1; } -/* - * A link met under /proc while resolving, as canonicalize() reads it: /proc/self and - * /proc/thread-self name the caller for proot as for the kernel, the others are what proot's - * readlink() gives. 0 for /proc/<pid>/fd/N named last, which proot does not follow itself. - */ static long proc_link(struct res *r, char *text, int final) { int changed; if (!under(r->host, "/proc", 5)) return FP_SLOW; @@ -603,20 +489,10 @@ static int pop(char *g) { return 0; } -/* The final component is there for the call to meet but could not be looked at (sysfs and /proc - * nodes the app may not stat): proot leaves it to the kernel too. Only callers that act on r->host - * alone ask for it with MAY_BE_UNSEEN; to the others it is FP_SLOW. */ #define UNSEEN 2 #define MAY_BE_UNSEEN 2 -/* The final component is proot's glue: a directory, but not one the kernel has. */ #define GLUE 3 -/* - * Resolves `text` below r->guest (an existing canonical directory; "/" for an absolute text), as - * canonicalize() does. The final component is followed when `follow` is set; `last`: it is also the - * last of the whole path. 0 with r->exists set, 1 when only the final component is missing, - * UNSEEN, GLUE, -errno, or FP_SLOW. - */ static long walk(struct res *r, const char *text, int follow, int last) { char rest[PATH_MAX]; size_t len = strlen(text); @@ -646,9 +522,6 @@ static long walk(struct res *r, const char *text, int follow, int last) { if (to_host(r->guest, r->host, &through) < 0) return FP_SLOW; long s = sc(SYS_newfstatat, AT_FDCWD, (long)r->host, (long)&r->st, AT_SYMLINK_NOFOLLOW, 0); if (s < 0) { - /* A directory above a binding that proot could not look at when it set the binding up (one - * missing, or debugfs) is its glue; any other component it cannot look at is ENOENT unless it - * is the last, which the call itself then meets. */ int glue = glue_above_binding(r->guest, glen + n); if (glue < 0) return FP_SLOW; if (glue) { @@ -656,7 +529,6 @@ static long walk(struct res *r, const char *text, int follow, int last) { c = e; continue; } - /* glibc's realpath() meets it as its readlink() fails. */ if (!final || !last) return r->libc ? s : -ENOENT; return s == -ENOENT ? 1 : UNSEEN; } @@ -695,7 +567,6 @@ static long walk(struct res *r, const char *text, int follow, int last) { c = e; } if (!stated) { - /* Ended on the root, ".", ".." or glue: the directory itself. */ if (to_host(r->guest, r->host, NULL) < 0) return FP_SLOW; long s = sc(SYS_newfstatat, AT_FDCWD, (long)r->host, (long)&r->st, AT_SYMLINK_NOFOLLOW, 0); if (s < 0 && strcmp(r->guest, "/") != 0 && glue_above_binding(r->guest, strlen(r->guest))) return FP_SLOW; @@ -705,8 +576,6 @@ static long walk(struct res *r, const char *text, int follow, int last) { return 0; } -/* The host directory a relative path starts in; -ENOTDIR, as proot answers, for a descriptor that - * is no file-system object (a pipe, a socket), or -1. */ static int base_of(int dirfd, char *h) { char link[40]; if (dirfd == AT_FDCWD) snprintf(link, sizeof link, "/proc/self/cwd"); @@ -718,19 +587,12 @@ static int base_of(int dirfd, char *h) { return gone(h, n) ? -1 : 0; } -/* - * Resolves the guest path `base`/`path` (base NULL for an absolute path). Directories are looked - * up in the cache by their literal absolute path; only a path with ".." in it is resolved in full. - * A trailing "/" asks for an existing directory, as the kernel does; what a missing one creates is - * the caller's to check. - */ static long resolve_in(const char *base, const char *path, int follow, struct res *r) { char norm[PATH_MAX]; size_t o = 0; int dotdot = 0; size_t plen = strlen(path); int trailing = plen > 1 && path[plen - 1] == '/'; - /* "//" and "/./" go; ".." stays, it is not lexical once symlinks are involved. */ for (int part = base ? 0 : 1; part < 2; part++) { for (const char *c = part ? path : base; *c;) { while (*c == '/') c++; @@ -771,12 +633,6 @@ static long resolve_in(const char *base, const char *path, int follow, struct re return w; } -/* - * Resolves `path` relative to dirfd, as the guest means it. When the starting directory has two - * guest names (a library bound twice), proot resolves from the one it was reached by, which is - * not known here: the answer stands only if every name leads to the same place. r->named is set - * when the result's guest path is the one proot would report (realpath). - */ static __attribute__((noinline)) long same_from_other_names(const char *h, const char *path, int follow, long rs, const struct res *r) { char base[PATH_MAX]; @@ -811,12 +667,10 @@ static long resolve_as(int dirfd, const char *path, int how, int libc, struct re return rs == UNSEEN && !(how & MAY_BE_UNSEEN) ? FP_SLOW : rs; } -/* `how`: whether to follow a final link (1), and MAY_BE_UNSEEN. */ static long resolve(int dirfd, const char *path, int how, struct res *r) { return resolve_as(dirfd, path, how, 0, r); } -/* "name/" makes the kernel follow a final link; calls that act on the link itself leave it to proot. */ static int trailing_slash(const char *path) { size_t len = path ? strlen(path) : 0; return len > 1 && path[len - 1] == '/'; @@ -831,11 +685,9 @@ static int ret(long r) { static __typeof__(type) real_##name; \ if (!real_##name) real_##name = (type)dlsym(RTLD_NEXT, #name) -/* An answer from here (r != FP_SLOW) or the real call. */ #define ANSWER(r, dirfd, path, real_call) \ do { long r_ = (r); if (r_ != FP_SLOW) { hits++; return ret(r_); } missed(__func__, dirfd, path); return real_call; } while (0) -/* ---------------------------------------------------------------- open */ static long fp_open(int dirfd, const char *path, int flags, mode_t mode) { struct res r; @@ -846,7 +698,6 @@ static long fp_open(int dirfd, const char *path, int flags, mode_t mode) { if (rs < 0) return rs; if (rs == 1 && !(flags & O_CREAT)) return -ENOENT; if (rs == UNSEEN) return sc(SYS_openat, AT_FDCWD, (long)r.host, flags, (flags & O_CREAT) ? mode : 0, 0); - /* Resolved here: the kernel must not follow a link that appeared since in host terms. */ return sc(SYS_openat, AT_FDCWD, (long)r.host, flags | (follow && !r.magic ? O_NOFOLLOW : 0), (flags & O_CREAT) ? mode : 0, 0); } @@ -879,11 +730,9 @@ int __openat64_2(int d, const char *path, int flags) { return openat(d, path, fl int creat(const char *path, mode_t mode) { return open(path, O_WRONLY | O_CREAT | O_TRUNC, mode); } int creat64(const char *path, mode_t mode) __attribute__((alias("creat"))); -/* ---------------------------------------------------------------- stat family */ static long fp_stat(int dirfd, const char *path, int flags, struct stat *st) { struct res r; - /* fstat() is fstatat(fd, "", AT_EMPTY_PATH): nothing to translate. */ if ((flags & AT_EMPTY_PATH) && path && !path[0] && fp_ready()) return sc(SYS_newfstatat, dirfd, (long)"", (long)st, flags, 0); if (flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT)) return FP_SLOW; @@ -916,8 +765,6 @@ int lstat(const char *path, struct stat *st) { } int lstat64(const char *p, struct stat64 *st) { return lstat(p, (struct stat *)st); } -/* glibc turns fstat() into fstatat(fd, "", AT_EMPTY_PATH), which proot traps as a path call; the - * plain syscall is not traced at all. */ int fstat(int fd, struct stat *st) { REAL(fstat, int (*)(int, struct stat *)); if (!fp_ready()) return real_fstat(fd, st); @@ -926,13 +773,6 @@ int fstat(int fd, struct stat *st) { } int fstat64(int fd, struct stat64 *st) { return fstat(fd, (struct stat *)st); } -/* - * The pre-2.33 entry points. Programs linked against an older glibc - the Steam client, - * steamclient.so and filesystem_stdio.so among them - call these rather than stat(), so without - * them every stat the client makes goes to proot: a download's preallocation stats each file it - * creates. On aarch64 the only version is 0 (_STAT_VER), whose struct stat is the one above; - * glibc refuses any other with EINVAL, and so does this. - */ #define FP_STAT_VER 0 static int xstat_common(int ver, int dirfd, const char *path, struct stat *st, int flags) { REAL(fstatat, int (*)(int, const char *, struct stat *, int)); @@ -967,10 +807,7 @@ int statx(int dirfd, const char *path, int flags, unsigned mask, struct statx *o ANSWER(rs, dirfd, path, real_statx(dirfd, path, flags, mask, out)); } -/* ---------------------------------------------------------------- access */ -/* AT_EACCESS only differs from a plain check in a set-ID program, where glibc does the work itself; - * elsewhere glibc's own fallback is the plain faccessat, and so is this. */ static int effective_is_real(void) { return !getauxval(AT_SECURE) && getuid() == geteuid() && getgid() == getegid(); } @@ -979,8 +816,6 @@ static int access_common(int dirfd, const char *path, int mode, int flags) { REAL(faccessat, int (*)(int, const char *, int, int)); struct res r; if ((flags & AT_EACCESS) && effective_is_real()) flags &= ~AT_EACCESS; - /* faccessat(2) has no flags; AT_SYMLINK_NOFOLLOW (the Steam client passes it for every file it - * reserves) only differs on a symlink, which goes to proot. */ long rs = (flags & ~AT_SYMLINK_NOFOLLOW) == 0 ? resolve(dirfd, path, (flags & AT_SYMLINK_NOFOLLOW ? 0 : 1) | MAY_BE_UNSEEN, &r) : FP_SLOW; if (rs == 1) rs = -ENOENT; @@ -988,7 +823,6 @@ static int access_common(int dirfd, const char *path, int mode, int flags) { if (rs == 0 || rs == UNSEEN) rs = sc(SYS_faccessat, AT_FDCWD, (long)r.host, mode, 0, 0); if (rs != FP_SLOW) { hits++; return ret(rs); } missed(__func__, dirfd, path); - /* glibc tries faccessat2 first, which Android answers with SIGSYS: a second round trip. */ if (flags == 0) return (int)syscall(SYS_faccessat, dirfd, path, mode); return real_faccessat(dirfd, path, mode, flags); } @@ -997,13 +831,7 @@ int access(const char *path, int mode) { return access_common(AT_FDCWD, path, mo int eaccess(const char *path, int mode) { return access_common(AT_FDCWD, path, mode, AT_EACCESS); } int euidaccess(const char *path, int mode) __attribute__((alias("eaccess"))); -/* ---------------------------------------------------------------- readlink, realpath, getcwd */ -/* - * The text proot's readlink() gives for the link r names: the stored text as the kernel cuts it to - * `size` (0: no limit), detranslated. `changed`: proot wrote its own answer rather than leaving the - * kernel's. - */ static long link_text(const struct res *r, char *text, size_t size, int *changed) { int in_proc = under(r->guest, "/proc", 5); *changed = 0; @@ -1040,8 +868,6 @@ static ssize_t readlink_common(int dirfd, const char *path, char *buf, size_t si memcpy(buf, text, n); return n; } - /* As proot writes its answer: with the terminator when it fits, else cut at the buffer (or - * PATH_MAX) and reported as that long. */ size_t max = size < PATH_MAX ? size : PATH_MAX; if (n + 1 < max) { memcpy(buf, text, n + 1); @@ -1053,8 +879,6 @@ static ssize_t readlink_common(int dirfd, const char *path, char *buf, size_t si ssize_t readlinkat(int dirfd, const char *path, char *buf, size_t size) { return readlink_common(dirfd, path, buf, size); } ssize_t readlink(const char *path, char *buf, size_t size) { return readlink_common(AT_FDCWD, path, buf, size); } -/* _FORTIFY_SOURCE builds call these, which reach libc's own readlink and getcwd directly; an - * overflow is still the real one's to report. */ ssize_t __readlink_chk(const char *path, char *buf, size_t size, size_t room) { REAL(__readlink_chk, ssize_t (*)(const char *, char *, size_t, size_t)); if (size > room) return real___readlink_chk(path, buf, size, room); @@ -1066,9 +890,6 @@ ssize_t __readlinkat_chk(int dirfd, const char *path, char *buf, size_t size, si return readlink_common(dirfd, path, buf, size); } -/* glibc's realpath() reads every component as a link, each a trapped readlinkat with an exit stop: - * wineserver and Wine resolve their prefix that way for every file they open, and a file's name - * through /proc/self/fd. */ char *realpath(const char *path, char *resolved) { REAL(realpath, char *(*)(const char *, char *)); struct res r; @@ -1093,8 +914,6 @@ char *__realpath_chk(const char *path, char *resolved, size_t size) { } char *canonicalize_file_name(const char *path) { return realpath(path, NULL); } -/* proot keeps the guest cwd and answers getcwd on the way out; with the fast path the kernel's cwd - * is its host directory, which names it whenever only one guest path does. */ char *getcwd(char *buf, size_t size) { REAL(getcwd, char *(*)(char *, size_t)); char g[PATH_MAX]; @@ -1117,7 +936,6 @@ char *__getcwd_chk(char *buf, size_t size, size_t room) { return getcwd(buf, size); } -/* ---------------------------------------------------------------- changes */ int mkdirat(int dirfd, const char *path, mode_t mode) { REAL(mkdirat, int (*)(int, const char *, mode_t)); @@ -1128,7 +946,6 @@ int mkdirat(int dirfd, const char *path, mode_t mode) { } int mkdir(const char *path, mode_t mode) { return mkdirat(AT_FDCWD, path, mode); } -/* The last name in a path is "." or "..", which the kernel refuses to remove. */ static int ends_in_dots(const char *path) { size_t n = strlen(path); while (n > 1 && path[n - 1] == '/') n--; @@ -1143,7 +960,6 @@ static long fp_unlink(int dirfd, const char *path, int flags) { int slash = trailing_slash(path); if ((flags & ~AT_REMOVEDIR) || !path || ends_in_dots(path) || (slash && !(flags & AT_REMOVEDIR))) return FP_SLOW; if (slash) { - /* rmdir("dir/") removes dir itself, never what a link of that name points to. */ size_t n = strlen(path); if (n >= sizeof bare) return FP_SLOW; while (n > 1 && path[n - 1] == '/') n--; @@ -1172,8 +988,6 @@ int unlinkat(int dirfd, const char *path, int flags) { return unlink_common(dirf int unlink(const char *path) { return unlink_common(AT_FDCWD, path, 0); } int rmdir(const char *path) { return unlink_common(AT_FDCWD, path, AT_REMOVEDIR); } -/* A renamed directory moves the guest cwd proot keeps for every process under it, so those stay - * with proot; files are what programs rename all the time (write a copy, rename it over). */ static int rename_common(int olddirfd, const char *old, int newdirfd, const char *new, unsigned flags) { REAL(renameat2, int (*)(int, const char *, int, const char *, unsigned)); struct res from, to; @@ -1192,7 +1006,6 @@ static int rename_common(int olddirfd, const char *old, int newdirfd, const char } if (rs != FP_SLOW) { hits++; return ret(rs); } missed(__func__, olddirfd, old); - /* renameat, not renameat2: it is the one proot follows to move the cwd it keeps. */ if (!flags) return (int)syscall(SYS_renameat, olddirfd, old, newdirfd, new); return real_renameat2(olddirfd, old, newdirfd, new, flags); } @@ -1202,7 +1015,6 @@ int renameat2(int olddirfd, const char *old, int newdirfd, const char *new, unsi int renameat(int olddirfd, const char *old, int newdirfd, const char *new) { return rename_common(olddirfd, old, newdirfd, new, 0); } int rename(const char *old, const char *new) { return rename_common(AT_FDCWD, old, AT_FDCWD, new, 0); } -/* The link's text is stored as given, as proot stores it. */ int symlinkat(const char *target, int dirfd, const char *path) { REAL(symlinkat, int (*)(const char *, int, const char *)); struct res r; @@ -1230,7 +1042,7 @@ static int utimens_common(int dirfd, const char *path, const struct timespec tim struct res r; long rs; if (!path && fp_ready()) { - rs = sc(SYS_utimensat, dirfd, 0, (long)times, flags, 0); /* futimens() */ + rs = sc(SYS_utimensat, dirfd, 0, (long)times, flags, 0); } else { rs = flags & ~AT_SYMLINK_NOFOLLOW ? FP_SLOW : resolve(dirfd, path, (flags & AT_SYMLINK_NOFOLLOW ? 0 : 1) | MAY_BE_UNSEEN, &r); if (rs == 1) rs = -ENOENT; @@ -1273,7 +1085,6 @@ int truncate(const char *path, off_t length) { } int truncate64(const char *path, off_t length) __attribute__((alias("truncate"))); -/* proot reports /dev/shm as tmpfs on the way out of statfs; that stays with it. */ static int statfs_common(const char *path, struct statfs *buf, int (*real)(const char *, struct statfs *)) { struct res r; long rs = resolve(AT_FDCWD, path, 1, &r); @@ -1291,9 +1102,9 @@ int statfs64(const char *path, struct statfs64 *buf) { return statfs_common(path, (struct statfs *)buf, real_statfs64); } -#define FP_ST_VALID 0x0020 /* the kernel's ST_VALID: statfs filled in f_flags */ -/* statvfs() is statfs() converted as glibc converts it; without ST_VALID the mount flags would come - * from /proc/mounts, which glibc reads itself. */ +#define FP_ST_VALID 0x0020 + + static int statvfs_common(const char *path, struct statvfs *buf, int (*real)(const char *, struct statvfs *)) { struct res r; struct statfs fs; @@ -1328,8 +1139,6 @@ int statvfs64(const char *path, struct statvfs64 *buf) { return statvfs_common(path, (struct statvfs *)buf, real_statvfs64); } -/* Reading attributes only: fake_id0 rewrites the permission side of setting them. Wine reads - * user.DOSATTRIB for every file it reports on. */ static ssize_t getxattr_common(const char *path, const char *name, void *value, size_t size, int follow, ssize_t (*real)(const char *, const char *, void *, size_t)) { struct res r; @@ -1377,14 +1186,7 @@ int inotify_add_watch(int fd, const char *path, uint32_t mask) { ANSWER(rs, AT_FDCWD, path, real_inotify_add_watch(fd, path, mask)); } -/* ---------------------------------------------------------------- Unix sockets */ -/* - * proot only translates the path of a named Unix socket; every other address (abstract, unnamed, - * IP) passes through untouched, so the call itself needs no stop. A named one is resolved as proot - * would and sent with its host path, as long as that fits sun_path: a longer one proot binds to a - * short temporary name, so that stays with it. - */ static long socket_address(const struct sockaddr *addr, socklen_t len, int follow, struct sockaddr_un *out, socklen_t *out_len) { const struct sockaddr_un *un = (const struct sockaddr_un *)addr; size_t room = sizeof un->sun_path; @@ -1410,9 +1212,6 @@ static long socket_address(const struct sockaddr *addr, socklen_t len, int follo return 1; } -/* accept() and connect() are cancellation points in libc; a raw call is not, so only those that - * cannot block for long come here: a non-blocking socket, a listener with a connection waiting, - * or connecting a Unix one. */ static int nonblocking(int fd) { long fl = sc(SYS_fcntl, fd, F_GETFL, 0, 0, 0); return fl >= 0 && (fl & O_NONBLOCK); @@ -1439,8 +1238,6 @@ int bind(int fd, const struct sockaddr *addr, socklen_t len) { ANSWER(rs, AT_FDCWD, "(bind)", real_bind(fd, addr, len)); } -/* An address the kernel reports, as proot reports it: a named Unix socket's host path in guest - * terms. One no guest path names unambiguously is left as is, which proot cannot do better. */ static void report_address(const struct sockaddr_storage *got, socklen_t got_len, struct sockaddr *addr, socklen_t *len) { struct sockaddr_storage shown = *got; socklen_t shown_len = got_len; @@ -1500,10 +1297,7 @@ int getpeername(int fd, struct sockaddr *addr, socklen_t *len) { return name_common(SYS_getpeername, fd, addr, len, real_getpeername); } -/* ---------------------------------------------------------------- libc calls that open internally */ -/* shm_open() and shm_unlink() open /dev/shm/<name> with libc's internal calls; an overlay asks for - * FEX's statistics that way every frame. The name is checked and the file opened as glibc does. */ static int shm_path(const char *name, char *path) { while (*name == '/') name++; size_t n = strlen(name); @@ -1561,8 +1355,6 @@ static FILE *fopen_common(const char *path, const char *m, FILE *(*real)(const c FILE *fopen(const char *path, const char *m) { REAL(fopen, FILE *(*)(const char *, const char *)); return fopen_common(path, m, real_fopen); } FILE *fopen64(const char *path, const char *m) { REAL(fopen64, FILE *(*)(const char *, const char *)); return fopen_common(path, m, real_fopen64); } -/* glibc reads /sys/devices/system/cpu/online with its own open for every get_nprocs(), and - * sysconf(_SC_NPROCESSORS_ONLN) is that: overlays and thread pools ask it every frame. */ static int online_cpus(void) { long fd = fp_open(AT_FDCWD, "/sys/devices/system/cpu/online", O_RDONLY | O_CLOEXEC, 0); if (fd < 0) return -1; @@ -1595,11 +1387,7 @@ long sysconf(int name) { return real_sysconf(name); } -/* ---------------------------------------------------------------- uname */ -/* proot answers uname() with the utsname it started with (--kernel-release), the same for every - * call: asked once, it is every later answer. libX11 asks for each display connection, glibc's - * gethostname() through it. */ static struct utsname uts; static int uts_known; static long own_uname(struct utsname *u) { @@ -1639,8 +1427,6 @@ int gethostname(char *name, size_t len) { return 0; } -/* glibc's isatty() asks with TCGETS2, which proot rewrites to TCGETS (Android allows no termios2 - * request on a tty): one stop each way for every check. libudev and shells check all the time. */ int isatty(int fd) { REAL(isatty, int (*)(int)); struct termios k; @@ -1651,9 +1437,6 @@ int isatty(int fd) { return r == 0; } -/* proot reads a memfd's name only to refuse the few that are then executed or locked; the rest are - * made here. The session's /proc/net and status copies, wineserver and Xwayland make them often. - * Without memfd in the kernel, proot's ashmem stands in. */ int memfd_create(const char *name, unsigned flags) { REAL(memfd_create, int (*)(const char *, unsigned)); if (!name || !strncmp(name, "JITCode:", 8) || !strcmp(name, "opcache_lock") || !strncmp(name, "lib/apk/exec/", 13) @@ -1664,16 +1447,7 @@ int memfd_create(const char *name, unsigned flags) { return ret(r); } -/* ---------------------------------------------------------------- passwd */ -/* - * glibc's NSS stats /etc/nsswitch.conf and reads /etc/passwd with its own calls on every lookup, - * two stops each: Xwayland looks a user up for every client, every shell for itself. When - * nsswitch.conf sends passwd to "files" first with no action after it, a lookup that finds its - * entry there ends there, so that case is answered here from the same file. Anything else - not - * found, a file nss_files might read differently from this strict reading, a buffer it might call - * too small - goes to glibc. - */ static long read_small(const char *path, char *buf, size_t size) { long fd = fp_open(AT_FDCWD, path, O_RDONLY | O_CLOEXEC, 0); if (fd < 0) return -1; @@ -1707,8 +1481,6 @@ static int passwd_is_files(char *conf, size_t size) { return found; } -/* The line for `name` (or `uid`, name NULL) split in place into its seven fields; -1 for a file - * read strictly that has none, 0 when this cannot tell. `longest`: the longest line read up to it. */ static int passwd_line(char *file, const char *name, uid_t uid, char **f, size_t *longest) { *longest = 0; for (char *line = file; line && *line;) { @@ -1733,7 +1505,6 @@ static int passwd_line(char *file, const char *name, uid_t uid, char **f, size_t return -1; } -/* 1 with *pw filled from buf, 0 to leave it to glibc. */ static int own_passwd(const char *name, uid_t uid, struct passwd *pw, char *buf, size_t buflen) { char file[16384], *f[7]; size_t longest; @@ -1783,7 +1554,6 @@ int getpwuid_r(uid_t uid, struct passwd *pw, char *buf, size_t buflen, struct pa return passwd_r(NULL, uid, pw, buf, buflen, result, NULL, real_getpwuid_r); } -/* glibc's own getpwnam/getpwuid keep one static entry, as these do. */ static struct passwd static_pw; static char static_pw_buf[4096]; static pthread_mutex_t static_pw_lock = PTHREAD_MUTEX_INITIALIZER; diff --git a/tools/proot/patches/0004-seccomp-filter-by-argument.patch b/tools/proot/patches/0004-seccomp-filter-by-argument.patch index 302a97eba..f5497337d 100644 --- a/tools/proot/patches/0004-seccomp-filter-by-argument.patch +++ b/tools/proot/patches/0004-seccomp-filter-by-argument.patch @@ -101,8 +101,8 @@ index 6d1ba47..8557d4a 100644 * Append to @program->filter the statements that allow anything (if - * unfiltered). Note that @nb_traced_syscalls is used to make a - * sanity check. This function returns -errno if an error occurred, -+ * unfiltered). Note that @section_length is used to make a sanity -+ * check. This function returns -errno if an error occurred, ++ ++ * otherwise 0. */ -static int end_arch_section(struct sock_fprog *program, size_t nb_traced_syscalls) @@ -125,7 +125,7 @@ index 6d1ba47..8557d4a 100644 /** * Append to @program->filter the statements that check the current - * @architecture. Note that @nb_traced_syscalls is used to make a -+ * @architecture. Note that @section_length is used to make a ++ * sanity check. This function returns -errno if an error occurred, * otherwise 0. */ diff --git a/tools/proot/patches/0010-new-mount-api-enosys.patch b/tools/proot/patches/0010-new-mount-api-enosys.patch index 87fd43d1f..f10c1fb2a 100644 --- a/tools/proot/patches/0010-new-mount-api-enosys.patch +++ b/tools/proot/patches/0010-new-mount-api-enosys.patch @@ -15,8 +15,8 @@ index 1d3b3fd..32d2503 100644 status = -ENOSYS; break; -+ /* The new mount API opens and moves paths proot never sees: open_tree(AT_FDCWD, "/") -+ * hands back the host's root. Callers (libglnx, systemd) fall back to openat. */ ++ ++ + case PR_open_tree: + case PR_move_mount: + case PR_fspick: diff --git a/tools/proot/patches/0011-kompat-utsname-only.patch b/tools/proot/patches/0011-kompat-utsname-only.patch index 9c1949cce..407970b25 100644 --- a/tools/proot/patches/0011-kompat-utsname-only.patch +++ b/tools/proot/patches/0011-kompat-utsname-only.patch @@ -16,9 +16,9 @@ index 5c39942..4cb7dec 100644 FILTERED_SYSNUM_END, }; -+/* When the virtual kernel is not older than the actual one and the -+ * hwcap is left alone, the only thing left to emulate is the utsname -+ * itself: every other handler above returns without a change. */ ++ ++ ++ +static FilteredSysnum utsname_only_sysnums[] = { + { PR_setdomainname, FILTER_SYSEXIT }, + { PR_sethostname, FILTER_SYSEXIT }, @@ -26,12 +26,12 @@ index 5c39942..4cb7dec 100644 + FILTERED_SYSNUM_END, +}; + -+/** -+ * Return whether @config only renames the kernel identity (nodename, -+ * domainname, version string) without emulating an older kernel or -+ * another hwcap, in which case futex, fcntl, epoll_pwait, ... don't -+ * have to stop in the tracer and the vDSO can be left in place. -+ */ ++ ++ ++ ++ ++ ++ +static bool utsname_only(const Config *config) +{ + return config->actual_release != 0 diff --git a/tools/proot/patches/0012-android-hardlink-denial.patch b/tools/proot/patches/0012-android-hardlink-denial.patch index e2d089fb3..b8346a8cd 100644 --- a/tools/proot/patches/0012-android-hardlink-denial.patch +++ b/tools/proot/patches/0012-android-hardlink-denial.patch @@ -19,7 +19,7 @@ index 8c3eb2e..4c28e10 100644 case PR_open: flags = peek_reg(tracee, CURRENT, SYSARG_2); +#ifdef __ANDROID__ -+ /* An O_TMPFILE file can only be named by linkat, which Android denies apps. */ ++ + if ((flags & __O_TMPFILE) != 0) { + status = -EOPNOTSUPP; + break; @@ -50,8 +50,8 @@ index df41084..5d981db 100644 break; +#ifdef __ANDROID__ -+ /* Android denies apps hard links with EACCES; EPERM is what callers such as -+ * libostree take as "no hard links here" and copy instead. */ ++ ++ + case PR_link: + case PR_linkat: + if ((int) syscall_result == -EACCES) diff --git a/tools/proot/patches/0012-fake_id0-identity-only.patch b/tools/proot/patches/0012-fake_id0-identity-only.patch index 155b62b8e..73f9ac35e 100644 --- a/tools/proot/patches/0012-fake_id0-identity-only.patch +++ b/tools/proot/patches/0012-fake_id0-identity-only.patch @@ -17,7 +17,7 @@ index 2e78783..5227ced 100644 #define FAKE_ID0_CONFIG_H #include <sys/types.h> /* uid_t, gid_t */ -+#include <stdbool.h> /* bool, */ ++#include <stdbool.h> typedef struct { uid_t ruid; @@ -26,8 +26,8 @@ index 2e78783..5227ced 100644 mode_t umask; + -+ /* The emulated ids are the ones PRoot really has: see -+ * identity_sysnums in fake_id0.c. */ ++ ++ + bool identity; } Config; @@ -40,17 +40,17 @@ index 9477f1c..fe08372 100644 FILTERED_SYSNUM_END, }; -+/* When the emulated ids are the ones PRoot really has, and the tracee -+ * is not root, every handler for the syscalls above but these is a -+ * no-op: stat and get*id report the real ids already, SCM_CREDENTIALS -+ * and SO_PEERCRED carry them, chown swaps an id for itself, and the -+ * permission and chroot fixups only apply to euid 0. The set*id -+ * family is still emulated (Android's app seccomp policy traps it) and -+ * so are the xattr permission fixups, which apply to any euid. An -+ * unprivileged set*id can only take an id the tracee already holds, -+ * and set-user-ID bits are not honoured in this mode (Android mounts -+ * the app's data nosuid, so the kernel would not honour them either), -+ * so the ids never change and the list stays sufficient. */ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +static FilteredSysnum identity_sysnums[] = { + { PR_fsetxattr, FILTER_SYSEXIT }, + { PR_lsetxattr, FILTER_SYSEXIT }, @@ -81,8 +81,8 @@ index 9477f1c..fe08372 100644 if ((int) result < 0 || tracee->status < 0 || sysnum != PR_execve) return 0; -+ /* The auxv already carries the real ids, and set-user-ID bits -+ * are not honoured in this mode: see identity_sysnums. */ ++ ++ + if (config->identity) + return 0; + diff --git a/tools/proot/patches/0013-seccomp-ioctl-by-request-and-kernel-exit-stops.patch b/tools/proot/patches/0013-seccomp-ioctl-by-request-and-kernel-exit-stops.patch index eaf62c565..08fb2954c 100644 --- a/tools/proot/patches/0013-seccomp-ioctl-by-request-and-kernel-exit-stops.patch +++ b/tools/proot/patches/0013-seccomp-ioctl-by-request-and-kernel-exit-stops.patch @@ -16,11 +16,11 @@ index 0b381bb..1116ab2 100644 #include <stdint.h> /* uint*_t, UINT*_MAX, */ #include <stdbool.h> #include <sys/resource.h> -+#include <sys/utsname.h> /* uname(2), */ -+#include <stdio.h> /* sscanf(3), */ -+#include <asm/ioctls.h> /* TCGETS2, TCSETS*2, */ -+#include <asm/termbits.h> /* struct termios2, */ -+#include <linux/fs.h> /* FICLONE, */ ++#include <sys/utsname.h> ++#include <stdio.h> ++#include <asm/ioctls.h> ++#include <asm/termbits.h> ++#include <linux/fs.h> #include <assert.h> /* assert(3), */ #include "syscall/seccomp.h" @@ -28,27 +28,27 @@ index 0b381bb..1116ab2 100644 return add_statements(program, LENGTH_TRACE_PRLIMIT_STACK, statements); } -+/** -+ * Append to @program->filter the statements that trace @syscall -+ * (ioctl) only for the requests enter.c and exit.c act on - the -+ * termios requests Android's policy denies an app on a pty, rewritten -+ * to ones it allows, and FICLONE, whose EACCES is turned into -+ * EOPNOTSUPP - and allow any other request at once. Every GPU submit -+ * and wait is an ioctl, so they no longer stop in the tracer, and the -+ * block is emitted first so they are also spared the rest of the list. -+ * (After WinNative, maxjivi05, 53836ca9.) -+ */ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +static int add_trace_ioctl_requests(struct sock_fprog *program, word_t syscall, int flag) +{ + const size_t request_offset = offsetof(struct seccomp_data, args[1]); + + #define LENGTH_TRACE_IOCTL 10 + struct sock_filter statements[LENGTH_TRACE_IOCTL] = { -+ /* Not this syscall: skip the block, the accumulator -+ * still holds the syscall number. */ ++ ++ + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, syscall, 0, 9), + -+ /* Load the request's low word into the accumulator. */ ++ + BPF_STMT(BPF_LD + BPF_W + BPF_ABS, request_offset), + + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, TCSETSF, 5, 0), @@ -102,8 +102,8 @@ index 0b381bb..1116ab2 100644 if (status < 0) goto end; -+ /* Two passes: ioctl first, it is by far the most frequent -+ * syscall and almost every one of them is allowed. */ ++ ++ + for (pass = 0; pass < 2; pass++) for (j = 0; j < seccomp_archs[i].nb_abis; j++) { for (k = 0; sysnums[k].value != PR_void; k++) { @@ -117,15 +117,15 @@ index 0b381bb..1116ab2 100644 return 0; } -+/** -+ * Drop the exit stops PRoot only needs on kernels older than the one it -+ * runs on. The faccessat2 exit stop restores the arguments for glibc's -+ * fallback to faccessat on ENOSYS (kernels before 5.8, termux 5ba8b95); -+ * the statx one emulates the call when it fails, which only a kernel -+ * before 4.11 does for want of it - a statx Android's policy traps is -+ * emulated from SIGSYS instead. Both stops re-read and re-translate -+ * the path, and are paid on every call. -+ */ ++ ++ ++ ++ ++ ++ ++ ++ ++ +static void drop_unneeded_exit_stops(void) +{ + static bool done = false; diff --git a/tools/proot/patches/0014-fastpath-trampoline.patch b/tools/proot/patches/0014-fastpath-trampoline.patch index a865a5046..8daacbbee 100644 --- a/tools/proot/patches/0014-fastpath-trampoline.patch +++ b/tools/proot/patches/0014-fastpath-trampoline.patch @@ -22,7 +22,7 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. -#include <errno.h> /* errno(3), E* */ +#include <errno.h> -+#include <stdlib.h> /* errno(3), E* */ ++#include <stdlib.h> #include <talloc.h> /* talloc_*, */ #include <sys/un.h> /* struct sockaddr_un, */ #include <linux/net.h> /* SYS_*, */ @@ -30,15 +30,15 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. if ((statl.st_mode & S_IXUSR) == 0) return -EACCES; -+ /* With the fast path, the kernel's cwd follows the guest's -+ * (at its host path), so relative lookups made inside the -+ * tracee resolve where proot would resolve them. */ ++ ++ ++ + if (getenv("PROOT_FASTPATH") != NULL) { + int fd = open(path, O_PATH | O_DIRECTORY | O_CLOEXEC); + -+ /* Where the kernel could not follow, it goes to the -+ * host's "/", which no binding names: the fast path -+ * then sends every relative lookup to proot. */ ++ ++ ++ + strcpy(oldpath, fd >= 0 ? path : "/"); + if (fd >= 0) + close(fd); @@ -52,7 +52,7 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. talloc_set_name_const(tracee->fs->cwd, "$cwd"); + if (getenv("PROOT_FASTPATH") != NULL) { -+ /* fchdir(2) runs as it is; chdir(2) on the host path. */ ++ + status = syscall_number == PR_chdir + ? set_sysarg_path(tracee, oldpath, SYSARG_1) + : 0; @@ -68,7 +68,7 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. #include <talloc.h> /* talloc_*, */ #include <errno.h> /* E*, */ #include <string.h> /* memcpy(3), */ -+#include <stdlib.h> /* getenv(3), */ ++#include <stdlib.h> #include <stddef.h> /* offsetof(3), */ #include <stdint.h> /* uint*_t, UINT*_MAX, */ #include <stdbool.h> @@ -76,11 +76,11 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. * items). This function returns -errno if an error occurred, * otherwise 0. */ -+/* The fast path's trampoline page (tools/proot/fastpath): a traced syscall made from it was -+ * already translated inside the tracee and is let through without a stop. With PROOT_FASTPATH -+ * set, every SECCOMP_RET_TRACE is preceded by a check of the caller's address. The check sits -+ * after the syscall-number dispatch, so syscalls proot never traces stay constant-ALLOW and keep -+ * the kernel's seccomp action cache. FASTPATH_STUB_ADDR must match FP_STUB_ADDR in fastpath.c. */ ++ ++ ++ ++ ++ +#define FASTPATH_STUB_ADDR 0xffff00000ULL +#define FASTPATH_STUB_SIZE 0x1000ULL +#define LENGTH_FASTPATH_TAIL 7 @@ -102,8 +102,8 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. +static int add_statements_raw(struct sock_fprog *program, size_t nb_statements, + struct sock_filter *statements); + -+/* Append @statements, each RET_TRACE expanded into the trampoline check when the fast path is -+ * on, every jump within (or out of) the block adjusted for the expansion. */ ++ ++ static int add_statements(struct sock_fprog *program, size_t nb_statements, struct sock_filter *statements) { @@ -176,7 +176,7 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. + static size_t trace_length(Sysnum sysnum) { -+ /* Each trace block has exactly one RET_TRACE (see add_statements). */ ++ + return trace_length_raw(sysnum) + (fastpath_enabled() ? LENGTH_FASTPATH_TAIL - 1 : 0); +} + @@ -191,9 +191,9 @@ resolve where proot resolves them. Without PROOT_FASTPATH nothing changes. if (getenv("PROOT_NO_SECCOMP") == NULL) (void) enable_syscall_filtering(tracee); -+ /* The fast path resolves relative paths against the -+ * kernel's cwd: start it at the guest's (see chdir in -+ * syscall/enter.c). */ ++ ++ ++ + if (getenv("PROOT_FASTPATH") != NULL) { + char host_cwd[PATH_MAX]; + if (translate_path(tracee, host_cwd, AT_FDCWD, tracee->fs->cwd, true) >= 0) diff --git a/tools/quest/android_wrapper.c b/tools/quest/android_wrapper.c index 241ad7846..6f211b1be 100644 --- a/tools/quest/android_wrapper.c +++ b/tools/quest/android_wrapper.c @@ -1,7 +1,4 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later - * Loaded as the AdrenoTools driver; DT_NEEDED points to the original Turnip. - * Vulkan/HAL symbols are resolved from that dependency. Its ioctl calls resolve here. - */ +/* SPDX-License-Identifier: GPL-3.0-or-later */ #define _GNU_SOURCE #include "kgsl_compat.h" #include <dlfcn.h> diff --git a/tools/quest/build-driver.py b/tools/quest/build-driver.py index f5376f20b..e72b2991e 100644 --- a/tools/quest/build-driver.py +++ b/tools/quest/build-driver.py @@ -94,7 +94,6 @@ def build(ndk, output=OUTPUT): with tempfile.TemporaryDirectory(prefix='quest-driver-') as folder: work = Path(folder) with tarfile.open(fileobj=io.BytesIO(data)) as archive: - # Only read the known driver entry; no archive paths are extracted. driver = archive.extractfile('vulkan.ad07xx.so').read() (work / 'vulkan.ad07xx.so').write_bytes(driver) wrapper = work / 'libquest_turnip.so' diff --git a/tools/quest/kgsl_compat.c b/tools/quest/kgsl_compat.c index 4b1ab6684..33f4cfe18 100644 --- a/tools/quest/kgsl_compat.c +++ b/tools/quest/kgsl_compat.c @@ -1,8 +1,4 @@ -/* SPDX-License-Identifier: GPL-3.0-or-later - * Quest experimental build, 2026-10-11. - * Horizon permits GPUOBJ allocation, but rejects the legacy GPUMEM alloc/free calls. - * Retry only those two calls after EACCES/EPERM. No change to device permissions. - */ +/* SPDX-License-Identifier: GPL-3.0-or-later */ #include "kgsl_compat.h" #include <errno.h> #include <limits.h> @@ -22,7 +18,6 @@ _Static_assert(sizeof(struct kgsl_gpuobj_info) == 48, "GPUOBJ info ABI"); static void report(const char *operation, int error) { static unsigned successes, failures; unsigned *counter = error ? &failures : &successes; - /* Log the first success and at most eight failures per process. */ if (__atomic_fetch_add(counter, 1, __ATOMIC_RELAXED) >= (error ? 8u : 1u)) return; int saved = errno; #ifdef __ANDROID__ @@ -52,7 +47,6 @@ int quest_kgsl_ioctl(quest_ioctl_fn real, int fd, unsigned long request, void *a error = EOVERFLOW; } if (rc != 0) { - /* The caller never receives this allocation, so clean it up on an INFO failure. */ struct kgsl_gpuobj_free release = {.id = alloc.id}; if (real(fd, IOCTL_KGSL_GPUOBJ_FREE, &release) != 0) report("cleanup failed", errno); diff --git a/tools/quest/kgsl_compat.h b/tools/quest/kgsl_compat.h index f54e334ec..b6f0eb053 100644 --- a/tools/quest/kgsl_compat.h +++ b/tools/quest/kgsl_compat.h @@ -2,7 +2,6 @@ #ifndef DROIDDECK_QUEST_KGSL_COMPAT_H #define DROIDDECK_QUEST_KGSL_COMPAT_H -/* Use the vendored Qualcomm UAPI, including its architecture-dependent ioctl sizes. */ #ifndef __user #define __user #endif diff --git a/tools/release/axml_rename.py b/tools/release/axml_rename.py index 92da2753a..24fba660c 100644 --- a/tools/release/axml_rename.py +++ b/tools/release/axml_rename.py @@ -1,6 +1,4 @@ #!/usr/bin/env python3 -# axml_rename.py in.apk out.apk old=new ... : rewrite whole strings in AndroidManifest.xml's string -# pool (binary XML), leaving every other entry of the apk byte-identical. Unsigned output. import struct, sys, zipfile def dec_len8(b, o): diff --git a/tools/release/runtime_inputs.py b/tools/release/runtime_inputs.py index eac1c27c8..6c7944ce1 100644 --- a/tools/release/runtime_inputs.py +++ b/tools/release/runtime_inputs.py @@ -95,7 +95,6 @@ def required(): def record(root=ROOT): - # Refresh scripts even on a CI native-cache hit; these are source, not cached binaries. if root == ROOT: from check_session_assets import source_assets for source, asset in source_assets().items(): @@ -112,7 +111,6 @@ def record(root=ROOT): gzip.decompress((root / path).read_bytes()) except (OSError, EOFError) as error: raise ValueError('Invalid desktop removal inventory: ' + name) from error - # Check inside the compressed payload, where a plain Gradle build used to ship no sinks. process = subprocess.Popen(['zstd', '-dc', str(root / paths['assets/pulseaudio.tzst'])], stdout=subprocess.PIPE) try: with tarfile.open(fileobj=process.stdout, mode='r|') as archive: diff --git a/tools/release/sign-apk.sh b/tools/release/sign-apk.sh index 63475b400..351e2fb0b 100755 --- a/tools/release/sign-apk.sh +++ b/tools/release/sign-apk.sh @@ -1,20 +1,4 @@ #!/usr/bin/env bash -# sign-apk.sh <in.apk> <variant> <out.apk> -# -# Turns a CI build (signed with the public AOSP testkey) into one package of tools/release/variants.txt -# signed with DroidDeck's own key, carrying the hand-over from the testkey: -# - v1 + v2 are signed by the testkey, v3 by our key, with a signed lineage testkey -> our key; -# - an install from before (testkey) updates to it and keeps its data; -# - the testkey loses its rollback right, so an apk signed with the testkey alone - which anyone -# can make - is refused as an update to it (device-proven on the Pocket FIT, 2026-09-24); -# - --rotation-min-sdk-version 28: the lineage applies from Android 9, not only from 13. -# -# The key comes from the environment and is never written anywhere but the path given: -# RELEASE_KEYSTORE path to the PKCS12 keystore -# RELEASE_STORE_PASSWORD its password (the key has the same one: PKCS12) -# RELEASE_KEY_ALIAS the key's alias -# BUILD_TOOLS Android build-tools dir (zipalign, apksigner, aapt) -# RELEASE_SIGNER_SHA256 optional: the expected certificate digest, else keystore/release-signer.sha256 set -euo pipefail die() { echo "::error::$*" >&2; exit 1; } @@ -37,7 +21,6 @@ export TESTKEY_PASSWORD=android work=$(mktemp -d) trap 'rm -rf "$work"' EXIT -# The package name and everything the manifest derives from it; component class names stay. P=com.droiddeck.launcher if [ "$pkg" = "$P" ]; then python3 "$here/axml_rename.py" "$in" "$work/renamed.apk" @@ -50,7 +33,6 @@ fi got=$("$BUILD_TOOLS/aapt" dump badging "$work/renamed.apk" | sed -n "s/^package: name='\([^']*\)'.*/\1/p") [ "$got" = "$pkg" ] || die "$variant: the apk says package '$got', not '$pkg'" if [ "$pkg" != "$P" ]; then - # A provider left on the old authority would clash with the standard app installed beside it. tree=$("$BUILD_TOOLS/aapt" dump xmltree "$work/renamed.apk" AndroidManifest.xml) grep -E 'android:(authorities|taskAffinity)' <<<"$tree" | grep -F "\"$P." \ && die "$variant: the manifest still names $P in an authority or task" @@ -58,8 +40,6 @@ fi "$BUILD_TOOLS/zipalign" -p -f 4 "$work/renamed.apk" "$work/aligned.apk" -# The lineage: the testkey vouches for our key. Made fresh each time (it is two certificates and -# a signature, nothing secret beyond the key that signs it). "$BUILD_TOOLS/apksigner" rotate --out "$work/lineage" \ --old-signer --ks "$TESTKEY" --ks-type PKCS12 --ks-pass env:TESTKEY_PASSWORD \ --ks-key-alias testkey --key-pass env:TESTKEY_PASSWORD --set-rollback false \ @@ -76,13 +56,9 @@ fi --out "$out" "$work/aligned.apk" rm -f "$out.idsig" -# What the apk must carry, checked rather than assumed. "$BUILD_TOOLS/apksigner" verify --min-sdk-version 26 "$out" >/dev/null || die "$variant: does not verify" -# (Listed first, then searched: grep -q stops reading early, which fails a pipe under pipefail.) listing=$(unzip -l "$out") grep -qE 'META-INF/.*\.(SF|RSA)$' <<<"$listing" || die "$variant: no JAR signature (old installers)" -# Android 9+ must see our key and only our key. apksigner labels signers "Signer #1", "Signer -# (minSdkVersion=28, ...)" or "V3.0 Signer:" depending on its version; the digests are what count. certs=$("$BUILD_TOOLS/apksigner" verify --min-sdk-version 28 --print-certs "$out") v3=$(sed -n 's/^.*Signer.* certificate SHA-256 digest: //p' <<<"$certs" | sort -u) if [ "$v3" != "$expected" ]; then diff --git a/tools/release/update_catalog.py b/tools/release/update_catalog.py index b16e4eab3..808e576a0 100755 --- a/tools/release/update_catalog.py +++ b/tools/release/update_catalog.py @@ -368,8 +368,6 @@ def build_catalog(source: GitHub, ci: GitHub, source_repo: str, ci_repo: str) -> if version_code_text and version_code_text.isdigit(): version_code = int(version_code_text) else: - # Legacy test releases did not record it. If the PR did not change build.gradle, - # the base value is exact; otherwise use the PR head's value. version_code = base_code try: changed = source.get(f"/repos/{source_repo}/pulls/{pr}/files?per_page=100") diff --git a/tools/release/variants.txt b/tools/release/variants.txt index 607106800..0df71a299 100644 --- a/tools/release/variants.txt +++ b/tools/release/variants.txt @@ -1,8 +1,3 @@ -# The packages a signed build ships as. The same apk, with only the manifest's package name (and -# the names derived from it) changed: some phones give their performance modes only to apps with -# certain names. Each is its own app to Android - installs beside the others, with its own data. -# -# name package file suffix standard com.droiddeck.launcher - pubg com.tencent.ig -pubg antutu com.antutu.benchmark.full -antutu diff --git a/tools/test_local.sh b/tools/test_local.sh index a2a69da62..bd29aef7d 100755 --- a/tools/test_local.sh +++ b/tools/test_local.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -# Session helpers and ELF preload tests need Linux (/proc, inotify and glibc), as in CI. set -euo pipefail repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) cd "$repo_root" diff --git a/tools/tests/effects_source_cache_harness.c b/tools/tests/effects_source_cache_harness.c index e45e4bb78..1d355c244 100644 --- a/tools/tests/effects_source_cache_harness.c +++ b/tools/tests/effects_source_cache_harness.c @@ -3,7 +3,6 @@ #include <stdio.h> #include <string.h> -/* Exercise the real cache with Vulkan lifetime callbacks, without an Android surface or GPU. */ #define VK_PRESENT_H const char *vkp_gpu_name(void) { return "cache test"; } void droiddeck_log(const char *tag, const char *fmt, ...) { (void)tag; (void)fmt; } @@ -61,7 +60,6 @@ int main(int argc, char **argv) { assert(scene_ensure(source, 1280, 720) == 0); VkImageView original = bound_view; if (!strcmp(argv[1], "recycled")) { - /* The owner destroys the image, then Vulkan reuses its handle at the same size. */ vkp_effects_forget_image(source); assert(freed_sets == 1 && freed_views == 1); assert(scene_ensure(source, 1280, 720) == 0); diff --git a/tools/tests/sysv_otime_harness.c b/tools/tests/sysv_otime_harness.c index 2c362189e..6fa57ca73 100644 --- a/tools/tests/sysv_otime_harness.c +++ b/tools/tests/sysv_otime_harness.c @@ -1,6 +1,3 @@ -/* Host test for sysv.c: IPC_STAT's sem_otime turns non-zero with the first semop, which the usual - * race-free init (and SteamVR's web helper) waits for. Linked with sysv.c, whose semget/semop/semctl - * replace libc's. */ #define _GNU_SOURCE #include <stdio.h> #include <sys/ipc.h> diff --git a/tools/tests/termux_jni_harness.c b/tools/tests/termux_jni_harness.c index 66dc923c8..049419ed2 100644 --- a/tools/tests/termux_jni_harness.c +++ b/tools/tests/termux_jni_harness.c @@ -1,6 +1,3 @@ -// Drives Java_com_termux_terminal_JNI_createSubprocess against a mock JNIEnv that tracks local -// references the way ART does, so the JNI hygiene of app/src/main/cpp/termux/termux.c can be -// checked on the host. Built and run by tools/tests/test_termux_jni.py. #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -26,7 +23,6 @@ static jsize mock_array_length(JNIEnv* TERMUX_UNUSED(env), jarray array) static jobject mock_get_element(JNIEnv* TERMUX_UNUSED(env), jobjectArray TERMUX_UNUSED(array), jsize TERMUX_UNUSED(index)) { if (++live_refs > peak_refs) peak_refs = live_refs; - // ART aborts the process here: "local reference table overflow (max=512)". if (live_refs > LOCAL_REF_LIMIT) overflowed = 1; return (jobject) malloc(1); } @@ -53,7 +49,6 @@ static void mock_release_utf(JNIEnv* TERMUX_UNUSED(env), jstring string, const c { for (size_t i = 0; i < sizeof(pinned) / sizeof(pinned[0]); i++) { if (pinned[i].owner != NULL && pinned[i].chars == chars) { - // Releasing chars against some other string is a JNI contract violation. if (pinned[i].owner != string) mismatched_releases++; free(pinned[i].chars); pinned[i].owner = NULL; @@ -83,7 +78,6 @@ int main(int argc, char** argv) jsize arg_count = atoi(argv[1]); jsize env_count = atoi(argv[2]); - // The struct is JNINativeInterface in the NDK's header and JNINativeInterface_ in the JDK's. __typeof__(**(JNIEnv*) 0) table = { .GetArrayLength = mock_array_length, .GetObjectArrayElement = mock_get_element, @@ -97,7 +91,6 @@ int main(int argc, char** argv) }; const __typeof__(table)* env = &table; - // Arrays are modelled as their length; the caller's own references are not counted. jsize args = arg_count, envs = env_count; jint pid_slot[1] = {0}; jstring cmd = (jstring) "cmd", cwd = (jstring) "cwd"; diff --git a/tools/tests/test_binfmt_preload.py b/tools/tests/test_binfmt_preload.py index b1b0c0940..3a940b6b8 100644 --- a/tools/tests/test_binfmt_preload.py +++ b/tools/tests/test_binfmt_preload.py @@ -46,7 +46,6 @@ int main(int argc, char **argv) { } """ -# Shell builtins only: anything it ran would be an x86 program, handed back to it. HANDLER = "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$HANDLER_LOG\"\nexit 7\n" diff --git a/tools/tests/test_cardboard.py b/tools/tests/test_cardboard.py index fa6f3d120..8e1d26080 100644 --- a/tools/tests/test_cardboard.py +++ b/tools/tests/test_cardboard.py @@ -28,8 +28,6 @@ class CardboardParamsTest(unittest.TestCase): @staticmethod def _env(folder): - # cardboard.h includes <vulkan/vulkan.h>; the host test only needs the types, so a stub - # header stands in when the Vulkan SDK is not installed. import os stub = Path(folder) / 'vulkan' stub.mkdir(exist_ok=True) @@ -50,7 +48,6 @@ class CardboardShaderTest(unittest.TestCase): 'cardboard_frag.h is stale: regenerate it from cardboard.frag (see cardboard.c)') def test_push_constants_fit_the_guaranteed_range(self): - # 7 vec4 = 112 bytes in the shader; Vulkan guarantees 128. glsl = (SRC / 'cardboard.frag').read_text() block = glsl[glsl.index('uniform PC {'):glsl.index('} pc;')] self.assertLessEqual(16 * len(re.findall(r'\bvec4\b', block)), 128) diff --git a/tools/tests/test_desktop_removal.py b/tools/tests/test_desktop_removal.py index 674b9643f..845e08357 100644 --- a/tools/tests/test_desktop_removal.py +++ b/tools/tests/test_desktop_removal.py @@ -13,8 +13,6 @@ class DesktopRemovalInventoryTest(unittest.TestCase): self.assertIn('usr/bin/lxqt-session', paths) self.assertIn('usr/bin/labwc', paths) self.assertIn('usr/local/bin/steamdeck-desktop', paths) - # The runtime pacman database did not protect libseat on the Thor. Removing it - # made gamescope and every Steam/native session exit 127 after desktop cleanup. self.assertNotIn('usr/lib/libseat.so.1', paths) self.assertNotIn('usr/lib/libseat.so', paths) self.assertFalse(any(p.startswith('usr/lib/libSDL2') for p in paths)) diff --git a/tools/tests/test_dircache_preload.py b/tools/tests/test_dircache_preload.py index 62792888c..50ad24d5f 100644 --- a/tools/tests/test_dircache_preload.py +++ b/tools/tests/test_dircache_preload.py @@ -21,7 +21,6 @@ FAKE = r""" int real_opendirs; -/* COARSE=1: directory stats that never change, like NTFS's whole-second times and fixed size. */ static void coarse(const char *path, struct stat *st) { if (getenv("COARSE") && strstr(path, "/fuse/") && S_ISDIR(st->st_mode)) { st->st_mtim = st->st_ctim = (struct timespec){1, 0}; @@ -70,7 +69,6 @@ int statfs(const char *path, struct statfs *out) { return r; } -/* FASTPATH=1: open directories the way the fast-path preload does, through fdopendir. */ DIR *opendir(const char *path) { static DIR *(*next)(const char *); if (!next) next = dlsym(RTLD_NEXT, "opendir"); diff --git a/tools/tests/test_droiddeckctl.py b/tools/tests/test_droiddeckctl.py index 9e3c87ac9..02400452d 100644 --- a/tools/tests/test_droiddeckctl.py +++ b/tools/tests/test_droiddeckctl.py @@ -28,8 +28,6 @@ def load(name): ctl = load("droiddeckctl") scenario = load("droiddeck-scenario") -# A stand-in for adb: answers the provider's content calls from a JSON config (a list answers -# successive calls), records every invocation, and fakes the few shell commands droiddeckctl uses. FAKE_ADB = textwrap.dedent('''\ #!/usr/bin/env python3 import base64, json, os, sys @@ -136,7 +134,6 @@ class DroiddeckctlTest(FakeAdbTest): "launch": {"ok": True, "command": "launch", "appId": "489830"}, "state": [state(focused=769, focusable=[769]), state(focused=489830, focusable=[769])], }) - # Focus alone is not enough: the game must also be in the list Steam orders from. code, out = self.run_ctl("launch", "489830", "--wait", "--timeout", "1") self.assertEqual(5, code) self.assertEqual("TIMEOUT", out["error"]["code"]) @@ -333,7 +330,7 @@ class ScenarioTest(FakeAdbTest): "steps": [["state"], {"assert": "session.focus.focusedApp", "equals": 489830}]}) code, report = self.run_scenario(path) self.assertEqual(1, code) - self.assertEqual(1, len(report["runs"])) # stops at the first failure without --keep-going + self.assertEqual(1, len(report["runs"])) self.assertIn("is 769", report["runs"][0]["error"]) evidence = json.loads((Path(report["runs"][0]["folder"]) / "evidence.json").read_text()) self.assertEqual(769, evidence["focus"]["focusedApp"]) diff --git a/tools/tests/test_effects_source_cache.py b/tools/tests/test_effects_source_cache.py index 4ad95710a..d42045b1c 100644 --- a/tools/tests/test_effects_source_cache.py +++ b/tools/tests/test_effects_source_cache.py @@ -29,7 +29,6 @@ class EffectsSourceCacheTest(unittest.TestCase): cls.temp = tempfile.TemporaryDirectory(prefix='droiddeck-effects-cache-') cls.addClassCleanup(cls.temp.cleanup) folder = Path(cls.temp.name) - # Copy only Vulkan headers so Android's libc headers cannot shadow the host's libc. shutil.copytree(headers, folder / 'include/vulkan', ignore=shutil.ignore_patterns('*.hpp')) if (headers.parent / 'vk_video').is_dir(): shutil.copytree(headers.parent / 'vk_video', folder / 'include/vk_video') diff --git a/tools/tests/test_game_environment.py b/tools/tests/test_game_environment.py index 6b2362faa..06e063339 100644 --- a/tools/tests/test_game_environment.py +++ b/tools/tests/test_game_environment.py @@ -341,8 +341,6 @@ class GameEnvironmentTest(unittest.TestCase): self.assertIn("GE-Proton11-7", [entry["name"] for entry in COMPAT["tool_catalog"](str(home), None)]) def test_droiddeck_proton_is_adopted_and_labelled_like_any_other_extra(self): - # Its folder has a space ("DroidDeck-Proton 11-7.5") and its internal name starts with - # "DroidDeck-Proton-", which lower-cases to our own tools' prefix: neither may matter. with tempfile.TemporaryDirectory() as tmp: home = Path(tmp) (home / "label").write_text("Compatible\n") @@ -370,15 +368,12 @@ class GameEnvironmentTest(unittest.TestCase): config.write_text(config_text([("0", COMPAT["TOOL"]), ("42", "DroidDeck-Proton-11-7.5")])) COMPAT["register_default"](str(config), ["42", "43"], protect=protect) self.assertEqual(mapping_of(config.read_text()), {"0": COMPAT["TOOL"], "42": "DroidDeck-Proton-11-7.5", "43": COMPAT["TOOL"]}) - # Once removed it is no longer runnable, and its games go back to the default. changes, _ = COMPAT["plan_mapping"]({"0": COMPAT["TOOL"], "42": "DroidDeck-Proton-11-7.5"}, ["42"], [COMPAT["TOOL"]], COMPAT["TOOL"], {}) self.assertEqual(changes, {"42": COMPAT["TOOL"]}) def test_both_proton_wrappers_call_environment_launcher(self): for script in (COMPAT["LAUNCHER_SH"], COMPAT["EXTRA_WRAPPER_SH"] % "proton"): subprocess.run(["bash", "-n"], input=script, text=True, check=True) - # Started through bl_run, which execs it - or, for a store game with cloud saves, waits - # for it so droiddeck-store-launch --exited can follow (test_store_launch.CloudHooks). self.assertIn('bl_run ${BL_TASKSET:-} /usr/local/bin/droiddeck-game-env', script) def test_wrappers_preload_the_session_library_before_the_input_shim(self): @@ -564,8 +559,6 @@ class DirectAudioPrefixTest(unittest.TestCase): wine = self.tool / "files/bin-arm64/wine" wine.write_text("#!/bin/sh\necho wine-11.0-4c8f2e1 '(Staging)'\n") wine.chmod(0o755) - # Proton's own winepulse.so names the audio interface: the classic table never imports - # PsCreateSystemThread, the system-thread one (Proton-CachyOS) always does. (self.tool / "files/lib/wine/aarch64-unix").mkdir(parents=True) self.winepulse = self.tool / "files/lib/wine/aarch64-unix/winepulse.so" self.winepulse.write_bytes(b"\x7fELF classic wine 11 pulse driver") @@ -607,8 +600,6 @@ class DirectAudioPrefixTest(unittest.TestCase): result = self.run_setup() self.assertIn("DirectAudio selected", result.stderr) self.assertIn("WINEDLLPATH=%s/lib/wine" % self.audio, result.stdout) - # Proton-CachyOS's mmdevapi takes the driver list from this variable, and its launcher - # pre-sets it to pulse,alsa unless it is already set. self.assertIn("WINE_AUDIO_DRIVER=directaudio,pulse\n", result.stdout) self.assertEqual(self.link("system32"), str(self.audio / "lib/wine/aarch64-windows/winedirectaudio.drv")) self.assertEqual(self.link("syswow64"), str(self.audio / "lib/wine/i386-windows/winedirectaudio.drv")) @@ -701,16 +692,11 @@ class StartWithoutSteamTest(unittest.TestCase): game = [str(ge), "waitforexitandrun", "/games/Game.exe", "-windowed"] self.assertEqual(start(game, {}, "3384071994"), (game, {})) on = {"DROIDDECK_NO_STEAM": "1"} - # An added game (a shortcut id) has no Steam fixes: UMU_ID 0. The command is unchanged. self.assertEqual(start(game, on, "3384071994"), (game, dict(on, UMU_ID="0"))) - # A Steam game keeps its protonfixes under its own id. self.assertEqual(start(game, on, "570940")[1]["UMU_ID"], "570940") - # A UMU_ID the user set wins. self.assertEqual(start(game, dict(on, UMU_ID="umu-1"), "570940")[1]["UMU_ID"], "umu-1") - # Valve's Proton has no umu launch: the game runs straight in the prefix. plain = [str(valve)] + game[1:] self.assertEqual(start(plain, on, "570940"), ([str(valve), "runinprefix", "/games/Game.exe", "-windowed"], on)) - # Only a game launch: the client's own runs and other verbs are left alone. for other in ([str(ge), "run", "/x.exe"], [str(ge), "waitforexitandrun"], [str(valve), "getcompatpath", "/x"]): self.assertEqual(start(other, on, "570940"), (other, on)) diff --git a/tools/tests/test_launcher_cores.py b/tools/tests/test_launcher_cores.py index 98656b745..4f05df242 100644 --- a/tools/tests/test_launcher_cores.py +++ b/tools/tests/test_launcher_cores.py @@ -48,7 +48,7 @@ class LauncherCoresTest(unittest.TestCase): self.proc = self.root / 'proc' self.proc.mkdir() cpufreq = self.root / 'cpu' - for cpu in ALL: # 0-5 one cluster, 6-7 the fast one + for cpu in ALL: (cpufreq / f'cpu{cpu}/cpufreq').mkdir(parents=True) (cpufreq / f'cpu{cpu}/cpufreq/cpuinfo_max_freq').write_text('3532800\n' if cpu < 6 else '4320000\n') self.cores = FakeCores() @@ -118,7 +118,7 @@ class LauncherCoresTest(unittest.TestCase): self.process(100, LAUNCHER) self.process(200, HELPER) self.process(9, '/root/.local/share/Steam/steamrtarm64/steam') - self.governor.tick() # no focus file yet + self.governor.tick() self.focus_on(9, app=769) self.governor.tick() self.focus_on(100) @@ -141,7 +141,6 @@ class LauncherCoresTest(unittest.TestCase): self.process(400, GAME) self.focus_on(400) self.governor.tick() - # A new thread, and a new helper process the held one started: both born held. self.thread(200, 203, {5}, NICE) self.process(500, HELPER, (501,), cpus={5}, nice=NICE) self.now += 10 @@ -156,16 +155,16 @@ class LauncherCoresTest(unittest.TestCase): self.process(400, GAME) self.focus_on(400) self.governor.tick() - self.cores.state[201] = (ALL, -5) # the program raised a thread's priority itself + self.cores.state[201] = (ALL, -5) self.now += 5 self.governor.tick() - self.assertFalse(self.held(201)) # not looked at again before the rescan + self.assertFalse(self.held(201)) self.now += 5 self.governor.tick() self.assertTrue(self.held(201)) self.focus_on(200) self.governor.tick() - self.assertEqual((ALL, 0), self.cores.state[201]) # its first state, not the program's + self.assertEqual((ALL, 0), self.cores.state[201]) def test_a_steady_hold_is_not_rewritten(self): self.process(200, HELPER, (201,)) @@ -182,7 +181,7 @@ class LauncherCoresTest(unittest.TestCase): self.process(200, HELPER, (201,)) self.process(300, HELPER) self.process(400, GAME) - del self.cores.state[201] # ended between the listing and the call + del self.cores.state[201] self.focus_on(400) self.governor.tick() self.assertNotIn(201, self.holder.held[200]) diff --git a/tools/tests/test_login1.py b/tools/tests/test_login1.py index 947fd8eeb..abfdc70a2 100644 --- a/tools/tests/test_login1.py +++ b/tools/tests/test_login1.py @@ -80,7 +80,7 @@ class SleepHandshakeTest(unittest.TestCase): token = self.suspend() self.assertTrue(self.preparing()) self.state(token, "paused") - self.state("0" * 32, "awake") # stale session acknowledgments cannot wake this one + self.state("0" * 32, "awake") time.sleep(0.1) self.assertTrue(self.preparing()) self.state(token, "awake") diff --git a/tools/tests/test_msi_install.py b/tools/tests/test_msi_install.py index b896288eb..dda3eeea5 100644 --- a/tools/tests/test_msi_install.py +++ b/tools/tests/test_msi_install.py @@ -120,9 +120,6 @@ class LayoutTest(unittest.TestCase): self.assertEqual(inst.included_components, {"Up", "Wide"}) def test_a_duplicate_onto_the_file_itself_is_skipped(self): - # The .NET host packages (dotnet-host-*-win-x86.msi) duplicate dotnet.exe into its own - # folder under its own name; copying it onto itself raised SameFileError and stopped every - # .NET Core / Desktop runtime install. with tempfile.TemporaryDirectory() as tmp: src = Path(tmp) / "cab" / "dotnet.exe" src.parent.mkdir() @@ -269,13 +266,11 @@ class RegistryApplyTest(unittest.TestCase): text = (self.pfx / "system.reg").read_text() self.assertIn('"Version"="4.0"', text) self.assertIn('"Kept"="mine"', text) - # Through the Wow6432Node\Classes link into the shared Classes\Wow6432Node key, never under the link. self.assertIn('[Software\\\\Classes\\\\Wow6432Node\\\\CLSID\\\\{X}]', text) self.assertNotIn('[Software\\\\Wow6432Node\\\\Classes\\\\CLSID', text) self.assertIn('"PATH"=str(2):"C:\\\\PhysX;%SystemRoot%\\\\system32;%SystemRoot%"', text) self.assertNotIn("[System\\\\CurrentControlSet\\\\", text) self.assertIn('"Opt"=dword:00000002', (self.pfx / "user.reg").read_text()) - # The prefix's own spelling of Program Files (x86) is kept. self.assertTrue((self.pfx / "drive_c/program files (x86)/Microsoft XNA/xna.dll").is_file()) self.assertFalse((self.pfx / "drive_c/Program Files (x86)").exists()) diff --git a/tools/tests/test_netmanager.py b/tools/tests/test_netmanager.py index 23ce08070..69159fdfc 100644 --- a/tools/tests/test_netmanager.py +++ b/tools/tests/test_netmanager.py @@ -117,7 +117,6 @@ class NetworkObjectsTest(unittest.TestCase): self.assertTrue(secured["RsnFlags"].unpack() & 0x100) self.assertTrue(secured["RsnFlags"].unpack() & 0x400) self.assertEqual(found[nm.ap_path(other["bssid"])][nm.NAME + ".AccessPoint"]["Flags"].unpack(), 0) - # Keep nearby-network discovery while a cellular connection provides the default route. cellular = nm.objects(dict(CELLULAR, accessPoints=[other])) self.assertIn(nm.ap_path(other["bssid"]), cellular) self.assertEqual(cellular[nm.ROOT + "/Devices/Wifi"][nm.NAME + ".Device.Wireless"]["ActiveAccessPoint"].unpack(), "/") @@ -131,7 +130,6 @@ class NetworkBusTest(unittest.TestCase): self.write(WIFI) self.bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) env = dict(os.environ, DBUS_SYSTEM_BUS_ADDRESS=os.environ["DBUS_SESSION_BUS_ADDRESS"]) - # Use the production service and only redirect its input file for the fixture. launch = "import runpy,sys; from pathlib import Path; n=runpy.run_path(sys.argv[1]); g=n['main'].__globals__; g['SNAPSHOT']=Path(sys.argv[2]); g['SCAN_REQUEST']=Path(sys.argv[2]).with_suffix('.request'); n['main']()" self.process = subprocess.Popen([sys.executable, "-c", launch, str(SCRIPT), str(self.snapshot)], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -217,7 +215,6 @@ class NetworkBusTest(unittest.TestCase): self.assertTrue(any(interface == nm.OBJECT_MANAGER and member == "InterfacesRemoved" and args[0] == nm.AP for _, interface, member, args in signals)) self.assertTrue(any(member == "AccessPointAdded" for _, _, member, _ in signals)) - # A missing/invalid snapshot is a transient input error, never a fake Ethernet handover. self.snapshot.write_text("{") time.sleep(1.2) self.assertEqual(self.prop(nm.ROOT, nm.NAME, "PrimaryConnectionType"), "802-11-wireless") @@ -250,8 +247,6 @@ class NetworkBusTest(unittest.TestCase): from gi.repository import NM except ValueError: self.skipTest("requires gir1.2-nm-1.0") - # libnm is a real consumer of the same API Steam reads. It caches objects by path, - # so the handover must replace the Wi-Fi device with a modem rather than mutate its type. previous = os.environ.get("DBUS_SYSTEM_BUS_ADDRESS") os.environ["DBUS_SYSTEM_BUS_ADDRESS"] = os.environ["DBUS_SESSION_BUS_ADDRESS"] try: diff --git a/tools/tests/test_proton_extra.py b/tools/tests/test_proton_extra.py index dab3e2c95..5d8cc95f2 100644 --- a/tools/tests/test_proton_extra.py +++ b/tools/tests/test_proton_extra.py @@ -89,7 +89,6 @@ class ProtonExtraTest(unittest.TestCase): steam = Path(tmp) / "Steam" adopted = [] real = subprocess.call - # tar runs for real; steam-compatibility (the adoption step) is only recorded. fake = lambda args, **kw: adopted.append(args) or 0 if args[0] == "steam-compatibility" else real(args, **kw) with mock.patch.object(GLOBALS["subprocess"], "call", fake), contextlib.redirect_stdout(io.StringIO()) as said: with mock.patch.object(GLOBALS["sys"], "argv", ["droiddeck-proton-extra", str(steam), str(tarball)]): @@ -98,7 +97,6 @@ class ProtonExtraTest(unittest.TestCase): self.assertEqual(sorted(os.listdir(tools)), [".droiddeck-download", "DroidDeck-Proton 11-7.5"]) self.assertTrue((tools / "DroidDeck-Proton 11-7.5" / "toolmanifest.vdf").is_file()) self.assertEqual(adopted, [["steam-compatibility", str(steam)]]) - # The app reads the folder from this line, space and all. self.assertIn("droiddeck-proton-extra: installed DroidDeck-Proton 11-7.5\n", said.getvalue()) self.assertEqual(EXTRA["installed"](str(tools)), ["DroidDeck-Proton 11-7.5"]) diff --git a/tools/tests/test_seed_redists.py b/tools/tests/test_seed_redists.py index 1f9e2ae71..248e3f75a 100644 --- a/tools/tests/test_seed_redists.py +++ b/tools/tests/test_seed_redists.py @@ -22,8 +22,6 @@ GAME_SCRIPT = r'''"InstallScript" } ''' -# Red Dead Redemption 2's own script: Proton needs Khronos' vulkan-1.dll for it, which only the -# Vulkan runtime installer puts in the prefix. VULKAN_SCRIPT = r'''"InstallScript" { "Run Process" @@ -148,7 +146,6 @@ class SeedRedistsTest(unittest.TestCase): self.assertIn('"Other"=dword:00000001', reg) self.assertNotIn("Vulkan111080", (self.reg.parent / ".droiddeck-redists").read_text()) self.assertEqual(int(self.reg.stat().st_mtime), 1_000_000_000) - # The client writes the marker itself once the installer has run; that one is left alone. with self.reg.open("a") as out: out.write('\n[Software\\\\Valve\\\\Steam\\\\Apps\\\\1174180] 1791730000\n"Vulkan111080"=dword:00000001\n') self.run_seed() diff --git a/tools/tests/test_session_guest_command.py b/tools/tests/test_session_guest_command.py index 536450824..9c319ce95 100644 --- a/tools/tests/test_session_guest_command.py +++ b/tools/tests/test_session_guest_command.py @@ -53,8 +53,6 @@ class SessionGuestCommandTest(unittest.TestCase): def test_child_receives_its_package_socket_after_environment_is_cleared(self): for package in PACKAGES: with self.subTest(package=package): - # env with no command prints the environment it would pass to the - # session. A misplaced assignment fails at exec, before it runs. result = subprocess.run( guest_prefix(package), capture_output=True, text=True, check=True, timeout=5, env={"DROIDDECK_BWRAP_SOCKET": "wrong.bwrap", "STALE_HOST": "1"}) diff --git a/tools/tests/test_session_tmp.py b/tools/tests/test_session_tmp.py index 58607911d..55caf4278 100644 --- a/tools/tests/test_session_tmp.py +++ b/tools/tests/test_session_tmp.py @@ -12,7 +12,7 @@ SESSION = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin/d STEAM = ["mangohud.Ab3dE6gH", ".com.valvesoftware.Steam.x1Y2z3/", "steam_chrome_shmem_uid10480_0", "steamADfAnd/"] CHROMIUM = ["scoped_dir3yu9Xt/SingletonSocket"] APPIMAGE = ["droiddeck-appimage.19PoOD"] -BUS = ["dbus-1zWfOP1IaR="] # = a socket +BUS = ["dbus-1zWfOP1IaR="] KDE = ["plasma-csd-generator.BVWIWA/"] STALE = STEAM + CHROMIUM + APPIMAGE + BUS + KDE KEPT = ["droiddeck-mangohud.conf", ".X11-unix/", "steamworks", "steamABCDEF/inside", "notes.txt", diff --git a/tools/tests/test_steam_desktop_launch.py b/tools/tests/test_steam_desktop_launch.py index 4461e0e4f..bf91609d1 100644 --- a/tools/tests/test_steam_desktop_launch.py +++ b/tools/tests/test_steam_desktop_launch.py @@ -107,7 +107,7 @@ class DesktopSteamLaunchTest(unittest.TestCase): def test_cef_software_is_scoped_to_the_kde_client(self): source = (BIN / 'droiddeck-session').read_text() - setup = source[source.index(' _bl_ui=(-gamepadui)'):source.index(' # Deck mode, always')] + setup = source[source.index(' _bl_ui=(-gamepadui)'):source.index(' if [ "${BL_STEAM_UI:-}" != desktop ]; then')] for desktop, ui, software in [('1', 'desktop', True), ('1', 'bigpicture', False), ('', 'desktop', False)]: with self.subTest(desktop=desktop, ui=ui): env = dict(self.env, BL_DESKTOP=desktop, BL_STEAM_UI=ui) diff --git a/tools/tests/test_steam_install.py b/tools/tests/test_steam_install.py index f3e29bbe2..03b2d694a 100644 --- a/tools/tests/test_steam_install.py +++ b/tools/tests/test_steam_install.py @@ -25,7 +25,6 @@ class SteamInstallTest(unittest.TestCase): self.requests = self.root / 'requests' self.bin = self.root / 'bin' self.bin.mkdir() - # Only the network is faked: exercise real manifest parsing, checksums and zip extraction. curl = self.bin / 'curl' curl.write_text('''#!/bin/bash set -eu @@ -33,7 +32,6 @@ printf '%s\\n' "$2" >> "$TEST_REQUESTS" cp "$TEST_CDN/${2##*/}" "$4" ''') curl.chmod(0o755) - # macOS has shasum; the guest and Linux CI have sha256sum. if shutil.which('sha256sum') is None: sha = self.bin / 'sha256sum' sha.write_text('#!/bin/bash\nexec shasum -a 256 "$@"\n') @@ -78,7 +76,6 @@ cp "$TEST_CDN/${2##*/}" "$4" self.assert_installed() def test_an_install_from_another_branch_is_left_for_steam_to_move(self): - # An older app could install publicbeta; droiddeck-session moves the client at its start. self.assert_installed() requests = self.requests.read_text() (self.steam / 'package/beta').write_text('publicbeta\n') diff --git a/tools/tests/test_steam_language.py b/tools/tests/test_steam_language.py index 43b1e1a95..22d26e463 100644 --- a/tools/tests/test_steam_language.py +++ b/tools/tests/test_steam_language.py @@ -37,7 +37,6 @@ def value(text, *path): STEAM = ('Registry', 'HKCU', 'Software', 'Valve', 'Steam', 'language') GLOBAL = ('Registry', 'HKCU', 'Software', 'Valve', 'Steamsteamglobal', 'language') -# As the client leaves it on a device (trimmed): both keys, other values beside them, and HKLM. CLIENT = '''"Registry" { "HKLM" diff --git a/tools/tests/test_steam_shortcuts.py b/tools/tests/test_steam_shortcuts.py index 1213888f1..a00dbc9e7 100644 --- a/tools/tests/test_steam_shortcuts.py +++ b/tools/tests/test_steam_shortcuts.py @@ -172,7 +172,6 @@ class ShortcutsTest(unittest.TestCase): self.assertEqual('Example: Renamed', entry['AppName']) self.assertEqual('"%s/data"' % GUEST, entry['StartDir']) self.assertEqual('-dx11', entry['LaunchOptions']) - # One shortcut, the same appid. self.assertEqual([APPID], list(self.run_writer(game))) def test_a_target_chosen_in_the_app_updates_the_same_shortcut(self): diff --git a/tools/tests/test_store_launch.py b/tools/tests/test_store_launch.py index 014be591e..88e9f4156 100644 --- a/tools/tests/test_store_launch.py +++ b/tools/tests/test_store_launch.py @@ -56,7 +56,6 @@ class Detection(unittest.TestCase): def test_anything_else_is_left_alone(self): with tempfile.TemporaryDirectory() as root: folder = game(root) - # The exe of a store game is one too (a GOG game without a launcher); a stray file is not. self.assertEqual(folder, store.find_store_game([str(folder / 'Metalstorm.exe')])[0]) self.assertIsNone(store.find_store_game([str(Path(root) / 'elsewhere/readme.txt')])) (folder / '.droiddeck-store.json').unlink() @@ -65,7 +64,6 @@ class Detection(unittest.TestCase): def test_only_epic_asks_for_a_code(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog') - # Cloud saves off: GOG still prepares achievements, never an Epic exchange code. sc = json.loads((folder / '.droiddeck-store.json').read_text()); sc['cloud'] = False (folder / '.droiddeck-store.json').write_text(json.dumps(sc)) os.environ['BL_LAUNCH_DIR'] = str(Path(root) / 'session') @@ -136,7 +134,6 @@ class Prefix(unittest.TestCase): text = self.reg.read_text() self.assertIn('[Software\\\\Wine\\\\WineBrowser] ', text) self.assertIn('"Browsers"="/usr/local/bin/droiddeck-open-url,xdg-open"', text) - # A second launch adds nothing: the last word already says so. store.provision_prefix(str(self.compat)) self.assertEqual(text, self.reg.read_text()) @@ -158,7 +155,6 @@ class Choices(unittest.TestCase): folder = game(root) sidecar = json.loads((folder / '.droiddeck-store.json').read_text()) self.assertEqual({'eos': True, 'offline': False}, store.options(sidecar)) - # An earlier build's overlay field is read past. self.assertEqual({'eos': True, 'offline': False}, store.options({'epic': {'v': 2, 'overlay': True}})) sidecar['epic'] = {'eos': True, 'offline': True} sidecar['cloud'] = False @@ -175,7 +171,6 @@ class Choices(unittest.TestCase): finally: del os.environ['BL_LAUNCH_DIR'] del os.environ['STEAM_COMPAT_DATA_PATH'] - # Offline: no request was made and no code is left; the old overlay pointer is gone. self.assertFalse((Path(root) / 'session/stores/req').exists()) self.assertFalse((folder / '.droiddeck-epic-code').exists()) text = reg.read_text() @@ -200,7 +195,6 @@ class Browser(unittest.TestCase): class ExeTarget(unittest.TestCase): def test_a_game_started_by_its_exe_is_found_from_a_subfolder(self): - # Most GOG games have no launcher .bat: Steam runs the exe, which may sit in a subfolder. with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog', ident='1207664643') (folder / 'bin/x64').mkdir(parents=True) @@ -222,7 +216,6 @@ class CloudHooks(unittest.TestCase): original = store.ask asked = [] store.ask = lambda channel, request, timeout=5.0: asked.append((request, timeout)) or {'ok': True, 'files': 2, 'reason': 'ok'} - # 10: the launcher waits for Proton and calls --exited afterwards. self.assertEqual(store.STORE_GAME, store.main([bat])) self.assertEqual(0, store.main(['--exited', bat])) finally: @@ -309,7 +302,6 @@ class GogAchievements(unittest.TestCase): self.assertTrue((data / 'pfx/.droiddeck-comet-service').is_file()) self.assertEqual('ready', store.provision_galaxy(str(data), str(proton))) self.assertEqual(1, len(seen)) - # A recreated prefix must be provisioned again, even with the same Proton. __import__('shutil').rmtree(data / 'pfx') self.assertEqual('ready', store.provision_galaxy(str(data), str(proton))) self.assertEqual(2, len(seen)) diff --git a/tools/tests/test_termux_jni.py b/tools/tests/test_termux_jni.py index 084114006..6fd3d9787 100644 --- a/tools/tests/test_termux_jni.py +++ b/tools/tests/test_termux_jni.py @@ -59,8 +59,6 @@ class TermuxJniTest(unittest.TestCase): return {key: int(value) for key, value in re.findall(r"(\w+)=(\d+)", line)} def test_a_long_argv_and_environment_stay_under_the_local_reference_limit(self): - # 300 + 300 is under 512 for each loop on its own but over it together, as the issue says: - # ART aborts with "local reference table overflow (max=512)". stats = self.run_harness(300, 300) self.assertEqual(stats["overflowed"], 0, stats) diff --git a/tools/tests/test_wincomponents.py b/tools/tests/test_wincomponents.py index fe0902af4..ac50803d1 100644 --- a/tools/tests/test_wincomponents.py +++ b/tools/tests/test_wincomponents.py @@ -98,7 +98,7 @@ class WinComponentsTest(unittest.TestCase): self.pick("vc") self.apply() self.assertEqual(installer.read_bytes(), b"ours") - self.apply() # a second launch keeps the first backup, not ours + self.apply() self.pick() names, notes = self.apply() self.assertEqual(installer.read_bytes(), b"from a real installer") diff --git a/tools/tests/test_zram_preload.py b/tools/tests/test_zram_preload.py index b0967cf62..a9a437e3e 100644 --- a/tools/tests/test_zram_preload.py +++ b/tools/tests/test_zram_preload.py @@ -47,7 +47,6 @@ static uint64_t sum(const unsigned char *p, size_t n) { return s; } -/* Rewrites its pages with a round number and checks every page it wrote last round. */ static void *writer(void *arg) { unsigned char *mine = shared + (size_t)(uintptr_t)arg * (size / 4); for (unsigned char round = 1; !stop; round++) { @@ -78,7 +77,6 @@ int main(int argc, char **argv) { sigaction(SIGURG, NULL, &sa); printf("handler %d\n", (sa.sa_flags & SA_SIGINFO) ? 1 : 0); if (argc > 4) { - /* Read-only memory the process never pages out itself, swapped as the kernel would. */ unsigned char *ro = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); memset(ro, 0x5a, size); mprotect(ro, size, PROT_READ); diff --git a/tools/vr/build.sh b/tools/vr/build.sh index f9e4fa49c..fc90d8905 100755 --- a/tools/vr/build.sh +++ b/tools/vr/build.sh @@ -1,14 +1,4 @@ #!/usr/bin/env bash -# Builds Cardboard mode's SteamVR pieces for the guest (glibc aarch64): -# <out>/droiddeck the SteamVR driver: driver_droiddeck.so, its manifest and default settings -# <out>/layer VK_LAYER_DROIDDECK_vr, the Vulkan layer SteamVR's compositor needs on Turnip/KGSL -# -# tools/vr/build.sh [OUT] in the local cross image (default OUT: tools/vr/out) -# tools/vr/build.sh --native OUT with aarch64-linux-gnu-gcc/g++ on PATH (CI, or inside the image) -# -# Vulkan headers: VK_INCLUDE (a directory holding vulkan/), else the NDK's (ANDROID_NDK_HOME, the -# runner's latest NDK, or the newest under ANDROID_HOME). Only vulkan/ is copied out of it: the rest of -# the NDK sysroot is Android's libc, not the guest's. set -euo pipefail here=$(cd "$(dirname "$0")" && pwd) repo=$(cd "$here/../.." && pwd) @@ -29,8 +19,6 @@ if [ -z "$vkinc" ]; then fi [ -n "$vkinc" ] && [ -f "$vkinc/vulkan/vulkan_core.h" ] || { echo "tools/vr/build.sh: no Vulkan headers (set VK_INCLUDE)" >&2; exit 1; } -# Everything the compilers read sits under the repo, so the cross image (which mounts only the repo) -# sees it too. work="$repo/build/vr" rm -rf "$work" && mkdir -p "$work/include" cp -R "$vkinc/vulkan" "$work/include/" @@ -40,7 +28,6 @@ mkdir -p "$work/out/droiddeck/bin/linuxarm64" "$work/out/layer" compile() { local cmd=(sh -c ' set -e - # libwayland-client: linked against a stub with its soname (the guest has the real one). mkdir -p build/vr/stub aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wl,-soname,libwayland-client.so.0 -Itools/vr/third_party/wayland \ -o build/vr/stub/libwayland-client.so tools/vr/src/wayland_client_stub.c diff --git a/tools/vr/src/driver_droiddeck.cpp b/tools/vr/src/driver_droiddeck.cpp index eaded9ebb..1397cc1cf 100644 --- a/tools/vr/src/driver_droiddeck.cpp +++ b/tools/vr/src/driver_droiddeck.cpp @@ -1,15 +1,3 @@ -// SteamVR driver for DroidDeck's Cardboard mode: one 3DoF HMD whose pose is the phone's rotation -// sensor, read from the head pose file the app shares with the guest -// (app/src/main/cpp/waylandcomp/src/headpose_shm.h, /run/droiddeck/droiddeck-headpose). -// -// The HMD renders an undistorted side-by-side pair: the app's Cardboard pass, in its side-by-side -// mode, does the lens distortion for the viewer the user picked. So the projection here is exactly -// the frustum that pass samples (tan_half_fov, eye_aspect from the pose file), and the distortion is -// the identity. -// -// How the frames reach the screen is the open question this driver is for: see docs/cardboard-vr.md. -// It exposes IVRDisplayComponent with the display "on the desktop" (extended mode) at the session's -// size, and logs what vrserver / vrcompositor ask of it. #include "openvr_driver.h" #include <algorithm> @@ -50,7 +38,6 @@ void Log(const char *fmt, ...) { fprintf(stderr, "[droiddeck] %s\n", buf); } -// The shared pose file, mapped read-only; reads follow the seqlock in headpose_shm.h. class PoseFile { public: bool Read(droiddeck_headpose *out) { @@ -82,7 +69,6 @@ private: }; HmdQuaternion_t QuatFromMatrix(const float m[9]) { - // Row-major rotation -> quaternion (Shepperd's method). HmdQuaternion_t q; float tr = m[0] + m[4] + m[8]; if (tr > 0.0f) { @@ -101,8 +87,6 @@ HmdQuaternion_t QuatFromMatrix(const float m[9]) { return q; } -// IVRVirtualDisplay: the compositor renders the final frame into its own backbuffer and hands it -// here instead of scanning it out. What the handle is on Linux is what this logs (experiment). class VirtualDisplay : public IVRVirtualDisplay { public: void Present(const PresentInfo_t *info, uint32_t size) override { @@ -115,7 +99,6 @@ public: } void WaitForPresent() override {} bool GetTimeSinceLastVsync(float *seconds, uint64_t *counter) override { - // A free-running 60 Hz vsync. using namespace std::chrono; double t = duration<double>(steady_clock::now().time_since_epoch()).count(); double period = 1.0 / 60.0; @@ -129,8 +112,6 @@ private: std::chrono::steady_clock::time_point last_; }; -// The vsync grid SteamVR paces against in driver-direct mode (Prop_DriverDirectModeSendsVsyncEvents): -// a free-running 60 Hz tick, declared a little ahead of time (ALVR's announcer, alvr/server_openvr/src/vsync.rs). class Vsync { public: static constexpr std::chrono::nanoseconds kInterval{16666667}; @@ -172,10 +153,6 @@ private: static inline std::chrono::steady_clock::time_point last_{}; }; -// IVRDriverDirectModeComponent: SteamVR renders each eye into textures this driver allocates -// (shared Vulkan images from SteamVR's IPC resource manager, dmabufs on Linux) and hands them -// over per frame. This is how ALVR gets frames on Linux; it avoids the compositor's own direct mode, -// which needs VK_KHR_display - something Turnip on KGSL refuses outright. class DirectMode : public IVRDriverDirectModeComponent { public: struct Set { @@ -190,8 +167,8 @@ public: Set set; set.pid = pid; set.desc = *d; - const uint32_t usage = 0x1 /* TRANSFER_SRC */ | 0x4 /* SAMPLED */ | 0x80 /* INPUT_ATTACHMENT */; - const uint32_t create = 0x8 /* MUTABLE_FORMAT */; + const uint32_t usage = 0x1 | 0x4 | 0x80 ; + const uint32_t create = 0x8 ; for (int i = 0; i < 3; i++) { SharedTextureHandle_t h = 0; if (!VRIPCResourceManager()->NewSharedVulkanImage(d->nFormat, d->nWidth, d->nHeight, true, false, true, 1, 1, @@ -245,7 +222,6 @@ public: void SubmitLayer(const SubmitLayerPerEye_t (&eyes)[2]) override { layers_++; - // The first layer of a frame is the compositor's own picture; overlays after it are not drawn yet. if (layerInFrame_++ == 0) { for (int e = 0; e < 2; e++) { frame_.texture[e] = eyes[e].hTexture; @@ -274,7 +250,6 @@ public: } void PostPresent(const Throttling_t *t) override { - // Hold until the tick after the last one declared (ALVR's pacing): a slow frame costs one frame. auto last = Vsync::LastTick(); auto until = last + Vsync::kInterval * (1 + (t ? t->nFramesToThrottle : 0)); if (until > std::chrono::steady_clock::now()) std::this_thread::sleep_until(until); @@ -304,12 +279,9 @@ private: droiddeck::EyeFrame frame_{}; }; -// The session's game pad, as the app publishes it for the guest: the absolute-state snapshot at the -// head of fake input ring 0 (app/src/main/cpp/fakeinput_steam.cpp, FakeInputRingHeader), under a -// seqlock. FAKE_EVDEV_MEMFD_PATHS names the rings in every session process's environment. struct PadState { - uint32_t buttons = 0; // bit order of FakeInputWriter.BUTTON_MAP; bit 11 = the QAM button - int16_t axes[8] = {}; // X, Y, RX, RY (+-32767), GAS = right trigger, BRAKE = left (0..255), HAT0X, HAT0Y + uint32_t buttons = 0; + int16_t axes[8] = {}; }; enum : uint32_t { PAD_A = 1u << 0, PAD_B = 1u << 1, PAD_X = 1u << 2, PAD_Y = 1u << 3, PAD_LB = 1u << 4, PAD_RB = 1u << 5, @@ -340,7 +312,7 @@ private: std::string path; if (const char *env = getenv("DROIDDECK_VR_PAD_RING")) path = env; if (path.empty()) { - const char *paths = getenv("FAKE_EVDEV_MEMFD_PATHS"); // "0=/path;1=/path;..." + const char *paths = getenv("FAKE_EVDEV_MEMFD_PATHS"); if (paths && !strncmp(paths, "0=", 2)) path.assign(paths + 2, strcspn(paths + 2, ";")); } if (path.empty()) { @@ -363,10 +335,6 @@ private: std::chrono::steady_clock::time_point retry_{}; }; -// One hand: a Vive-profile controller driven by the game pad. 3DoF, so it is a gaze pointer: it sits -// low in front of its side of the body and points where the head looks, and SteamVR's laser follows the -// view. Right hand: right stick = trackpad, A = trackpad click, RT = trigger, RB = grip, Start = menu, -// the Steam/guide button = system. Left: left stick, X, LT, LB, Select. class CardboardController : public ITrackedDeviceServerDriver { public: explicit CardboardController(bool right) : right_(right) {} @@ -404,14 +372,12 @@ public: DriverPose_t GetPose() override { return pose_; } - // From the driver's pose thread: the head's orientation and the pad. void Update(const HmdQuaternion_t &head, const PadState *pad) { if (id_ == k_unTrackedDeviceIndexInvalid) return; DriverPose_t p = {}; p.qWorldFromDriverRotation.w = 1; p.qDriverFromHeadRotation.w = 1; p.qRotation = head; - // Low and in front of its side, in head space, then into the room with the head's rotation. const double off[3] = {right_ ? 0.18 : -0.18, -0.35, -0.30}; double m[3][3]; const double w = head.w, x = head.x, y = head.y, z = head.z; @@ -460,7 +426,6 @@ public: if (height_ <= 0) height_ = 1080; float hz = VRSettings()->GetFloat(kSettingsSection, "displayFrequency"); if (hz <= 0.0f) hz = 60.0f; - // Until the app has drawn an eye: the Cardboard pass's defaults for a 1920x1080 panel. tanHalf_ = 1.0f; eyeAspect_ = (width_ * 0.5f) / height_; droiddeck_headpose hp; @@ -476,8 +441,6 @@ public: VRProperties()->SetFloatProperty(c, Prop_DisplayFrequency_Float, hz); VRProperties()->SetFloatProperty(c, Prop_SecondsFromVsyncToPhotons_Float, 0.011f); VRProperties()->SetUint64Property(c, Prop_CurrentUniverseId_Uint64, 2); - // Off the desktop: with a direct mode component that sends SteamVR's compositor down the - // driver-direct path; on it, the compositor opens its own direct mode window (VK_KHR_display). VRProperties()->SetBoolProperty(c, Prop_IsOnDesktop_Bool, false); VRProperties()->SetBoolProperty(c, Prop_DisplayDebugMode_Bool, false); directMode_ = VRSettings()->GetBool(kSettingsSection, "directMode"); @@ -489,9 +452,6 @@ public: VRProperties()->SetBoolProperty(c, Prop_HasDisplayComponent_Bool, true); virtualDisplay_ = VRSettings()->GetBool(kSettingsSection, "virtualDisplay"); VRProperties()->SetBoolProperty(c, Prop_HasVirtualDisplayComponent_Bool, virtualDisplay_); - // A proximity sensor that reads "worn" while Cardboard mode is on: otherwise SteamVR sees a headset - // lying still (a phone on a table between sessions) and puts the display in standby, after which - // the compositor submits no layers at all. VRProperties()->SetBoolProperty(c, Prop_ContainsProximitySensor_Bool, true); VRDriverInput()->CreateBooleanComponent(c, "/proximity", &proximity_); VRDriverInput()->UpdateBooleanComponent(proximity_, true, 0); @@ -528,7 +488,7 @@ public: p.qWorldFromDriverRotation.w = 1; p.qDriverFromHeadRotation.w = 1; p.qRotation.w = 1; - p.vecPosition[1] = 1.6; // standing eye height: 3DoF, the head never moves + p.vecPosition[1] = 1.6; droiddeck_headpose hp; bool ok = pose_.Read(&hp); if (ok) { @@ -543,14 +503,12 @@ public: return p; } - // ---- IVRDisplayComponent void GetWindowBounds(int32_t *x, int32_t *y, uint32_t *w, uint32_t *h) override { *x = 0; *y = 0; *w = (uint32_t)width_; *h = (uint32_t)height_; } bool IsDisplayOnDesktop() override { return !directMode_; } bool IsDisplayRealDisplay() override { return false; } void GetRecommendedRenderTargetSize(uint32_t *w, uint32_t *h) override { - // Rendered a little larger than the panel: the lens pass magnifies the middle. *w = (uint32_t)(width_ / 2 * 1.4f); *h = (uint32_t)(height_ * 1.4f); } @@ -559,7 +517,6 @@ public: *x = eye == Eye_Left ? 0 : (uint32_t)width_ / 2; } void GetProjectionRaw(EVREye, float *left, float *right, float *top, float *bottom) override { - // Exactly what the Cardboard pass samples from each half of a side-by-side frame. *left = -tanHalf_ * eyeAspect_; *right = tanHalf_ * eyeAspect_; *top = -tanHalf_; *bottom = tanHalf_; } @@ -582,7 +539,6 @@ private: droiddeck_headpose hp; bool fresh = pose_.Read(&hp) && hp.seq != lastSeq; auto now = std::chrono::steady_clock::now(); - // A new sample, or a heartbeat so SteamVR never thinks the HMD went away. PadState pad; const bool havePad = pad_.Read(&pad); const bool padChanged = havePad && (pad.buttons != lastPad_.buttons || memcmp(pad.axes, lastPad_.axes, sizeof(pad.axes))); @@ -657,7 +613,7 @@ private: Provider g_provider; -} // namespace +} DD_EXPORT void *HmdDriverFactory(const char *interfaceName, int *returnCode) { if (!strcmp(interfaceName, IServerTrackedDeviceProvider_Version)) return &g_provider; diff --git a/tools/vr/src/openvr_driver.h b/tools/vr/src/openvr_driver.h index fcd675583..a781cacb6 100644 --- a/tools/vr/src/openvr_driver.h +++ b/tools/vr/src/openvr_driver.h @@ -1,8 +1,6 @@ #pragma once -// openvr_driver.h //========= Copyright Valve Corporation ============// -// Dynamically generated file. Do not modify this file directly. #ifndef _OPENVR_DRIVER_API #define _OPENVR_DRIVER_API @@ -11,16 +9,14 @@ -// version.h namespace vr { static const uint32_t k_nSteamVRVersionMajor = 2; static const uint32_t k_nSteamVRVersionMinor = 15; static const uint32_t k_nSteamVRVersionBuild = 6; -} // namespace vr +} -// public_vrtypes.h #ifndef _INCLUDE_CORE_VRTYPES_PUBLIC_H #define _INCLUDE_CORE_VRTYPES_PUBLIC_H @@ -31,11 +27,6 @@ namespace vr typedef uint32_t PropertyTypeTag_t; -// right-handed system -// +y is up -// +x is to the right -// -z is forward -// Distance unit is meters struct HmdMatrix34_t { float m[3][4]; @@ -97,7 +88,6 @@ struct HmdRect2_t HmdVector2_t vBottomRight; }; -/** Holds the transform for a single bone */ struct VRBoneTransform_t { HmdVector4_t position; @@ -110,13 +100,10 @@ struct VREyeTrackingData_t bool bValid; bool bTracked; - vr::HmdVector3_t vGazeOrigin; // Ray origin - vr::HmdVector3_t vGazeTarget; // Gaze target (fixation point) + vr::HmdVector3_t vGazeOrigin; + vr::HmdVector3_t vGazeTarget; }; -/** Used to return the post-distortion UVs for each color channel. -* UVs range from 0 to 1 with 0,0 in the upper left corner of the -* source render target. The 0,0 to 1,1 range covers a single eye. */ struct DistortionCoordinates_t { float rfRed[2]; @@ -132,55 +119,52 @@ enum EVREye enum ETextureType { - TextureType_Invalid = -1, // Handle has been invalidated - TextureType_DirectX = 0, // Handle is an ID3D11Texture - TextureType_OpenGL = 1, // Handle is an OpenGL texture name or an OpenGL render buffer name, depending on submit flags - TextureType_Vulkan = 2, // Handle is a pointer to a VRVulkanTextureData_t structure - TextureType_IOSurface = 3, // Handle is a macOS cross-process-sharable IOSurfaceRef, deprecated in favor of TextureType_Metal on supported platforms - TextureType_DirectX12 = 4, // Handle is a pointer to a D3D12TextureData_t structure - TextureType_DXGISharedHandle = 5, // Handle is a HANDLE DXGI share handle, only supported for Overlay render targets. - // this texture is used directly by our renderer, so only perform atomic (copyresource or resolve) on it - TextureType_Metal = 6, // Handle is a MTLTexture conforming to the MTLSharedTexture protocol. Textures submitted to IVRCompositor::Submit which - // are of type MTLTextureType2DArray assume layer 0 is the left eye texture (vr::EVREye::Eye_left), layer 1 is the right - // eye texture (vr::EVREye::Eye_Right) + TextureType_Invalid = -1, + TextureType_DirectX = 0, + TextureType_OpenGL = 1, + TextureType_Vulkan = 2, + TextureType_IOSurface = 3, + TextureType_DirectX12 = 4, + TextureType_DXGISharedHandle = 5, + + TextureType_Metal = 6, + + TextureType_Reserved = 7, - TextureType_SharedTextureHandle = 8, // A pointer to a vr::SharedTextureHandle_t that was imported via, eg. ImportDmabuf. + TextureType_SharedTextureHandle = 8, }; enum EColorSpace { - ColorSpace_Auto = 0, // Assumes 'gamma' for 8-bit per component formats, otherwise 'linear'. This mirrors the DXGI formats which have _SRGB variants. - ColorSpace_Gamma = 1, // Texture data can be displayed directly on the display without any conversion (a.k.a. display native format). - ColorSpace_Linear = 2, // Same as gamma but has been converted to a linear representation using DXGI's sRGB conversion algorithm. + ColorSpace_Auto = 0, + ColorSpace_Gamma = 1, + ColorSpace_Linear = 2, }; struct Texture_t { - void* handle; // See ETextureType definition above + void* handle; ETextureType eType; EColorSpace eColorSpace; }; -/** Allows the application to control what part of the provided texture will be used in the -* frame buffer. */ struct VRTextureBounds_t { float uMin, vMin; float uMax, vMax; }; -/** Allows specifying pose used to render provided scene texture (if different from value returned by WaitGetPoses). */ struct VRTextureWithPose_t : public Texture_t { - HmdMatrix34_t mDeviceToAbsoluteTracking; // Actual pose used to render scene textures. + HmdMatrix34_t mDeviceToAbsoluteTracking; }; struct VRTextureDepthInfo_t { - void* handle; // See ETextureType definition above + void* handle; HmdMatrix44_t mProjection; - HmdVector2_t vRange; // 0..1 + HmdVector2_t vRange; }; struct VRTextureWithDepth_t : public Texture_t @@ -195,8 +179,8 @@ struct VRTextureWithPoseAndDepth_t : public VRTextureWithPose_t struct VRTextureMotionInfo_t { - void *handle; // See ETextureType definition above - HmdMatrix44_t mDeltaPose; // Incremental application-applied transform, if any, since the previous frame that affects the view. + void *handle; + HmdMatrix44_t mDeltaPose; }; struct VRTextureWithMotion_t : VRTextureWithPoseAndDepth_t @@ -204,8 +188,6 @@ struct VRTextureWithMotion_t : VRTextureWithPoseAndDepth_t VRTextureMotionInfo_t motion; }; -// 64-bit types that are part of public structures -// that are replicated in shared memory. #if defined(__linux__) || defined(__APPLE__) typedef uint64_t vrshared_uint64_t __attribute__ ((aligned(8))); typedef double vrshared_double __attribute__ ((aligned(8))); @@ -220,12 +202,12 @@ struct DmabufPlane_t { uint32_t unOffset; uint32_t unStride; - int32_t nFd; // This is not consumed, it is dup'ed. + int32_t nFd; }; struct DmabufAttributes_t { - void *pNext; // MUST be NULL. Unused right now, but could be used to extend this structure in the future. + void *pNext; uint32_t unWidth; uint32_t unHeight; @@ -233,8 +215,8 @@ struct DmabufAttributes_t uint32_t unMipLevels; uint32_t unArrayLayers; uint32_t unSampleCount; - uint32_t unFormat; // DRM_FORMAT_ - uint64_t ulModifier; // DRM_FORMAT_MOD_ + uint32_t unFormat; + uint64_t ulModifier; uint32_t unPlaneCount; DmabufPlane_t plane[MaxDmabufPlaneCount]; @@ -242,22 +224,19 @@ struct DmabufAttributes_t #pragma pack( pop ) -} // namespace vr +} #endif -// vrtypes.h #ifndef _INCLUDE_VRTYPES_H #define _INCLUDE_VRTYPES_H -// Forward declarations to avoid requiring vulkan.h struct VkDevice_T; struct VkPhysicalDevice_T; struct VkInstance_T; struct VkQueue_T; -// Forward declarations to avoid requiring d3d12.h struct ID3D12Resource; struct ID3D12CommandQueue; @@ -265,8 +244,6 @@ namespace vr { #pragma pack( push, 8 ) -/** A handle for a spatial anchor. This handle is only valid during the session it was created in. -* Anchors that live beyond one session should be saved by their string descriptors. */ typedef uint32_t SpatialAnchorHandle_t; typedef void* glSharedTextureHandle_t; @@ -274,7 +251,6 @@ typedef int32_t glInt_t; typedef uint32_t glUInt_t; -// Handle to a shared texture (HANDLE on Windows obtained using OpenSharedResource). typedef uint64_t SharedTextureHandle_t; #define INVALID_SHARED_TEXTURE_HANDLE ((vr::SharedTextureHandle_t)0) @@ -296,41 +272,37 @@ static const uint32_t k_nDriverNone = 0xFFFFFFFF; static const uint32_t k_unMaxDriverDebugResponseSize = 32768; -/** Used to pass device IDs to API calls */ typedef uint32_t TrackedDeviceIndex_t; static const uint32_t k_unTrackedDeviceIndex_Hmd = 0; static const uint32_t k_unMaxTrackedDeviceCount = 64; static const uint32_t k_unTrackedDeviceIndexOther = 0xFFFFFFFE; static const uint32_t k_unTrackedDeviceIndexInvalid = 0xFFFFFFFF; -/** Describes what kind of object is being tracked at a given ID */ enum ETrackedDeviceClass { - TrackedDeviceClass_Invalid = 0, // the ID was not valid. - TrackedDeviceClass_HMD = 1, // Head-Mounted Displays - TrackedDeviceClass_Controller = 2, // Tracked controllers - TrackedDeviceClass_GenericTracker = 3, // Generic trackers, similar to controllers - TrackedDeviceClass_TrackingReference = 4, // Camera and base stations that serve as tracking reference points - TrackedDeviceClass_DisplayRedirect = 5, // Accessories that aren't necessarily tracked themselves, but may redirect video output from other tracked devices + TrackedDeviceClass_Invalid = 0, + TrackedDeviceClass_HMD = 1, + TrackedDeviceClass_Controller = 2, + TrackedDeviceClass_GenericTracker = 3, + TrackedDeviceClass_TrackingReference = 4, + TrackedDeviceClass_DisplayRedirect = 5, TrackedDeviceClass_Max }; -/** Describes what specific role associated with a tracked device */ enum ETrackedControllerRole { - TrackedControllerRole_Invalid = 0, // Invalid value for controller type - TrackedControllerRole_LeftHand = 1, // Tracked device associated with the left hand - TrackedControllerRole_RightHand = 2, // Tracked device associated with the right hand - TrackedControllerRole_OptOut = 3, // Tracked device is opting out of left/right hand selection - TrackedControllerRole_Treadmill = 4, // Tracked device is a treadmill or other locomotion device - TrackedControllerRole_Stylus = 5, // Tracked device is a stylus + TrackedControllerRole_Invalid = 0, + TrackedControllerRole_LeftHand = 1, + TrackedControllerRole_RightHand = 2, + TrackedControllerRole_OptOut = 3, + TrackedControllerRole_Treadmill = 4, + TrackedControllerRole_Stylus = 5, TrackedControllerRole_Max = 5 }; -/** Returns true if the tracked controller role is allowed to be a hand */ inline bool IsRoleAllowedAsHand( ETrackedControllerRole eRole ) { switch ( eRole ) @@ -345,27 +317,22 @@ inline bool IsRoleAllowedAsHand( ETrackedControllerRole eRole ) } -/** describes a single pose for a tracked object */ struct TrackedDevicePose_t { HmdMatrix34_t mDeviceToAbsoluteTracking; - HmdVector3_t vVelocity; // velocity in tracker space in m/s - HmdVector3_t vAngularVelocity; // angular velocity in radians/s (?) + HmdVector3_t vVelocity; + HmdVector3_t vAngularVelocity; ETrackingResult eTrackingResult; bool bPoseIsValid; - // This indicates that there is a device connected for this spot in the pose array. - // It could go from true to false if the user unplugs the device. bool bDeviceIsConnected; }; -/** Identifies which style of tracking origin the application wants to use -* for the poses it is requesting */ enum ETrackingUniverseOrigin { - TrackingUniverseSeated = 0, // Poses are provided relative to the seated zero pose - TrackingUniverseStanding = 1, // Poses are provided relative to the safe bounds configured by the user - TrackingUniverseRawAndUncalibrated = 2, // Poses are provided in the coordinate system defined by the driver. It has Y up and is unified for devices of the same driver. You usually don't want this one. + TrackingUniverseSeated = 0, + TrackingUniverseStanding = 1, + TrackingUniverseRawAndUncalibrated = 2, }; enum EAdditionalRadioFeatures @@ -379,7 +346,6 @@ enum EAdditionalRadioFeatures typedef uint64_t WebConsoleHandle_t; #define INVALID_WEB_CONSOLE_HANDLE ((vr::WebConsoleHandle_t)0) -// Refers to a single container of properties typedef uint64_t PropertyContainerHandle_t; typedef uint32_t PropertyTypeTag_t; @@ -389,7 +355,6 @@ static const PropertyTypeTag_t k_unInvalidPropertyTag = 0; typedef PropertyContainerHandle_t DriverHandle_t; static const PropertyContainerHandle_t k_ulInvalidDriverHandle = 0; -// Use these tags to set/get common types as struct properties static const PropertyTypeTag_t k_unFloatPropertyTag = 1; static const PropertyTypeTag_t k_unInt32PropertyTag = 2; static const PropertyTypeTag_t k_unUint64PropertyTag = 3; @@ -421,13 +386,10 @@ static const PropertyTypeTag_t k_unOpenVRInternalReserved_Start = 1000; static const PropertyTypeTag_t k_unOpenVRInternalReserved_End = 10000; -/** Each entry in this enum represents a property that can be retrieved about a -* tracked device. Many fields are only valid for one ETrackedDeviceClass. */ enum ETrackedDeviceProperty { Prop_Invalid = 0, - // general properties that apply to all device classes Prop_TrackingSystemName_String = 1000, Prop_ModelNumber_String = 1001, Prop_SerialNumber_String = 1002, @@ -440,7 +402,7 @@ enum ETrackedDeviceProperty Prop_ConnectedWirelessDongle_String = 1009, Prop_DeviceIsWireless_Bool = 1010, Prop_DeviceIsCharging_Bool = 1011, - Prop_DeviceBatteryPercentage_Float = 1012, // 0 is empty, 1 is full + Prop_DeviceBatteryPercentage_Float = 1012, Prop_StatusDisplayTransform_Matrix34 = 1013, Prop_Firmware_UpdateAvailable_Bool = 1014, Prop_Firmware_ManualUpdate_Bool = 1015, @@ -465,16 +427,16 @@ enum ETrackedDeviceProperty Prop_ParentDriver_Uint64 = 1034, Prop_ResourceRoot_String = 1035, Prop_RegisteredDeviceType_String = 1036, - Prop_InputProfilePath_String = 1037, // input profile to use for this device in the input system. Will default to tracking system name if this isn't provided - Prop_NeverTracked_Bool = 1038, // Used for devices that will never have a valid pose by design + Prop_InputProfilePath_String = 1037, + Prop_NeverTracked_Bool = 1038, Prop_NumCameras_Int32 = 1039, - Prop_CameraFrameLayout_Int32 = 1040, // EVRTrackedCameraFrameLayout value - Prop_CameraStreamFormat_Int32 = 1041, // ECameraVideoStreamFormat value - Prop_AdditionalDeviceSettingsPath_String = 1042, // driver-relative path to additional device and global configuration settings - Prop_Identifiable_Bool = 1043, // Whether device supports being identified from vrmonitor (e.g. blink LED, vibrate haptics, etc) + Prop_CameraFrameLayout_Int32 = 1040, + Prop_CameraStreamFormat_Int32 = 1041, + Prop_AdditionalDeviceSettingsPath_String = 1042, + Prop_Identifiable_Bool = 1043, Prop_BootloaderVersion_Uint64 = 1044, - Prop_AdditionalSystemReportData_String = 1045, // additional string to include in system reports about a tracked device - Prop_CompositeFirmwareVersion_String = 1046, // additional FW components from a device that gets propagated into reports + Prop_AdditionalSystemReportData_String = 1045, + Prop_CompositeFirmwareVersion_String = 1046, Prop_Firmware_RemindUpdate_Bool = 1047, Prop_PeripheralApplicationVersion_Uint64 = 1048, Prop_ManufacturerSerialNumber_String = 1049, @@ -482,15 +444,14 @@ enum ETrackedDeviceProperty Prop_EstimatedDeviceFirstUseTime_Int32 = 1051, Prop_DevicePowerUsage_Float = 1052, Prop_IgnoreMotionForStandby_Bool = 1053, - Prop_ActualTrackingSystemName_String = 1054, // the literal local driver name in case someone is playing games with prop 1000 - Prop_AllowCameraToggle_Bool = 1055, // Shows the Enable/Disable camera option. Hide this for certain headsets if they have the camera tracking (since it's always on) - Prop_AllowLightSourceFrequency_Bool = 1056, // Shows the Anti-Flicker option in camera settings. - Prop_SteamRemoteClientID_Uint64 = 1057, // For vrlink + Prop_ActualTrackingSystemName_String = 1054, + Prop_AllowCameraToggle_Bool = 1055, + Prop_AllowLightSourceFrequency_Bool = 1056, + Prop_SteamRemoteClientID_Uint64 = 1057, Prop_Reserved_1058 = 1058, Prop_Reserved_1059 = 1059, Prop_Reserved_1060 = 1060, - // Properties that are unique to TrackedDeviceClass_HMD Prop_ReportsTimeSinceVSync_Bool = 2000, Prop_SecondsFromVsyncToPhotons_Float = 2001, Prop_DisplayFrequency_Float = 2002, @@ -538,14 +499,14 @@ enum ETrackedDeviceProperty Prop_DisplayDebugMode_Bool = 2044, Prop_GraphicsAdapterLuid_Uint64 = 2045, Prop_DriverProvidedChaperonePath_String = 2048, - Prop_ExpectedTrackingReferenceCount_Int32 = 2049, // expected number of sensors or basestations to reserve UI space for - Prop_ExpectedControllerCount_Int32 = 2050, // expected number of tracked controllers to reserve UI space for - Prop_NamedIconPathControllerLeftDeviceOff_String = 2051, // placeholder icon for "left" controller if not yet detected/loaded - Prop_NamedIconPathControllerRightDeviceOff_String = 2052, // placeholder icon for "right" controller if not yet detected/loaded - Prop_NamedIconPathTrackingReferenceDeviceOff_String = 2053, // placeholder icon for sensor/base if not yet detected/loaded - Prop_DoNotApplyPrediction_Bool = 2054, // currently no effect. was used to disable HMD pose prediction on MR, which is now done by MR driver setting velocity=0 + Prop_ExpectedTrackingReferenceCount_Int32 = 2049, + Prop_ExpectedControllerCount_Int32 = 2050, + Prop_NamedIconPathControllerLeftDeviceOff_String = 2051, + Prop_NamedIconPathControllerRightDeviceOff_String = 2052, + Prop_NamedIconPathTrackingReferenceDeviceOff_String = 2053, + Prop_DoNotApplyPrediction_Bool = 2054, Prop_CameraToHeadTransforms_Matrix34_Array = 2055, - Prop_DistortionMeshResolution_Int32 = 2056, // custom resolution of compositor calls to IVRSystem::ComputeDistortion + Prop_DistortionMeshResolution_Int32 = 2056, Prop_DriverIsDrawingControllers_Bool = 2057, Prop_DriverRequestsApplicationPause_Bool = 2058, Prop_DriverRequestsReducedRendering_Bool = 2059, @@ -557,20 +518,19 @@ enum ETrackedDeviceProperty Prop_ImuFactoryGyroScale_Vector3 = 2065, Prop_ImuFactoryAccelerometerBias_Vector3 = 2066, Prop_ImuFactoryAccelerometerScale_Vector3 = 2067, - // reserved 2068 Prop_ConfigurationIncludesLighthouse20Features_Bool = 2069, Prop_AdditionalRadioFeatures_Uint64 = 2070, - Prop_CameraWhiteBalance_Vector4_Array = 2071, // Prop_NumCameras_Int32-sized array of float[4] RGBG white balance calibration data (max size is vr::k_unMaxCameras) - Prop_CameraDistortionFunction_Int32_Array = 2072, // Prop_NumCameras_Int32-sized array of vr::EVRDistortionFunctionType values (max size is vr::k_unMaxCameras) - Prop_CameraDistortionCoefficients_Float_Array = 2073, // Prop_NumCameras_Int32-sized array of double[vr::k_unMaxDistortionFunctionParameters] (max size is vr::k_unMaxCameras) + Prop_CameraWhiteBalance_Vector4_Array = 2071, + Prop_CameraDistortionFunction_Int32_Array = 2072, + Prop_CameraDistortionCoefficients_Float_Array = 2073, Prop_ExpectedControllerType_String = 2074, - Prop_HmdTrackingStyle_Int32 = 2075, // one of EHmdTrackingStyle + Prop_HmdTrackingStyle_Int32 = 2075, Prop_DriverProvidedChaperoneVisibility_Bool = 2076, Prop_HmdColumnCorrectionSettingPrefix_String = 2077, Prop_CameraSupportsCompatibilityModes_Bool = 2078, Prop_SupportsRoomViewDepthProjection_Bool = 2079, - Prop_DisplayAvailableFrameRates_Float_Array = 2080, // populated by compositor from actual EDID list when available from GPU driver - Prop_DisplaySupportsMultipleFramerates_Bool = 2081, // if this is true but Prop_DisplayAvailableFrameRates_Float_Array is empty, explain to user + Prop_DisplayAvailableFrameRates_Float_Array = 2080, + Prop_DisplaySupportsMultipleFramerates_Bool = 2081, Prop_DisplayColorMultLeft_Vector3 = 2082, Prop_DisplayColorMultRight_Vector3 = 2083, Prop_DisplaySupportsRuntimeFramerateChange_Bool = 2084, @@ -579,12 +539,10 @@ enum ETrackedDeviceProperty Prop_DisplayMaxAnalogGain_Float = 2087, Prop_CameraExposureTime_Float = 2088, Prop_CameraGlobalGain_Float = 2089, - // Prop_DashboardLayoutPathName_String = 2090, // DELETED Prop_DashboardScale_Float = 2091, - // Prop_PeerButtonInfo_String = 2092, // DELETED Prop_Hmd_SupportsHDR10_Bool = 2093, Prop_Hmd_EnableParallelRenderCameras_Bool = 2094, - Prop_DriverProvidedChaperoneJson_String = 2095, // higher priority than Prop_DriverProvidedChaperonePath_String + Prop_DriverProvidedChaperoneJson_String = 2095, Prop_ForceSystemLayerUseAppPoses_Bool = 2096, Prop_DashboardLinkSupport_Int32 = 2097, Prop_DisplayMinUIAnalogGain_Float = 2098, @@ -598,7 +556,6 @@ enum ETrackedDeviceProperty Prop_Hmd_SupportsAppThrottling_Bool = 2106, Prop_Hmd_SupportsGpuBusMonitoring_Bool = 2107, Prop_DriverDisplaysIPDChanges_Bool = 2108, - // Prop_Driver_RecenterSupport_Int32 = 2109, // DELETED Prop_Reserved_2110 = 2110, Prop_Reserved_2111 = 2111, Prop_Reserved_2112 = 2112, @@ -610,7 +567,6 @@ enum ETrackedDeviceProperty Prop_Hmd_AllowsClientToControlTextureIndex = 2116, Prop_Reserved_2117 = 2117, - // Driver requested mura correction properties Prop_DriverRequestedMuraCorrectionMode_Int32 = 2200, Prop_DriverRequestedMuraFeather_InnerLeft_Int32 = 2201, Prop_DriverRequestedMuraFeather_InnerRight_Int32 = 2202, @@ -632,21 +588,18 @@ enum ETrackedDeviceProperty Prop_Audio_DriverRecordingVolume_Float = 2308, Prop_Audio_DriverRecordingMute_Bool = 2309, - // Pipewire Audio Stuff Prop_Audio_PipewirePlaybackNode_Int32 = 2400, Prop_Audio_PipewireRecordingNode_Int32 = 2401, - // Properties that are unique to TrackedDeviceClass_Controller Prop_AttachedDeviceId_String = 3000, Prop_SupportedButtons_Uint64 = 3001, - Prop_Axis0Type_Int32 = 3002, // Return value is of type EVRControllerAxisType - Prop_Axis1Type_Int32 = 3003, // Return value is of type EVRControllerAxisType - Prop_Axis2Type_Int32 = 3004, // Return value is of type EVRControllerAxisType - Prop_Axis3Type_Int32 = 3005, // Return value is of type EVRControllerAxisType - Prop_Axis4Type_Int32 = 3006, // Return value is of type EVRControllerAxisType - Prop_ControllerRoleHint_Int32 = 3007, // Return value is of type ETrackedControllerRole + Prop_Axis0Type_Int32 = 3002, + Prop_Axis1Type_Int32 = 3003, + Prop_Axis2Type_Int32 = 3004, + Prop_Axis3Type_Int32 = 3005, + Prop_Axis4Type_Int32 = 3006, + Prop_ControllerRoleHint_Int32 = 3007, - // Properties that are unique to TrackedDeviceClass_TrackingReference Prop_FieldOfViewLeftDegrees_Float = 4000, Prop_FieldOfViewRightDegrees_Float = 4001, Prop_FieldOfViewTopDegrees_Float = 4002, @@ -654,28 +607,27 @@ enum ETrackedDeviceProperty Prop_TrackingRangeMinimumMeters_Float = 4004, Prop_TrackingRangeMaximumMeters_Float = 4005, Prop_ModeLabel_String = 4006, - Prop_CanWirelessIdentify_Bool = 4007, // volatile, based on radio presence and fw discovery + Prop_CanWirelessIdentify_Bool = 4007, Prop_Nonce_Int32 = 4008, - // Properties that are used for user interface like icons names - Prop_IconPathName_String = 5000, // DEPRECATED. Value not referenced. Now expected to be part of icon path properties. - Prop_NamedIconPathDeviceOff_String = 5001, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceSearching_String = 5002, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceSearchingAlert_String = 5003, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceReady_String = 5004, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceReadyAlert_String = 5005, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceNotReady_String = 5006, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceStandby_String = 5007, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceAlertLow_String = 5008, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - Prop_NamedIconPathDeviceStandbyAlert_String = 5009, // {driver}/icons/icon_filename - PNG for static icon, or GIF for animation, 50x32 for headsets and 32x32 for others - // Properties that are used by helpers, but are opaque to applications + Prop_IconPathName_String = 5000, + Prop_NamedIconPathDeviceOff_String = 5001, + Prop_NamedIconPathDeviceSearching_String = 5002, + Prop_NamedIconPathDeviceSearchingAlert_String = 5003, + Prop_NamedIconPathDeviceReady_String = 5004, + Prop_NamedIconPathDeviceReadyAlert_String = 5005, + Prop_NamedIconPathDeviceNotReady_String = 5006, + Prop_NamedIconPathDeviceStandby_String = 5007, + Prop_NamedIconPathDeviceAlertLow_String = 5008, + Prop_NamedIconPathDeviceStandbyAlert_String = 5009, + + Prop_DisplayHiddenArea_Binary_Start = 5100, Prop_DisplayHiddenArea_Binary_End = 5150, Prop_ParentContainer = 5151, Prop_OverrideContainer_Uint64 = 5152, - // Properties that are unique to drivers Prop_UserConfigPath_String = 6000, Prop_InstallPath_String = 6001, Prop_HasDisplayComponent_Bool = 6002, @@ -689,16 +641,14 @@ enum ETrackedDeviceProperty Prop_DeviceHasNoIMU_Bool = 6010, Prop_UseAdvancedPrediction_Bool = 6011, - // Properties that are set internally based on other information provided by drivers Prop_ControllerType_String = 7000, - //Prop_LegacyInputProfile_String = 7001, // This is no longer used. See "legacy_binding" in the input profile instead. - Prop_ControllerHandSelectionPriority_Int32 = 7002, // Allows hand assignments to prefer some controllers over others. High numbers are selected over low numbers - // Vendors are free to expose private debug data in this reserved region + Prop_ControllerHandSelectionPriority_Int32 = 7002, + + Prop_VendorSpecific_Reserved_Start = 10000, Prop_VendorSpecific_Reserved_End = 10999, - // Addl SteamVR Reserved Space Prop_Reserved_11000 = 11000, Prop_Reserved_11001 = 11001, Prop_Reserved_11002 = 11002, @@ -708,22 +658,20 @@ enum ETrackedDeviceProperty Prop_TrackedDeviceProperty_Max = 1000000, }; -/** No string property will ever be longer than this length */ static const uint32_t k_unMaxPropertyStringSize = 32 * 1024; -/** Used to return errors that occur when reading properties. */ enum ETrackedPropertyError { TrackedProp_Success = 0, TrackedProp_WrongDataType = 1, TrackedProp_WrongDeviceClass = 2, TrackedProp_BufferTooSmall = 3, - TrackedProp_UnknownProperty = 4, // Driver has not set the property (and may not ever). + TrackedProp_UnknownProperty = 4, TrackedProp_InvalidDevice = 5, TrackedProp_CouldNotContactServer = 6, TrackedProp_ValueNotProvidedByDevice = 7, TrackedProp_StringExceedsMaximumLength = 8, - TrackedProp_NotYetAvailable = 9, // The property value isn't known yet, but is expected soon. Call again later. + TrackedProp_NotYetAvailable = 9, TrackedProp_PermissionDenied = 10, TrackedProp_InvalidOperation = 11, TrackedProp_CannotWriteToWildcards = 12, @@ -732,14 +680,13 @@ enum ETrackedPropertyError TrackedProp_InvalidContainer = 15, }; -/** Used to drive certain text in the UI when talking about the tracking system for the HMD */ enum EHmdTrackingStyle { HmdTrackingStyle_Unknown = 0, - HmdTrackingStyle_Lighthouse = 1, // base stations and lasers - HmdTrackingStyle_OutsideInCameras = 2, // Cameras and LED, Rift 1 style - HmdTrackingStyle_InsideOutCameras = 3, // Cameras on HMD looking at the world + HmdTrackingStyle_Lighthouse = 1, + HmdTrackingStyle_OutsideInCameras = 2, + HmdTrackingStyle_InsideOutCameras = 3, }; typedef uint64_t VRActionHandle_t; @@ -753,48 +700,30 @@ static const VRInputValueHandle_t k_ulInvalidInputValueHandle = 0; static const VRInputComponentHandle_t k_ulInvalidInputComponentHandle = 0; -/** Allows the application to control how scene textures are used by the compositor when calling Submit. */ enum EVRSubmitFlags { - // Simple render path. App submits rendered left and right eye images with no lens distortion correction applied. Submit_Default = 0x00, - // App submits final left and right eye images with lens distortion already applied (lens distortion makes the images appear - // barrel distorted with chromatic aberration correction applied). The app would have used the data returned by - // vr::IVRSystem::ComputeDistortion() to apply the correct distortion to the rendered images before calling Submit(). Submit_LensDistortionAlreadyApplied = 0x01, - // If the texture pointer passed in is actually a renderbuffer (e.g. for MSAA in OpenGL) then set this flag. Submit_GlRenderBuffer = 0x02, - // Do not use Submit_Reserved = 0x04, - // Set to indicate that pTexture is a pointer to a VRTextureWithPose_t. - // This flag can be combined with Submit_TextureWithDepth to pass a VRTextureWithPoseAndDepth_t. Submit_TextureWithPose = 0x08, - // Set to indicate that pTexture is a pointer to a VRTextureWithDepth_t. - // This flag can be combined with Submit_TextureWithPose to pass a VRTextureWithPoseAndDepth_t. Submit_TextureWithDepth = 0x10, - // Set to indicate a discontinuity between this and the last frame. - // This will prevent motion smoothing from attempting to extrapolate using the pair. Submit_FrameDiscontinuity = 0x20, - // Set to indicate that pTexture->handle is a contains VRVulkanTextureArrayData_t Submit_VulkanTextureWithArrayData = 0x40, - // If the texture pointer passed in is an OpenGL Array texture, set this flag Submit_GlArrayTexture = 0x80, - // If the texture is an EGL texture and not an glX/wGL texture (Linux only, currently) Submit_IsEgl = 0x100, - // Set to indicate that pTexture is a pointer to a VRTextureWithMotion_t. Submit_TextureWithMotion = 0x200 | Submit_TextureWithPose | Submit_TextureWithDepth, - // Do not use Submit_Reserved2 = 0x08000, Submit_Reserved3 = 0x10000, Submit_Reserved4 = 0x20000, @@ -802,12 +731,9 @@ enum EVRSubmitFlags Submit_Reserved6 = 0x80000, }; -/** Data required for passing Vulkan textures to IVRCompositor::Submit. -* Be sure to call OpenVR_Shutdown before destroying these resources. -* Please see https://github.com/ValveSoftware/openvr/wiki/Vulkan for Vulkan-specific documentation */ struct VRVulkanTextureData_t { - uint64_t m_nImage; // VkImage + uint64_t m_nImage; VkDevice_T *m_pDevice; VkPhysicalDevice_T *m_pPhysicalDevice; VkInstance_T *m_pInstance; @@ -816,17 +742,12 @@ struct VRVulkanTextureData_t uint32_t m_nWidth, m_nHeight, m_nFormat, m_nSampleCount; }; -/** Data required for passing Vulkan texture arrays to IVRCompositor::Submit. -* Be sure to call OpenVR_Shutdown before destroying these resources. -* Please see https://github.com/ValveSoftware/openvr/wiki/Vulkan for Vulkan-specific documentation */ struct VRVulkanTextureArrayData_t : public VRVulkanTextureData_t { uint32_t m_unArrayIndex; uint32_t m_unArraySize; }; -/** Data required for passing D3D12 textures to IVRCompositor::Submit. -* Be sure to call OpenVR_Shutdown before destroying these resources. */ struct D3D12TextureData_t { ID3D12Resource *m_pResource; @@ -834,7 +755,6 @@ struct D3D12TextureData_t uint32_t m_nNodeMask; }; -/** Status of the overall system or tracked objects */ enum EVRState { VRState_Undefined = -1, @@ -848,7 +768,6 @@ enum EVRState VRState_Ready_Alert_Low = 7, }; -/** The types of events that could be posted (and what the parameters mean for each event type) */ enum EVREventType { VREvent_None = 0, @@ -870,132 +789,132 @@ enum EVREventType VREvent_Reserved_0114 = 114, VREvent_Reserved_0115 = 115, - VREvent_ButtonPress = 200, // data is controller - VREvent_ButtonUnpress = 201, // data is controller - VREvent_ButtonTouch = 202, // data is controller - VREvent_ButtonUntouch = 203, // data is controller + VREvent_ButtonPress = 200, + VREvent_ButtonUnpress = 201, + VREvent_ButtonTouch = 202, + VREvent_ButtonUntouch = 203, - // VREvent_DualAnalog_Press = 250, // No longer sent - // VREvent_DualAnalog_Unpress = 251, // No longer sent - // VREvent_DualAnalog_Touch = 252, // No longer sent - // VREvent_DualAnalog_Untouch = 253, // No longer sent - // VREvent_DualAnalog_Move = 254, // No longer sent - // VREvent_DualAnalog_ModeSwitch1 = 255, // No longer sent - // VREvent_DualAnalog_ModeSwitch2 = 256, // No longer sent - VREvent_Modal_Cancel = 257, // Sent to overlays with the - VREvent_MouseMove = 300, // data is mouse - VREvent_MouseButtonDown = 301, // data is mouse - VREvent_MouseButtonUp = 302, // data is mouse - VREvent_FocusEnter = 303, // data is overlay - VREvent_FocusLeave = 304, // data is overlay - VREvent_ScrollDiscrete = 305, // data is scroll - VREvent_TouchPadMove = 306, // data is mouse - VREvent_OverlayFocusChanged = 307, // data is overlay, global event + + + + + + + VREvent_Modal_Cancel = 257, + + VREvent_MouseMove = 300, + VREvent_MouseButtonDown = 301, + VREvent_MouseButtonUp = 302, + VREvent_FocusEnter = 303, + VREvent_FocusLeave = 304, + VREvent_ScrollDiscrete = 305, + VREvent_TouchPadMove = 306, + VREvent_OverlayFocusChanged = 307, VREvent_ReloadOverlays = 308, - VREvent_ScrollSmooth = 309, // data is scroll - VREvent_LockMousePosition = 310, // data is mouse - VREvent_UnlockMousePosition = 311, // data is mouse + VREvent_ScrollSmooth = 309, + VREvent_LockMousePosition = 310, + VREvent_UnlockMousePosition = 311, - VREvent_InputFocusCaptured = 400, // data is process DEPRECATED - VREvent_InputFocusReleased = 401, // data is process DEPRECATED - // VREvent_SceneFocusLost = 402, // data is process - // VREvent_SceneFocusGained = 403, // data is process - VREvent_SceneApplicationChanged = 404, // data is process - The App actually drawing the scene changed (usually to or from the compositor) - // VREvent_SceneFocusChanged = 405, // data is process - This is defunct and will not be called. - VREvent_InputFocusChanged = 406, // data is process - // VREvent_SceneApplicationSecondaryRenderingStarted = 407, - VREvent_SceneApplicationUsingWrongGraphicsAdapter = 408, // data is process - VREvent_ActionBindingReloaded = 409, // data is process - The App that action binds reloaded for + VREvent_InputFocusCaptured = 400, + VREvent_InputFocusReleased = 401, - VREvent_HideRenderModels = 410, // Sent to the scene application to request hiding render models temporarily - VREvent_ShowRenderModels = 411, // Sent to the scene application to request restoring render model visibility - VREvent_SceneApplicationStateChanged = 412, // No data; but query VRApplications()->GetSceneApplicationState(); + VREvent_SceneApplicationChanged = 404, - VREvent_SceneAppPipeDisconnected = 413, // data is process - Called when the scene app's pipe has been closed. + VREvent_InputFocusChanged = 406, + + VREvent_SceneApplicationUsingWrongGraphicsAdapter = 408, + VREvent_ActionBindingReloaded = 409, + + VREvent_HideRenderModels = 410, + VREvent_ShowRenderModels = 411, + + VREvent_SceneApplicationStateChanged = 412, + + VREvent_SceneAppPipeDisconnected = 413, VREvent_ConsoleOpened = 420, VREvent_ConsoleClosed = 421, - VREvent_OverlayShown = 500, // Indicates that an overlay is now visible to someone and should be rendering normally. Reflects IVROverlay::IsOverlayVisible() becoming true. - VREvent_OverlayHidden = 501, // Indicates that an overlay is no longer visible to someone and doesn't need to render frames. Reflects IVROverlay::IsOverlayVisible() becoming false. + VREvent_OverlayShown = 500, + VREvent_OverlayHidden = 501, VREvent_DashboardActivated = 502, VREvent_DashboardDeactivated = 503, - //VREvent_DashboardThumbSelected = 504, // Sent to the overlay manager - data is overlay - No longer sent - //VREvent_DashboardRequested = 505, // Sent to the overlay manager - data is overlay - VREvent_ResetDashboard = 506, // Send to the overlay manager - //VREvent_RenderToast = 507, // Send to the dashboard to render a toast - data is the notification ID -- no longer sent - VREvent_ImageLoaded = 508, // Sent to overlays when a SetOverlayRaw or SetOverlayFromFile call finishes loading - VREvent_ShowKeyboard = 509, // Sent to keyboard renderer in the dashboard to invoke it - VREvent_HideKeyboard = 510, // Sent to keyboard renderer in the dashboard to hide it - VREvent_OverlayGamepadFocusGained = 511, // Sent to an overlay when IVROverlay::SetFocusOverlay is called on it - VREvent_OverlayGamepadFocusLost = 512, // Send to an overlay when it previously had focus and IVROverlay::SetFocusOverlay is called on something else + + + VREvent_ResetDashboard = 506, + + VREvent_ImageLoaded = 508, + VREvent_ShowKeyboard = 509, + VREvent_HideKeyboard = 510, + VREvent_OverlayGamepadFocusGained = 511, + VREvent_OverlayGamepadFocusLost = 512, VREvent_OverlaySharedTextureChanged = 513, - //VREvent_DashboardGuideButtonDown = 514, // These are no longer sent - //VREvent_DashboardGuideButtonUp = 515, - VREvent_ScreenshotTriggered = 516, // Screenshot button combo was pressed, Dashboard should request a screenshot - VREvent_ImageFailed = 517, // Sent to overlays when a SetOverlayRaw or SetOverlayfromFail fails to load + + + VREvent_ScreenshotTriggered = 516, + VREvent_ImageFailed = 517, VREvent_DashboardOverlayCreated = 518, VREvent_SwitchGamepadFocus = 519, - // Screenshot API - VREvent_RequestScreenshot = 520, // Sent by vrclient application to compositor to take a screenshot - VREvent_ScreenshotTaken = 521, // Sent by compositor to the application that the screenshot has been taken - VREvent_ScreenshotFailed = 522, // Sent by compositor to the application that the screenshot failed to be taken - VREvent_SubmitScreenshotToDashboard = 523, // Sent by compositor to the dashboard that a completed screenshot was submitted - VREvent_ScreenshotProgressToDashboard = 524, // Sent by compositor to the dashboard that a completed screenshot was submitted + + VREvent_RequestScreenshot = 520, + VREvent_ScreenshotTaken = 521, + VREvent_ScreenshotFailed = 522, + VREvent_SubmitScreenshotToDashboard = 523, + VREvent_ScreenshotProgressToDashboard = 524, VREvent_PrimaryDashboardDeviceChanged = 525, - VREvent_RoomViewShown = 526, // Sent by compositor whenever room-view is enabled (for scene apps only - not for construct or transient bounds) - VREvent_RoomViewHidden = 527, // Sent by compositor whenever room-view is disabled (for scene apps only - not for construct or transient bounds) - VREvent_ShowUI = 528, // data is showUi - VREvent_ShowDevTools = 529, // data is showDevTools + VREvent_RoomViewShown = 526, + VREvent_RoomViewHidden = 527, + VREvent_ShowUI = 528, + VREvent_ShowDevTools = 529, VREvent_DesktopViewUpdating = 530, VREvent_DesktopViewReady = 531, VREvent_StartDashboard = 532, VREvent_ElevatePrism = 533, - VREvent_OverlayClosed = 534, // The overlay's close button is pressed. - VREvent_DashboardThumbChanged = 535, // Sent when a dashboard thumbnail image changes - VREvent_DesktopMightBeVisible = 536, // Sent when any known desktop related overlay is visible - VREvent_DesktopMightBeHidden = 537, // Sent when all known desktop related overlays are hidden - VREvent_MutualSteamCapabilitiesChanged = 538, // Sent when the set of capabilities common between both Steam and SteamVR have changed. - VREvent_OverlayCreated = 539, // An OpenVR overlay of any sort was created. Data is overlay. - VREvent_OverlayDestroyed = 540, // An OpenVR overlay of any sort was destroyed. Data is overlay. - VREvent_OverlayNameChanged = 544, // An OpenVR overlay's name changed. Data is overlay. + VREvent_OverlayClosed = 534, + VREvent_DashboardThumbChanged = 535, + VREvent_DesktopMightBeVisible = 536, + VREvent_DesktopMightBeHidden = 537, + VREvent_MutualSteamCapabilitiesChanged = 538, + VREvent_OverlayCreated = 539, + VREvent_OverlayDestroyed = 540, + VREvent_OverlayNameChanged = 544, VREvent_TrackingRecordingStarted = 541, VREvent_TrackingRecordingStopped = 542, VREvent_SetTrackingRecordingPath = 543, - VREvent_Reserved_0560 = 560, // No data - VREvent_Reserved_0561 = 561, // No data - VREvent_Reserved_0562 = 562, // No data - VREvent_Reserved_0563 = 563, // No data + VREvent_Reserved_0560 = 560, + VREvent_Reserved_0561 = 561, + VREvent_Reserved_0562 = 562, + VREvent_Reserved_0563 = 563, VREvent_Notification_Shown = 600, VREvent_Notification_Hidden = 601, VREvent_Notification_BeginInteraction = 602, VREvent_Notification_Destroyed = 603, - VREvent_Quit = 700, // data is process - VREvent_ProcessQuit = 701, // data is process - //VREvent_QuitAborted_UserPrompt = 702, // data is process - VREvent_QuitAcknowledged = 703, // data is process - VREvent_DriverRequestedQuit = 704, // The driver has requested that SteamVR shut down - VREvent_RestartRequested = 705, // A driver or other component wants the user to restart SteamVR - VREvent_InvalidateSwapTextureSets = 706, - VREvent_RequestDisconnectWirelessHMD = 707, // vrserver asks vrlink to disconnect + VREvent_Quit = 700, + VREvent_ProcessQuit = 701, - VREvent_ChaperoneDataHasChanged = 800, // this will never happen with the new chaperone system + VREvent_QuitAcknowledged = 703, + VREvent_DriverRequestedQuit = 704, + VREvent_RestartRequested = 705, + VREvent_InvalidateSwapTextureSets = 706, + VREvent_RequestDisconnectWirelessHMD = 707, + + VREvent_ChaperoneDataHasChanged = 800, VREvent_ChaperoneUniverseHasChanged = 801, - VREvent_ChaperoneTempDataHasChanged = 802, // this will never happen with the new chaperone system + VREvent_ChaperoneTempDataHasChanged = 802, VREvent_ChaperoneSettingsHaveChanged = 803, VREvent_SeatedZeroPoseReset = 804, - VREvent_ChaperoneFlushCache = 805, // Sent when the process needs to reload any cached data it retrieved from VRChaperone() - VREvent_ChaperoneRoomSetupStarting = 806, // Triggered by CVRChaperoneClient::RoomSetupStarting - VREvent_ChaperoneRoomSetupCommitted = 807, // Triggered by CVRChaperoneClient::CommitWorkingCopy (formerly VREvent_ChaperoneRoomSetupFinished) + VREvent_ChaperoneFlushCache = 805, + VREvent_ChaperoneRoomSetupStarting = 806, + VREvent_ChaperoneRoomSetupCommitted = 807, VREvent_StandingZeroPoseReset = 808, VREvent_Reserved_0809 = 809, VREvent_Reserved_0810 = 810, @@ -1040,32 +959,26 @@ enum EVREventType VREvent_FirmwareUpdateStarted = 1100, VREvent_FirmwareUpdateFinished = 1101, - VREvent_KeyboardClosed = 1200, // DEPRECATED: Sent only to the overlay it closed for, or globally if it was closed for a scene app - VREvent_KeyboardCharInput = 1201, // Sent on keyboard input. Warning: event type appears as both global event and overlay event - VREvent_KeyboardDone = 1202, // Sent when DONE button clicked on keyboard. Warning: event type appears as both global event and overlay event - VREvent_KeyboardOpened_Global = 1203, // Sent globally when the keyboard is opened. data.keyboard.overlayHandle is who it was opened for (scene app if k_ulOverlayHandleInvalid) - VREvent_KeyboardClosed_Global = 1204, // Sent globally when the keyboard is closed. data.keyboard.overlayHandle is who it was opened for (scene app if k_ulOverlayHandleInvalid) + VREvent_KeyboardClosed = 1200, + VREvent_KeyboardCharInput = 1201, + VREvent_KeyboardDone = 1202, + VREvent_KeyboardOpened_Global = 1203, + VREvent_KeyboardClosed_Global = 1204, - //VREvent_ApplicationTransitionStarted = 1300, - //VREvent_ApplicationTransitionAborted = 1301, - //VREvent_ApplicationTransitionNewAppStarted = 1302, VREvent_ApplicationListUpdated = 1303, VREvent_ApplicationMimeTypeLoad = 1304, - // VREvent_ApplicationTransitionNewAppLaunchComplete = 1305, VREvent_ProcessConnected = 1306, VREvent_ProcessDisconnected = 1307, - //VREvent_Compositor_MirrorWindowShown = 1400, // DEPRECATED - //VREvent_Compositor_MirrorWindowHidden = 1401, // DEPRECATED VREvent_Compositor_ChaperoneBoundsShown = 1410, VREvent_Compositor_ChaperoneBoundsHidden = 1411, VREvent_Compositor_DisplayDisconnected = 1412, VREvent_Compositor_DisplayReconnected = 1413, - VREvent_Compositor_HDCPError = 1414, // data is hdcpError + VREvent_Compositor_HDCPError = 1414, VREvent_Compositor_ApplicationNotResponding = 1415, VREvent_Compositor_ApplicationResumed = 1416, VREvent_Compositor_OutOfVideoMemory = 1417, - VREvent_Compositor_DisplayModeNotSupported = 1418, // k_pch_SteamVR_PreferredRefreshRate + VREvent_Compositor_DisplayModeNotSupported = 1418, VREvent_Compositor_StageOverrideReady = 1419, VREvent_Compositor_RequestDisconnectReconnect = 1420, @@ -1082,56 +995,49 @@ enum EVREventType VREvent_MessageOverlay_Closed = 1650, VREvent_MessageOverlayCloseRequested = 1651, - VREvent_Input_HapticVibration = 1700, // data is hapticVibration - VREvent_Input_BindingLoadFailed = 1701, // data is inputBinding - VREvent_Input_BindingLoadSuccessful = 1702, // data is inputBinding - VREvent_Input_ActionManifestReloaded = 1703, // no data - VREvent_Input_ActionManifestLoadFailed = 1704, // data is actionManifest - VREvent_Input_ProgressUpdate = 1705, // data is progressUpdate + VREvent_Input_HapticVibration = 1700, + VREvent_Input_BindingLoadFailed = 1701, + VREvent_Input_BindingLoadSuccessful = 1702, + VREvent_Input_ActionManifestReloaded = 1703, + VREvent_Input_ActionManifestLoadFailed = 1704, + VREvent_Input_ProgressUpdate = 1705, VREvent_Input_TrackerActivated = 1706, VREvent_Input_BindingsUpdated = 1707, VREvent_Input_BindingSubscriptionChanged = 1708, - VREvent_SpatialAnchors_PoseUpdated = 1800, // data is spatialAnchor. broadcast - VREvent_SpatialAnchors_DescriptorUpdated = 1801, // data is spatialAnchor. broadcast - VREvent_SpatialAnchors_RequestPoseUpdate = 1802, // data is spatialAnchor. sent to specific driver - VREvent_SpatialAnchors_RequestDescriptorUpdate = 1803, // data is spatialAnchor. sent to specific driver + VREvent_SpatialAnchors_PoseUpdated = 1800, + VREvent_SpatialAnchors_DescriptorUpdated = 1801, + VREvent_SpatialAnchors_RequestPoseUpdate = 1802, + VREvent_SpatialAnchors_RequestDescriptorUpdate = 1803, - VREvent_SystemReport_Started = 1900, // user or system initiated generation of a system report. broadcast + VREvent_SystemReport_Started = 1900, - VREvent_Monitor_ShowHeadsetView = 2000, // data is process - VREvent_Monitor_HideHeadsetView = 2001, // data is process + VREvent_Monitor_ShowHeadsetView = 2000, + VREvent_Monitor_HideHeadsetView = 2001, VREvent_Audio_SetSpeakersVolume = 2100, VREvent_Audio_SetSpeakersMute = 2101, VREvent_Audio_SetMicrophoneVolume = 2102, VREvent_Audio_SetMicrophoneMute = 2103, - VREvent_RenderModel_CountChanged = 2200, //Number of RenderModels in the system has changed + VREvent_RenderModel_CountChanged = 2200, - // Vendors are free to expose private events in this reserved region VREvent_VendorSpecific_Reserved_Start = 10000, VREvent_VendorSpecific_Reserved_End = 19999, }; -/** Level of Hmd activity */ -// UserInteraction_Timeout means the device is in the process of timing out. -// InUse = ( k_EDeviceActivityLevel_UserInteraction || k_EDeviceActivityLevel_UserInteraction_Timeout ) -// VREvent_TrackedDeviceUserInteractionStarted fires when the devices transitions from Standby -> UserInteraction or Idle -> UserInteraction. -// VREvent_TrackedDeviceUserInteractionEnded fires when the devices transitions from UserInteraction_Timeout -> Idle enum EDeviceActivityLevel { k_EDeviceActivityLevel_Unknown = -1, - k_EDeviceActivityLevel_Idle = 0, // No activity for the last 10 seconds - k_EDeviceActivityLevel_UserInteraction = 1, // Activity (movement or prox sensor) is happening now - k_EDeviceActivityLevel_UserInteraction_Timeout = 2, // No activity for the last 0.5 seconds - k_EDeviceActivityLevel_Standby = 3, // Idle for at least 5 seconds (configurable in Settings -> Power Management) + k_EDeviceActivityLevel_Idle = 0, + k_EDeviceActivityLevel_UserInteraction = 1, + k_EDeviceActivityLevel_UserInteraction_Timeout = 2, + k_EDeviceActivityLevel_Standby = 3, k_EDeviceActivityLevel_Idle_Timeout = 4, }; -/** VR controller button and axis IDs */ enum EVRButtonId { k_EButton_System = 0, @@ -1151,7 +1057,6 @@ enum EVRButtonId k_EButton_Axis3 = 35, k_EButton_Axis4 = 36, - // aliases for well known controllers k_EButton_SteamVR_Touchpad = k_EButton_Axis0, k_EButton_SteamVR_Trigger = k_EButton_Axis1, @@ -1169,14 +1074,12 @@ enum EVRButtonId inline uint64_t ButtonMaskFromId( EVRButtonId id ) { return 1ull << id; } -/** used for controller button events */ struct VREvent_Controller_t { - uint32_t button; // EVRButtonId enum + uint32_t button; }; -/** used for simulated mouse events in overlay space */ enum EVRMouseButton { VRMouseButton_Left = 0x0001, @@ -1185,93 +1088,71 @@ enum EVRMouseButton }; -/** used for simulated mouse events in overlay space */ struct VREvent_Mouse_t { - float x, y; // co-ords are in GL space, bottom left of the texture is 0,0 - uint32_t button; // EVRMouseButton enum + float x, y; + uint32_t button; - // if from an event triggered by cursor input on an overlay that supports multiple cursors, this is the index of - // which tracked cursor the event is for uint32_t cursorIndex; }; -/** used for simulated mouse wheel scroll */ struct VREvent_Scroll_t { float xdelta, ydelta; uint32_t unused; - float viewportscale; // For scrolling on an overlay with laser mouse, this is the overlay's vertical size relative to the overlay height. Range: [0,1] + float viewportscale; - // if from an event triggered by cursor input on an overlay that supports multiple cursors, this is the index of - // which tracked cursor the event is for uint32_t cursorIndex; }; -/** when in mouse input mode you can receive data from the touchpad, these events are only sent if the users finger - is on the touchpad (or just released from it). These events are sent to overlays with the VROverlayFlags_SendVRTouchpadEvents - flag set. -**/ struct VREvent_TouchPadMove_t { - // true if the users finger is detected on the touch pad bool bFingerDown; - // How long the finger has been down in seconds float flSecondsFingerDown; - // These values indicate the starting finger position (so you can do some basic swipe stuff) float fValueXFirst; float fValueYFirst; - // This is the raw sampled coordinate without deadzoning float fValueXRaw; float fValueYRaw; }; -/** notification related events. Details will still change at this point */ struct VREvent_Notification_t { uint64_t ulUserValue; uint32_t notificationId; }; -/** Used for events about processes */ struct VREvent_Process_t { uint32_t pid; uint32_t oldPid; bool bForced; - // If the associated event was triggered by a connection loss bool bConnectionLost; }; -/** Used for a few events about overlays */ struct VREvent_Overlay_t { - uint64_t overlayHandle; // VROverlayHandle_t + uint64_t overlayHandle; uint64_t devicePath; uint64_t memoryBlockId; - // if from an event triggered by cursor input on an overlay that supports multiple cursors, this is the index of - // which tracked cursor the event is for uint32_t cursorIndex; }; -/** Used for a few events about overlays */ struct VREvent_Status_t { - uint32_t statusState; // EVRState enum + uint32_t statusState; }; -/** Used for keyboard events */ struct VREvent_Keyboard_t { - char cNewInput[8]; // 7 bytes of utf8 + null - uint64_t uUserValue; // caller specified opaque token - uint64_t overlayHandle; // VROverlayHandle_t + char cNewInput[8]; + uint64_t uUserValue; + uint64_t overlayHandle; }; struct VREvent_Ipd_t @@ -1285,7 +1166,6 @@ struct VREvent_Chaperone_t uint64_t m_nCurrentUniverse; }; -/** Not actually used for any events */ struct VREvent_Reserved_t { uint64_t reserved0; @@ -1331,7 +1211,7 @@ struct VREvent_EditingCameraSurface_t struct VREvent_MessageOverlay_t { - uint32_t unVRMessageOverlayResponse; // vr::VRMessageOverlayResponse enum + uint32_t unVRMessageOverlayResponse; }; struct VREvent_Property_t @@ -1342,8 +1222,8 @@ struct VREvent_Property_t struct VREvent_HapticVibration_t { - uint64_t containerHandle; // property container handle of the device with the haptic component - uint64_t componentHandle; // Which haptic component needs to vibrate + uint64_t containerHandle; + uint64_t componentHandle; float fDurationSeconds; float fFrequency; float fAmplitude; @@ -1389,7 +1269,6 @@ enum EShowUIType { ShowUI_ControllerBinding = 0, ShowUI_ManageTrackers = 1, - // ShowUI_QuickStart = 2, // Deprecated ShowUI_Pairing = 3, ShowUI_Settings = 4, ShowUI_DebugCommands = 5, @@ -1464,23 +1343,18 @@ typedef union VREvent_HDCPError_t hdcpError; VREvent_AudioVolumeControl_t audioVolumeControl; VREvent_AudioMuteControl_t audioMuteControl; - /** NOTE!!! If you change this you MUST manually update openvr_interop.cs.py and openvr_api_flat.h.py */ } VREvent_Data_t; #if defined(__linux__) || defined(__APPLE__) -// This structure was originally defined mis-packed on Linux, preserved for -// compatibility. #pragma pack( push, 4 ) #endif -/** An event posted by the server to all running applications */ struct VREvent_t { - uint32_t eventType; // EVREventType enum + uint32_t eventType; TrackedDeviceIndex_t trackedDeviceIndex; float eventAgeSeconds; - // event data must be the end of the struct as its size is variable VREvent_Data_t data; }; @@ -1501,11 +1375,10 @@ enum EVRComponentProperty }; -/** Describes state information about a render-model component, including transforms and other dynamic properties */ struct RenderModel_ComponentState_t { - HmdMatrix34_t mTrackingToComponentRenderModel; // Transform required when drawing the component render model - HmdMatrix34_t mTrackingToComponentLocal; // Transform available for attaching to a local component coordinate system (-Z out from surface ) + HmdMatrix34_t mTrackingToComponentRenderModel; + HmdMatrix34_t mTrackingToComponentLocal; VRComponentProperties uProperties; }; @@ -1553,12 +1426,6 @@ enum EVRSpatialAnchorError VRSpatialAnchorError_UnknownDriver = 13, }; -/** The mesh to draw into the stencil (or depth) buffer to perform -* early stencil (or depth) kills of pixels that will never appear on the HMD. -* This mesh draws on all the pixels that will be hidden after distortion. -* -* If the HMD does not provide a visible area mesh pVertexData will be -* NULL and unTriangleCount will be 0. */ struct HiddenAreaMesh_t { const HmdVector2_t *pVertexData; @@ -1576,48 +1443,36 @@ enum EHiddenAreaMeshType }; -/** Identifies what kind of axis is on the controller at index n. Read this type -* with pVRSystem->Get( nControllerDeviceIndex, Prop_Axis0Type_Int32 + n ); -*/ enum EVRControllerAxisType { k_eControllerAxis_None = 0, k_eControllerAxis_TrackPad = 1, k_eControllerAxis_Joystick = 2, - k_eControllerAxis_Trigger = 3, // Analog trigger data is in the X axis + k_eControllerAxis_Trigger = 3, }; -/** contains information about one axis on the controller */ struct VRControllerAxis_t { - float x; // Ranges from -1.0 to 1.0 for joysticks and track pads. Ranges from 0.0 to 1.0 for triggers were 0 is fully released. - float y; // Ranges from -1.0 to 1.0 for joysticks and track pads. Is always 0.0 for triggers. + float x; + float y; }; -/** the number of axes in the controller state */ static const uint32_t k_unControllerStateAxisCount = 5; #if defined(__linux__) || defined(__APPLE__) -// This structure was originally defined mis-packed on Linux, preserved for -// compatibility. #pragma pack( push, 4 ) #endif -/** Holds all the state of a controller at one moment in time. */ struct VRControllerState001_t { - // If packet num matches that on your prior call, then the controller state hasn't been changed since - // your last call and there is no need to process it uint32_t unPacketNum; - // bit flags for each of the buttons. Use ButtonMaskFromId to turn an ID into a mask uint64_t ulButtonPressed; uint64_t ulButtonTouched; - // Axis data for the controller's analog inputs VRControllerAxis_t rAxis[ k_unControllerStateAxisCount ]; }; #if defined(__linux__) || defined(__APPLE__) @@ -1628,7 +1483,6 @@ struct VRControllerState001_t typedef VRControllerState001_t VRControllerState_t; -/** determines how to provide output to the application of various event processing functions. */ enum EVRControllerEventOutputType { ControllerEventOutput_OSEvents = 0, @@ -1637,7 +1491,6 @@ enum EVRControllerEventOutputType -/** Collision Bounds Style */ enum ECollisionBoundsStyle { COLLISION_BOUNDS_STYLE_BEGINNER = 0, @@ -1649,12 +1502,10 @@ enum ECollisionBoundsStyle COLLISION_BOUNDS_STYLE_COUNT }; -/** used to refer to a single VR overlay */ typedef uint64_t VROverlayHandle_t; static const VROverlayHandle_t k_ulOverlayHandleInvalid = 0; -/** Errors that can occur around VR overlays */ enum EVROverlayError { VROverlayError_None = 0, @@ -1662,7 +1513,7 @@ enum EVROverlayError VROverlayError_UnknownOverlay = 10, VROverlayError_InvalidHandle = 11, VROverlayError_PermissionDenied = 12, - VROverlayError_OverlayLimitExceeded = 13, // No more overlays could be created because the maximum number already exist + VROverlayError_OverlayLimitExceeded = 13, VROverlayError_WrongVisibilityType = 14, VROverlayError_KeyTooLong = 15, VROverlayError_NameTooLong = 16, @@ -1685,33 +1536,29 @@ enum EVROverlayError VROverlayError_TimedOut = 34, }; -/** enum values to pass in to VR_Init to identify whether the application will -* draw a 3D scene. */ enum EVRApplicationType { - VRApplication_Other = 0, // Some other kind of application that isn't covered by the other entries - VRApplication_Scene = 1, // Application will submit 3D frames - VRApplication_Overlay = 2, // Application only interacts with overlays - VRApplication_Background = 3, // Application should not start SteamVR if it's not already running, and should not - // keep it running if everything else quits. - VRApplication_Utility = 4, // Init should not try to load any drivers. The application needs access to utility - // interfaces (like IVRSettings and IVRApplications) but not hardware. - VRApplication_VRMonitor = 5, // Reserved for vrmonitor - VRApplication_SteamWatchdog = 6,// Reserved for Steam - VRApplication_Bootstrapper = 7, // reserved for vrstartup - VRApplication_WebHelper = 8, // reserved for vrwebhelper - VRApplication_OpenXRInstance = 9, // reserved for openxr (created instance, but not session yet) - VRApplication_OpenXRScene = 10, // reserved for openxr (started session) - VRApplication_OpenXROverlay = 11, // reserved for openxr (started overlay session) - VRApplication_Prism = 12, // reserved for the vrprismhost process - VRApplication_RoomView = 13, // reserved for the RoomView process + VRApplication_Other = 0, + VRApplication_Scene = 1, + VRApplication_Overlay = 2, + VRApplication_Background = 3, + + VRApplication_Utility = 4, + + VRApplication_VRMonitor = 5, + VRApplication_SteamWatchdog = 6, + VRApplication_Bootstrapper = 7, + VRApplication_WebHelper = 8, + VRApplication_OpenXRInstance = 9, + VRApplication_OpenXRScene = 10, + VRApplication_OpenXROverlay = 11, + VRApplication_Prism = 12, + VRApplication_RoomView = 13, VRApplication_Max }; -/** returns true if the specified application type is one of the -* OpenXR types */ inline bool IsOpenXRAppType( EVRApplicationType eType ) { return eType == VRApplication_OpenXRInstance @@ -1720,7 +1567,6 @@ inline bool IsOpenXRAppType( EVRApplicationType eType ) } -/** returns true if the specified application type submits eye buffers */ inline bool BAppTypeSubmitsEyeBuffers( EVRApplicationType eType ) { return eType == VRApplication_Scene @@ -1729,7 +1575,6 @@ inline bool BAppTypeSubmitsEyeBuffers( EVRApplicationType eType ) } -/** error codes for firmware */ enum EVRFirmwareError { VRFirmwareError_None = 0, @@ -1738,7 +1583,6 @@ enum EVRFirmwareError }; -/** error codes for notifications */ enum EVRNotificationError { VRNotificationError_OK = 0, @@ -1752,31 +1596,17 @@ enum EVRNotificationError enum EVRSkeletalMotionRange { - // The range of motion of the skeleton takes into account any physical limits imposed by - // the controller itself. This will tend to be the most accurate pose compared to the user's - // actual hand pose, but might not allow a closed fist for example VRSkeletalMotionRange_WithController = 0, - // Retarget the range of motion provided by the input device to make the hand appear to move - // as if it was not holding a controller. eg: map "hand grasping controller" to "closed fist" VRSkeletalMotionRange_WithoutController = 1, }; enum EVRSkeletalTrackingLevel { - // body part location can't be directly determined by the device. Any skeletal pose provided by - // the device is estimated by assuming the position required to active buttons, triggers, joysticks, - // or other input sensors. - // E.g. Vive Controller, Gamepad VRSkeletalTracking_Estimated = 0, - // body part location can be measured directly but with fewer degrees of freedom than the actual body - // part. Certain body part positions may be unmeasured by the device and estimated from other input data. - // E.g. Index Controllers, gloves that only measure finger curl VRSkeletalTracking_Partial = 1, - // Body part location can be measured directly throughout the entire range of motion of the body part. - // E.g. Mocap suit for the full body, gloves that measure rotation of each finger segment VRSkeletalTracking_Full = 2, VRSkeletalTrackingLevel_Count, @@ -1784,14 +1614,11 @@ enum EVRSkeletalTrackingLevel }; -/** Type used for referring to bones by their index */ typedef int32_t BoneIndex_t; const BoneIndex_t k_unInvalidBoneIndex = -1; -/** error codes returned by Vr_Init */ -// Please add adequate error description to https://developer.valvesoftware.com/w/index.php?title=Category:SteamVRHelp enum EVRInitError { VRInitError_None = 0, @@ -1812,8 +1639,8 @@ enum EVRInitError VRInitError_Init_NoLogPath = 112, VRInitError_Init_PathRegistryNotWritable = 113, VRInitError_Init_AppInfoInitFailed = 114, - VRInitError_Init_Retry = 115, // Used internally to cause retries to vrserver - VRInitError_Init_InitCanceledByUser = 116, // The calling application should silently exit. The user canceled app startup + VRInitError_Init_Retry = 115, + VRInitError_Init_InitCanceledByUser = 116, VRInitError_Init_AnotherAppLaunching = 117, VRInitError_Init_SettingsInitFailed = 118, VRInitError_Init_ShuttingDown = 119, @@ -1878,14 +1705,12 @@ enum EVRInitError VRInitError_Driver_CalibrationInvalid = 207, VRInitError_Driver_HmdDisplayNotFound = 208, VRInitError_Driver_TrackedDeviceInterfaceUnknown = 209, - // VRInitError_Driver_HmdDisplayNotFoundAfterFix = 210, // not needed: here for historic reasons VRInitError_Driver_HmdDriverIdOutOfBounds = 211, VRInitError_Driver_HmdDisplayMirrored = 212, VRInitError_Driver_HmdDisplayNotFoundLaptop = 213, VRInitError_Driver_PeerDriverNotInstalled = 214, VRInitError_Driver_WirelessHmdNotConnected = 215, - // Never make error 259 because we return it from main and it would conflict with STILL_ACTIVE VRInitError_IPC_ServerInitFailed = 300, VRInitError_IPC_ConnectFailed = 301, @@ -2029,14 +1854,13 @@ enum EVRInitError VRInitError_Steam_SteamInstallationNotFound = 2000, - // Strictly a placeholder VRInitError_LastError }; enum EVRScreenshotType { VRScreenshotType_None = 0, - VRScreenshotType_Mono = 1, // left eye only + VRScreenshotType_Mono = 1, VRScreenshotType_Stereo = 2, VRScreenshotType_Cubemap = 3, VRScreenshotType_MonoPanorama = 4, @@ -2074,15 +1898,15 @@ enum EVRTrackedCameraFrameLayout { EVRTrackedCameraFrameLayout_Mono = 0x0001, EVRTrackedCameraFrameLayout_Stereo = 0x0002, - EVRTrackedCameraFrameLayout_VerticalLayout = 0x0010, // Stereo frames are Top/Bottom (left/right) - EVRTrackedCameraFrameLayout_HorizontalLayout = 0x0020, // Stereo frames are Left/Right + EVRTrackedCameraFrameLayout_VerticalLayout = 0x0010, + EVRTrackedCameraFrameLayout_HorizontalLayout = 0x0020, }; enum EVRTrackedCameraFrameType { - VRTrackedCameraFrameType_Distorted = 0, // This is the camera video frame size in pixels, still distorted. - VRTrackedCameraFrameType_Undistorted, // In pixels, an undistorted inscribed rectangle region without invalid regions. This size is subject to changes shortly. - VRTrackedCameraFrameType_MaximumUndistorted, // In pixels, maximum undistorted with invalid regions. Non zero alpha component identifies valid regions. + VRTrackedCameraFrameType_Distorted = 0, + VRTrackedCameraFrameType_Undistorted, + VRTrackedCameraFrameType_MaximumUndistorted, MAX_CAMERA_FRAME_TYPES }; @@ -2111,35 +1935,33 @@ struct CameraVideoStreamFrameHeader_t TrackedDevicePose_t trackedDevicePose; - uint64_t ulFrameExposureTime; // mid-point of the exposure of the image in host system ticks + uint64_t ulFrameExposureTime; }; -// Screenshot types typedef uint32_t ScreenshotHandle_t; static const uint32_t k_unScreenshotHandleInvalid = 0; -/** Compositor frame timing reprojection flags. */ const uint32_t VRCompositor_ReprojectionReason_Cpu = 0x01; const uint32_t VRCompositor_ReprojectionReason_Gpu = 0x02; -const uint32_t VRCompositor_ReprojectionAsync = 0x04; // This flag indicates the async reprojection mode is active, - // but does not indicate if reprojection actually happened or not. - // Use the ReprojectionReason flags above to check if reprojection - // was actually applied (i.e. scene texture was reused). - // NumFramePresents > 1 also indicates the scene texture was reused, - // and also the number of times that it was presented in total. +const uint32_t VRCompositor_ReprojectionAsync = 0x04; -const uint32_t VRCompositor_ReprojectionMotion = 0x08; // This flag indicates whether or not motion smoothing was triggered for this frame -const uint32_t VRCompositor_PredictionMask = 0xF0; // The runtime may predict more than one frame ahead if - // it detects the application is taking too long to render. These - // bits will contain the count of additional frames (normally zero). - // Use the VR_COMPOSITOR_ADDITIONAL_PREDICTED_FRAMES macro to read from - // the latest frame timing entry. -const uint32_t VRCompositor_ThrottleMask = 0xF00; // Number of frames the compositor is throttling the application. - // Use the VR_COMPOSITOR_NUMBER_OF_THROTTLED_FRAMES macro to read from - // the latest frame timing entry. + + + +const uint32_t VRCompositor_ReprojectionMotion = 0x08; + +const uint32_t VRCompositor_PredictionMask = 0xF0; + + + + + +const uint32_t VRCompositor_ThrottleMask = 0xF00; + + #define VR_COMPOSITOR_ADDITIONAL_PREDICTED_FRAMES( timing ) ( ( ( timing ).m_nReprojectionFlags & vr::VRCompositor_PredictionMask ) >> 4 ) #define VR_COMPOSITOR_NUMBER_OF_THROTTLED_FRAMES( timing ) ( ( ( timing ).m_nReprojectionFlags & vr::VRCompositor_ThrottleMask ) >> 8 ) @@ -2147,45 +1969,43 @@ const uint32_t VRCompositor_ThrottleMask = 0xF00; // Number of frames the comp #if defined(__linux__) || defined(__APPLE__) #pragma pack( push, 4 ) #endif -/** Provides a single frame's timing information to the app */ struct Compositor_FrameTiming { - uint32_t m_nSize; // Set to sizeof( Compositor_FrameTiming ) + uint32_t m_nSize; uint32_t m_nFrameIndex; - uint32_t m_nNumFramePresents; // number of times this frame was presented - uint32_t m_nNumMisPresented; // number of times this frame was presented on a vsync other than it was originally predicted to - uint32_t m_nNumDroppedFrames; // number of additional times previous frame was scanned out + uint32_t m_nNumFramePresents; + uint32_t m_nNumMisPresented; + uint32_t m_nNumDroppedFrames; uint32_t m_nReprojectionFlags; - /** Absolute time reference for comparing frames. This aligns with the vsync that running start is relative to. */ double m_flSystemTimeInSeconds; - /** These times may include work from other processes due to OS scheduling. - * The fewer packets of work these are broken up into, the less likely this will happen. - * GPU work can be broken up by calling Flush. This can sometimes be useful to get the GPU started - * processing that work earlier in the frame. */ - float m_flPreSubmitGpuMs; // time spent rendering the scene (gpu work submitted between WaitGetPoses and second Submit) - float m_flPostSubmitGpuMs; // additional time spent rendering by application (e.g. companion window) - float m_flTotalRenderGpuMs; // time between work submitted immediately after present (ideally vsync) until the end of compositor submitted work - float m_flCompositorRenderGpuMs; // time spend performing distortion correction, rendering chaperone, overlays, etc. - float m_flCompositorRenderCpuMs; // time spent on cpu submitting the above work for this frame - float m_flCompositorIdleCpuMs; // time spent waiting for running start (application could have used this much more time) - /** Miscellaneous measured intervals. */ - float m_flClientFrameIntervalMs; // time between calls to WaitGetPoses - float m_flPresentCallCpuMs; // time blocked on call to present (usually 0.0, but can go long) - float m_flWaitForPresentCpuMs; // time spent spin-waiting for frame index to change (not near-zero indicates wait object failure) - float m_flSubmitFrameMs; // time spent in IVRCompositor::Submit (not near-zero indicates driver issue) - /** The following are all relative to this frame's SystemTimeInSeconds */ + + + float m_flPreSubmitGpuMs; + float m_flPostSubmitGpuMs; + float m_flTotalRenderGpuMs; + float m_flCompositorRenderGpuMs; + float m_flCompositorRenderCpuMs; + float m_flCompositorIdleCpuMs; + + + float m_flClientFrameIntervalMs; + float m_flPresentCallCpuMs; + float m_flWaitForPresentCpuMs; + float m_flSubmitFrameMs; + + float m_flWaitGetPosesCalledMs; float m_flNewPosesReadyMs; - float m_flNewFrameReadyMs; // second call to IVRCompositor::Submit + float m_flNewFrameReadyMs; float m_flCompositorUpdateStartMs; float m_flCompositorUpdateEndMs; float m_flCompositorRenderStartMs; - vr::TrackedDevicePose_t m_HmdPose; // pose used by app to render this frame + vr::TrackedDevicePose_t m_HmdPose; uint32_t m_nNumVSyncsReadyForUse; uint32_t m_nNumVSyncsToFirstView; @@ -2196,34 +2016,32 @@ struct Compositor_FrameTiming #pragma pack( pop ) #endif -/** Provides compositor benchmark results to the app */ struct Compositor_BenchmarkResults { - float m_flMegaPixelsPerSecond; // Measurement of GPU MP/s performed by compositor benchmark - float m_flHmdRecommendedMegaPixelsPerSecond; // Recommended default MP/s given the HMD resolution, refresh, and panel mask. + float m_flMegaPixelsPerSecond; + float m_flHmdRecommendedMegaPixelsPerSecond; }; -/** Frame timing data provided by direct mode drivers. */ struct DriverDirectMode_FrameTiming { - uint32_t m_nSize; // Set to sizeof( DriverDirectMode_FrameTiming ) - uint32_t m_nNumFramePresents; // number of times frame was presented - uint32_t m_nNumMisPresented; // number of times frame was presented on a vsync other than it was originally predicted to - uint32_t m_nNumDroppedFrames; // number of additional times previous frame was scanned out (i.e. compositor missed vsync) + uint32_t m_nSize; + uint32_t m_nNumFramePresents; + uint32_t m_nNumMisPresented; + uint32_t m_nNumDroppedFrames; uint32_t m_nReprojectionFlags; }; -/** These flags will be set on DriverDirectMode_FrameTiming::m_nReprojectionFlags when IVRDriverDirectModeComponent::GetFrameTiming is called for drivers to optionally respond to. */ -const uint32_t VRCompositor_ReprojectionMotion_Enabled = 0x100; // Motion Smoothing is enabled in the UI for the currently running application -const uint32_t VRCompositor_ReprojectionMotion_ForcedOn = 0x200; // Motion Smoothing is forced on in the UI for the currently running application -const uint32_t VRCompositor_ReprojectionMotion_AppThrottled = 0x400; // Application is requesting throttling via ForceInterleavedReprojectionOn + +const uint32_t VRCompositor_ReprojectionMotion_Enabled = 0x100; +const uint32_t VRCompositor_ReprojectionMotion_ForcedOn = 0x200; +const uint32_t VRCompositor_ReprojectionMotion_AppThrottled = 0x400; enum EVSync { VSync_None, - VSync_WaitRender, // block following render work until vsync - VSync_NoWaitRender, // do not block following render work (allow to get started early) + VSync_WaitRender, + VSync_NoWaitRender, }; enum EVRMuraCorrectionMode @@ -2232,7 +2050,6 @@ enum EVRMuraCorrectionMode EVRMuraCorrectionMode_NoCorrection }; -/** raw IMU data provided by IVRIOBuffer from paths to tracked devices with IMUs */ enum Imu_OffScaleFlags { OffScale_AccelX = 0x01, @@ -2253,23 +2070,22 @@ struct ImuSample_t enum class EVRDistortionChannel : uint32_t { - Red = 0, // given a coordinate in distorted panel space, returns the coordinate to sample in rectilinear render space for the red channel - Green, // given a coordinate in distorted panel space, returns the coordinate to sample in rectilinear render space for the green channel - Blue, // given a coordinate in distorted panel space, returns the coordinate to sample in rectilinear render space for the blue channel - InverseRed, // given a coordinate in rectilinear render space, returns the corresponding coordinate in distorted panel space for the red channel - InverseGreen, // given a coordinate in rectilinear render space, returns the corresponding coordinate in distorted panel space for the green channel - InverseBlue, // given a coordinate in rectilinear render space, returns the corresponding coordinate in distorted panel space for the blue channel + Red = 0, + Green, + Blue, + InverseRed, + InverseGreen, + InverseBlue, Count }; struct DistortionCoordinate_t { - float u, v; // 0..1 + float u, v; }; #pragma pack( pop ) -// figure out how to import from the VR API dll #if defined(_WIN32) #if !defined(OPENVR_BUILD_STATIC) @@ -2301,10 +2117,9 @@ struct DistortionCoordinate_t #define VR_CALLTYPE #endif -} // namespace vr -#endif // _INCLUDE_VRTYPES_H +} +#endif -// vrannotation.h #ifdef API_GEN # define VR_CLANG_ATTR(ATTR) __attribute__((annotate( ATTR ))) @@ -2324,7 +2139,6 @@ struct DistortionCoordinate_t #define VR_OUT_BUFFER_COUNT(COUNTER) VR_CLANG_ATTR( "out_buffer_count:" #COUNTER ";" ) #define VR_OUT_STRING_COUNT(COUNTER) VR_CLANG_ATTR( "out_string_count:" #COUNTER ";" ) -// vrtrackedcameratypes.h #ifndef _VRTRACKEDCAMERATYPES_H #define _VRTRACKEDCAMERATYPES_H @@ -2337,14 +2151,14 @@ namespace vr enum ECameraVideoStreamFormat { CVS_FORMAT_UNKNOWN = 0, - CVS_FORMAT_RAW10 = 1, // 10 bits per pixel - CVS_FORMAT_NV12 = 2, // 12 bits per pixel - CVS_FORMAT_RGB24 = 3, // 24 bits per pixel - CVS_FORMAT_NV12_2 = 4, // 12 bits per pixel, 2x height - CVS_FORMAT_YUYV16 = 5, // 16 bits per pixel - CVS_FORMAT_BAYER16BG = 6, // 16 bits per pixel, 10-bit BG-format Bayer, see https://docs.opencv.org/3.1.0/de/d25/imgproc_color_conversions.html - CVS_FORMAT_MJPEG = 7, // variable-sized MJPEG Open DML format, see https://www.loc.gov/preservation/digital/formats/fdd/fdd000063.shtml - CVS_FORMAT_RGBX32 = 8, // Full-sized pixels, 4BPP, LSB = RED + CVS_FORMAT_RAW10 = 1, + CVS_FORMAT_NV12 = 2, + CVS_FORMAT_RGB24 = 3, + CVS_FORMAT_NV12_2 = 4, + CVS_FORMAT_YUYV16 = 5, + CVS_FORMAT_BAYER16BG = 6, + CVS_FORMAT_MJPEG = 7, + CVS_FORMAT_RGBX32 = 8, CVS_MAX_FORMATS }; @@ -2380,7 +2194,7 @@ enum ECameraRoomViewStyle #ifdef _MSC_VER #define VR_CAMERA_DECL_ALIGN( x ) __declspec( align( x ) ) #else -#define VR_CAMERA_DECL_ALIGN( x ) // +#define VR_CAMERA_DECL_ALIGN( x ) #endif static const uint32_t k_unMaxCameras = 4; @@ -2393,16 +2207,16 @@ VR_CAMERA_DECL_ALIGN( 8 ) struct CameraVideoStreamFrame_t uint32_t m_nWidth; uint32_t m_nHeight; - uint32_t m_nImageDataSize; // Based on stream format, width, height + uint32_t m_nImageDataSize; - uint32_t m_nFrameSequence; // Starts from 0 when stream starts. + uint32_t m_nFrameSequence; - uint32_t m_nBufferIndex; // Identifies which buffer the image data is hosted - uint32_t m_nBufferCount; // Total number of configured buffers + uint32_t m_nBufferIndex; + uint32_t m_nBufferCount; uint32_t m_nExposureTime; - uint32_t m_nISPFrameTimeStamp; // Driver provided time stamp per driver centric time base + uint32_t m_nISPFrameTimeStamp; uint32_t m_nISPReferenceTimeStamp; uint32_t m_nSyncCounter; @@ -2411,17 +2225,17 @@ VR_CAMERA_DECL_ALIGN( 8 ) struct CameraVideoStreamFrame_t double m_flReferenceCamSyncTime; - double m_flFrameElapsedTime; // Starts from 0 when stream starts. In seconds. + double m_flFrameElapsedTime; double m_flFrameDeliveryRate; - double m_flFrameCaptureTime_DriverAbsolute; // In USB time, via AuxEvent - double m_flFrameCaptureTime_ServerRelative; // In System time within the server - uint64_t m_nFrameCaptureTicks_ServerAbsolute; // In system ticks within the server - double m_flFrameCaptureTime_ClientRelative; // At the client, relative to when the frame was exposed/captured. + double m_flFrameCaptureTime_DriverAbsolute; + double m_flFrameCaptureTime_ServerRelative; + uint64_t m_nFrameCaptureTicks_ServerAbsolute; + double m_flFrameCaptureTime_ClientRelative; double m_flSyncMarkerError; - TrackedDevicePose_t m_RawTrackedDevicePose; // Raw-and-uncalibrated pose, supplied by HMD layer when used as a tracked camera + TrackedDevicePose_t m_RawTrackedDevicePose; uint64_t m_pImageData; }; @@ -2430,9 +2244,8 @@ VR_CAMERA_DECL_ALIGN( 8 ) struct CameraVideoStreamFrame_t } -#endif // _VRTRACKEDCAMERATYPES_H +#endif -// ivrsettings.h #ifndef OPENVR_NO_STL #include <string> @@ -2447,11 +2260,10 @@ namespace vr VRSettingsError_WriteFailed = 2, VRSettingsError_ReadFailed = 3, VRSettingsError_JsonParseFailed = 4, - VRSettingsError_UnsetSettingHasNoDefault = 5, // This will be returned if the setting does not appear in the appropriate default file and has not been set + VRSettingsError_UnsetSettingHasNoDefault = 5, VRSettingsError_AccessDenied = 6, }; - // The maximum length of a settings key static const uint32_t k_unMaxSettingsKeyLength = 128; class IVRSettings @@ -2464,8 +2276,6 @@ namespace vr virtual void SetFloat( const char *pchSection, const char *pchSettingsKey, float flValue, EVRSettingsError *peError = nullptr ) = 0; virtual void SetString( const char *pchSection, const char *pchSettingsKey, const char *pchValue, EVRSettingsError *peError = nullptr ) = 0; - // Users of the system need to provide a proper default in default.vrsettings in the resources/settings/ directory - // of either the runtime or the driver_xxx directory. Otherwise the default will be false, 0, 0.0 or "" virtual bool GetBool( const char *pchSection, const char *pchSettingsKey, EVRSettingsError *peError = nullptr ) = 0; virtual int32_t GetInt32( const char *pchSection, const char *pchSettingsKey, EVRSettingsError *peError = nullptr ) = 0; virtual float GetFloat( const char *pchSection, const char *pchSettingsKey, EVRSettingsError *peError = nullptr ) = 0; @@ -2475,7 +2285,6 @@ namespace vr virtual void RemoveKeyInSection( const char *pchSection, const char *pchSettingsKey, EVRSettingsError *peError = nullptr ) = 0; }; - //----------------------------------------------------------------------------- static const char * const IVRSettings_Version = "IVRSettings_003"; class CVRSettingHelper @@ -2558,8 +2367,6 @@ namespace vr }; - //----------------------------------------------------------------------------- - // steamvr keys static const char * const k_pch_SteamVR_Section = "steamvr"; static const char * const k_pch_SteamVR_Contrast_Float = "contrast"; static const char * const k_pch_SteamVR_RequireHmd_String = "requireHmd"; @@ -2649,21 +2456,15 @@ namespace vr static const char * const k_pch_SteamVR_AllowFallbackMirrorWindowLinux_Bool = "allowFallbackMirrorWindowLinux"; static const char * const k_pch_SteamVR_DisableKeyboardPrivacy_Bool = "disableKeyboardPrivacy"; - //----------------------------------------------------------------------------- - // openxr keys static const char * const k_pch_OpenXR_Section = "openxr"; static const char * const k_pch_OpenXR_MetaUnityPluginCompatibility_Int32 = "metaUnityPluginCompatibility"; - //----------------------------------------------------------------------------- - // direct mode keys static const char * const k_pch_DirectMode_Section = "direct_mode"; static const char * const k_pch_DirectMode_Enable_Bool = "enable"; static const char * const k_pch_DirectMode_Count_Int32 = "count"; static const char * const k_pch_DirectMode_EdidVid_Int32 = "edidVid"; static const char * const k_pch_DirectMode_EdidPid_Int32 = "edidPid"; - //----------------------------------------------------------------------------- - // lighthouse keys static const char * const k_pch_Lighthouse_Section = "driver_lighthouse"; static const char * const k_pch_Lighthouse_DisableIMU_Bool = "disableimu"; static const char * const k_pch_Lighthouse_DisableIMUExceptHMD_Bool = "disableimuexcepthmd"; @@ -2677,8 +2478,6 @@ namespace vr static const char * const k_pch_Lighthouse_InactivityTimeoutForBaseStations_Int32 = "InactivityTimeoutForBaseStations"; static const char * const k_pch_Lighthouse_EnableImuFallback_Bool = "enableImuFallback"; - //----------------------------------------------------------------------------- - // null keys static const char * const k_pch_Null_Section = "driver_null"; static const char * const k_pch_Null_SerialNumber_String = "serialNumber"; static const char * const k_pch_Null_ModelNumber_String = "modelNumber"; @@ -2691,12 +2490,8 @@ namespace vr static const char * const k_pch_Null_SecondsFromVsyncToPhotons_Float = "secondsFromVsyncToPhotons"; static const char * const k_pch_Null_DisplayFrequency_Float = "displayFrequency"; - //----------------------------------------------------------------------------- - // Windows MR keys static const char * const k_pch_WindowsMR_Section = "driver_holographic"; - //----------------------------------------------------------------------------- - // user interface keys static const char * const k_pch_UserInterface_Section = "userinterface"; static const char * const k_pch_UserInterface_StatusAlwaysOnTop_Bool = "StatusAlwaysOnTop"; static const char * const k_pch_UserInterface_MinimizeToTray_Bool = "MinimizeToTray"; @@ -2706,13 +2501,9 @@ namespace vr static const char * const k_pch_UserInterface_CheckStatusInterval_Int = "vrmStatusCheckInterval"; static const char * const k_pch_UserInterface_CheckForSteam_Bool = "vrmCheckForSteam"; - //----------------------------------------------------------------------------- - // notification keys static const char * const k_pch_Notifications_Section = "notifications"; static const char * const k_pch_Notifications_DoNotDisturb_Bool = "DoNotDisturb"; - //----------------------------------------------------------------------------- - // keyboard keys static const char * const k_pch_Keyboard_Section = "keyboard"; static const char * const k_pch_Keyboard_TutorialCompletions = "TutorialCompletions"; static const char * const k_pch_Keyboard_ScaleX = "ScaleX"; @@ -2722,8 +2513,6 @@ namespace vr static const char * const k_pch_Keyboard_OffsetY = "OffsetY"; static const char * const k_pch_Keyboard_Smoothing = "Smoothing"; - //----------------------------------------------------------------------------- - // perf keys static const char * const k_pch_Perf_Section = "perfcheck"; static const char * const k_pch_Perf_PerfGraphInHMD_Bool = "perfGraphInHMD"; static const char * const k_pch_Perf_AllowTimingStore_Bool = "allowTimingStore"; @@ -2732,8 +2521,6 @@ namespace vr static const char * const k_pch_Perf_GPUProfiling_Bool = "GPUProfiling"; static const char * const k_pch_Perf_GpuBusMonitoring_Bool = "gpuBusMonitoring"; - //----------------------------------------------------------------------------- - // collision bounds keys static const char * const k_pch_CollisionBounds_Section = "collisionBounds"; static const char * const k_pch_CollisionBounds_Style_Int32 = "CollisionBoundsStyle"; static const char * const k_pch_CollisionBounds_GroundPerimeterOn_Bool = "CollisionBoundsGroundPerimeterOn"; @@ -2747,8 +2534,6 @@ namespace vr static const char * const k_pch_CollisionBounds_ColorGammaA_Int32 = "CollisionBoundsColorGammaA"; static const char * const k_pch_CollisionBounds_EnableDriverImport = "enableDriverBoundsImport"; - //----------------------------------------------------------------------------- - // camera keys static const char * const k_pch_Camera_Section = "camera"; static const char * const k_pch_Camera_EnableCamera_Bool = "enableCamera"; static const char * const k_pch_Camera_ShowOnController_Bool = "showOnController"; @@ -2761,8 +2546,6 @@ namespace vr static const char * const k_pch_Camera_BoundsStrength_Int32 = "cameraBoundsStrength"; static const char * const k_pch_Camera_RoomViewStyle_Int32 = "roomViewStyle"; - //----------------------------------------------------------------------------- - // audio keys static const char * const k_pch_audio_Section = "audio"; static const char * const k_pch_audio_SetOsDefaultPlaybackDevice_Bool = "setOsDefaultPlaybackDevice"; static const char * const k_pch_audio_EnablePlaybackDeviceOverride_Bool = "enablePlaybackDeviceOverride"; @@ -2783,8 +2566,6 @@ namespace vr static const char * const k_pch_audio_DualSpeakerAndJackOutput_Bool = "dualSpeakerAndJackOutput"; static const char * const k_pch_audio_MuteMicMonitor_Bool = "muteMicMonitor"; - //----------------------------------------------------------------------------- - // power management keys static const char * const k_pch_Power_Section = "power"; static const char * const k_pch_Power_PowerOffOnExit_Bool = "powerOffOnExit"; static const char * const k_pch_Power_TurnOffScreensTimeout_Float = "turnOffScreensTimeout"; @@ -2794,8 +2575,6 @@ namespace vr static const char * const k_pch_Power_PauseCompositorOnStandby_Bool = "pauseCompositorOnStandby"; static const char * const k_pch_Power_OverrideWindowsPowerScheme_Bool = "overrideWindowsPowerScheme"; - //----------------------------------------------------------------------------- - // dashboard keys static const char * const k_pch_Dashboard_Section = "dashboard"; static const char * const k_pch_Dashboard_EnableDashboard_Bool = "enableDashboard"; static const char * const k_pch_Dashboard_ArcadeMode_Bool = "arcadeMode"; @@ -2808,35 +2587,22 @@ namespace vr static const char * const k_pch_Dashboard_GrabHandleAcceleration = "grabHandleAcceleration"; static const char * const k_pch_Dashboard_OverlayBacksideColor_String = "overlayBacksideColor"; - //----------------------------------------------------------------------------- - // model skin keys static const char * const k_pch_modelskin_Section = "modelskins"; - //----------------------------------------------------------------------------- - // driver keys - These could be checked in any driver_<name> section static const char * const k_pch_Driver_Enable_Bool = "enable"; static const char * const k_pch_Driver_BlockedBySafemode_Bool = "blocked_by_safe_mode"; static const char * const k_pch_Driver_LoadPriority_Int32 = "loadPriority"; static const char * const k_pch_Driver_Hmd_AllowsClientToControlTextureIndex_Bool = "hmdAllowsClientToControlTextureIndex"; static const char * const k_pch_Driver_ForceSystemLayerUseAppPoses_Bool = "forceSystemLayerUseAppPoses"; - //----------------------------------------------------------------------------- - // web interface keys static const char* const k_pch_WebInterface_Section = "WebInterface"; - //----------------------------------------------------------------------------- - // vrwebhelper keys static const char* const k_pch_VRWebHelper_Section = "VRWebHelper"; static const char* const k_pch_VRWebHelper_DebuggerEnabled_Bool = "DebuggerEnabled"; static const char* const k_pch_VRWebHelper_DebuggerPort_Int32 = "DebuggerPort"; - //----------------------------------------------------------------------------- - // tracking overrides - keys are device paths, values are the device paths their - // tracking/pose information overrides static const char* const k_pch_TrackingOverride_Section = "TrackingOverrides"; - //----------------------------------------------------------------------------- - // per-app keys - the section name for these is the app key itself. Some of these are prefixed by the controller type static const char* const k_pch_App_BindingAutosaveURLSuffix_String = "AutosaveURL"; static const char* const k_pch_App_BindingLegacyAPISuffix_String = "_legacy"; static const char *const k_pch_App_BindingSteamVRInputAPISuffix_String = "_steamvrinput"; @@ -2847,45 +2613,30 @@ namespace vr static const char* const k_pch_App_DominantHand_Int32 = "DominantHand"; static const char* const k_pch_App_BlockOculusSDK_Bool = "blockOculusSDK"; - //----------------------------------------------------------------------------- - // configuration for trackers static const char * const k_pch_Trackers_Section = "trackers"; - //----------------------------------------------------------------------------- - // configuration for desktop UI windows static const char * const k_pch_DesktopUI_Section = "DesktopUI"; - //----------------------------------------------------------------------------- - // Last known keys for righting recovery static const char * const k_pch_LastKnown_Section = "LastKnown"; static const char* const k_pch_LastKnown_HMDManufacturer_String = "HMDManufacturer"; static const char *const k_pch_LastKnown_HMDModel_String = "HMDModel"; static const char* const k_pch_LastKnown_ActualHMDDriver_String = "ActualHMDDriver"; static const char* const k_pch_LastKnown_HMDSerialNumber_String = "HMDSerialNumber"; - static const char* const k_pch_LastKnown_HMDRemoteClientID_String = "RemoteClientID"; // uint64 in string + static const char* const k_pch_LastKnown_HMDRemoteClientID_String = "RemoteClientID"; - //----------------------------------------------------------------------------- - // Dismissed warnings static const char * const k_pch_DismissedWarnings_Section = "DismissedWarnings"; - //----------------------------------------------------------------------------- - // Input Settings static const char * const k_pch_Input_Section = "input"; static const char* const k_pch_Input_LeftThumbstickRotation_Float = "leftThumbstickRotation"; static const char* const k_pch_Input_RightThumbstickRotation_Float = "rightThumbstickRotation"; static const char* const k_pch_Input_ThumbstickDeadzone_Float = "thumbstickDeadzone"; - //----------------------------------------------------------------------------- - // Log of GPU performance static const char * const k_pch_GpuSpeed_Section = "GpuSpeed"; - //----------------------------------------------------------------------------- - // OpenXR Render Model Extension keys static const char *const k_pch_XRRenderModelCache_Section = "XRRenderModelUuidCache"; -} // namespace vr +} -// iservertrackeddevicedriver.h namespace vr { @@ -2898,60 +2649,24 @@ struct DriverPoseQuaternion_t struct DriverPose_t { - /* Time offset of this pose, in seconds from the actual time of the pose, - * relative to the time of the PoseUpdated() call made by the driver. - */ double poseTimeOffset; - /* Generally, the pose maintained by a driver - * is in an inertial coordinate system different - * from the world system of x+ right, y+ up, z+ back. - * Also, the driver is not usually tracking the "head" position, - * but instead an internal IMU or another reference point in the HMD. - * The following two transforms transform positions and orientations - * to app world space from driver world space, - * and to HMD head space from driver local body space. - * - * We maintain the driver pose state in its internal coordinate system, - * so we can do the pose prediction math without having to - * use angular acceleration. A driver's angular acceleration is generally not measured, - * and is instead calculated from successive samples of angular velocity. - * This leads to a noisy angular acceleration values, which are also - * lagged due to the filtering required to reduce noise to an acceptable level. - */ vr::HmdQuaternion_t qWorldFromDriverRotation; double vecWorldFromDriverTranslation[ 3 ]; vr::HmdQuaternion_t qDriverFromHeadRotation; double vecDriverFromHeadTranslation[ 3 ]; - /* State of driver pose, in meters and radians. */ - /* Position of the driver tracking reference in driver world space - * +[0] (x) is right - * +[1] (y) is up - * -[2] (z) is forward - */ double vecPosition[ 3 ]; - /* Velocity of the pose in meters/second */ double vecVelocity[ 3 ]; - /* Acceleration of the pose in meters/second */ double vecAcceleration[ 3 ]; - /* Orientation of the tracker, represented as a quaternion */ vr::HmdQuaternion_t qRotation; - /* Angular velocity of the pose in axis-angle - * representation. The direction is the angle of - * rotation and the magnitude is the angle around - * that axis in radians/second. */ double vecAngularVelocity[ 3 ]; - /* Angular acceleration of the pose in axis-angle - * representation. The direction is the angle of - * rotation and the magnitude is the angle around - * that axis in radians/second^2. */ double vecAngularAcceleration[ 3 ]; ETrackingResult result; @@ -2963,41 +2678,20 @@ struct DriverPose_t }; -// ---------------------------------------------------------------------------------------------- -// Purpose: Represents a single tracked device in a driver -// ---------------------------------------------------------------------------------------------- class ITrackedDeviceServerDriver { public: - // ------------------------------------ - // Management Methods - // ------------------------------------ - /** This is called before an HMD is returned to the application. It will always be - * called before any display or tracking methods. Memory and processor use by the - * ITrackedDeviceServerDriver object should be kept to a minimum until it is activated. - * The pose listener is guaranteed to be valid until Deactivate is called, but - * should not be used after that point. */ virtual EVRInitError Activate( uint32_t unObjectId ) = 0; - /** This is called when The VR system is switching from this Hmd being the active display - * to another Hmd being the active display. The driver should clean whatever memory - * and thread use it can when it is deactivated */ virtual void Deactivate() = 0; - /** Handles a request from the system to put this device into standby mode. What that means is defined per-device. */ virtual void EnterStandby() = 0; - /** Requests a component interface of the driver for device-specific functionality. The driver should return NULL - * if the requested interface or version is not supported. */ virtual void *GetComponent( const char *pchComponentNameAndVersion ) = 0; - /** A VR Client has made this debug request of the driver. The set of valid requests is entirely - * up to the driver and the client to figure out, as is the format of the response. Responses that - * exceed the length of the supplied buffer should be truncated and null terminated */ virtual void DebugRequest( const char *pchRequest, char *pchResponseBuffer, uint32_t unResponseBufferSize ) = 0; - /** This interface is unused, and will never be called. */ virtual DriverPose_t GetPose() = 0; }; @@ -3007,48 +2701,30 @@ static const char *ITrackedDeviceServerDriver_Version = "ITrackedDeviceServerDri } -// ivrdisplaycomponent.h namespace vr { - // ---------------------------------------------------------------------------------------------- - // Purpose: The display component on a single tracked device - // ---------------------------------------------------------------------------------------------- class IVRDisplayComponent { public: - // ------------------------------------ - // Display Methods - // ------------------------------------ - /** Size and position that the window needs to be on the VR display. */ virtual void GetWindowBounds( int32_t *pnX, int32_t *pnY, uint32_t *pnWidth, uint32_t *pnHeight ) = 0; - /** Returns true if the display is extending the desktop. */ virtual bool IsDisplayOnDesktop( ) = 0; - /** Returns true if the display is real and not a fictional display. */ virtual bool IsDisplayRealDisplay( ) = 0; - /** Suggested size for the intermediate render target that the distortion pulls from. */ virtual void GetRecommendedRenderTargetSize( uint32_t *pnWidth, uint32_t *pnHeight ) = 0; - /** Gets the viewport in the frame buffer to draw the output of the distortion into */ virtual void GetEyeOutputViewport( EVREye eEye, uint32_t *pnX, uint32_t *pnY, uint32_t *pnWidth, uint32_t *pnHeight ) = 0; - /** The components necessary to build your own projection matrix in case your - * application is doing something fancy like infinite Z */ virtual void GetProjectionRaw( EVREye eEye, float *pfLeft, float *pfRight, float *pfTop, float *pfBottom ) = 0; - /** Returns the result of the distortion function for the specified eye and input UVs. UVs go from 0,0 in - * the upper left of that eye's viewport and 1,1 in the lower right of that eye's viewport. */ virtual DistortionCoordinates_t ComputeDistortion( EVREye eEye, float fU, float fV ) = 0; - /** Computes the result of the inverse distortion function for the specified eye, channel, and input UV. - * Operation can fail, returns success/failure, on success result is stored in *pResult */ virtual bool ComputeInverseDistortion( HmdVector2_t *pResult, EVREye eEye, uint32_t unChannel, float fU, float fV ) = 0; }; @@ -3056,27 +2732,18 @@ namespace vr } -// ivrdriverdirectmodecomponent.h namespace vr { enum VRSwapTextureFlag { - // Specify that the shared texture resource was created with the SHARED_NTHANDLE option (Windows) VRSwapTextureFlag_Shared_NTHandle = 1 << 0, }; - // ---------------------------------------------------------------------------------------------- - // Purpose: This component is used for drivers that implement direct mode entirely on their own - // without allowing the VR Compositor to own the window/device. - // ---------------------------------------------------------------------------------------------- class IVRDriverDirectModeComponent { public: - // ----------------------------------- - // Direct mode methods - // ----------------------------------- struct SwapTextureSetDesc_t { @@ -3092,44 +2759,30 @@ namespace vr uint32_t unTextureFlags; }; - /** Called to allocate textures for applications to render into. One of these per eye will be passed back to SubmitLayer each frame. */ virtual void CreateSwapTextureSet( uint32_t unPid, const SwapTextureSetDesc_t *pSwapTextureSetDesc, SwapTextureSet_t *pOutSwapTextureSet ) {} - /** Used to textures created using CreateSwapTextureSet. Only one of the set's handles needs to be used to destroy the entire set. */ virtual void DestroySwapTextureSet( vr::SharedTextureHandle_t sharedTextureHandle ) {} - /** Used to purge all texture sets for a given process. */ virtual void DestroyAllSwapTextureSets( uint32_t unPid ) {} - /** After Present returns, calls this to get the next index to use for rendering. */ virtual void GetNextSwapTextureSetIndex( vr::SharedTextureHandle_t sharedTextureHandles[ 2 ], uint32_t( *pIndices )[ 2 ] ) {} - /** Call once per layer to draw for this frame. One shared texture handle per eye. Textures must be created - * using CreateSwapTextureSet and should be alternated per frame. Call Present once all layers have been submitted. */ struct SubmitLayerPerEye_t { - // Shared texture handles (depth not always provided). vr::SharedTextureHandle_t hTexture, hDepthTexture; - // Valid region of provided texture (and depth). vr::VRTextureBounds_t bounds; - // Projection matrix used to render the depth buffer. vr::HmdMatrix44_t mProjection; - // Hmd pose used to render this layer. vr::HmdMatrix34_t mHmdPose; - // Time in seconds from now that mHmdPose was predicted to. float flHmdPosePredictionTimeInSecondsFromNow; }; virtual void SubmitLayer( const SubmitLayerPerEye_t( &perEye )[ 2 ] ) {} - /** Submits queued layers for display. */ virtual void Present( vr::SharedTextureHandle_t syncTexture ) {} - /** Called after Present to allow driver to take more time until vsync after they've successfully acquired the sync texture in Present. - * Set Prop_Hmd_SupportsAppThrottling_Bool to enable throttling / prediction UI in per-app video settings. */ struct Throttling_t { uint32_t nFramesToThrottle; @@ -3137,11 +2790,8 @@ namespace vr }; virtual void PostPresent( const Throttling_t *pThrottling ) {} - /** Called to get additional frame timing stats from driver. Check m_nSize for versioning (new members will be added to end only). */ virtual void GetFrameTiming( DriverDirectMode_FrameTiming *pFrameTiming ) { - /** VRCompositor_ReprojectionMotion_XXX flags get passed in, and since these overlap with VRCompositor_ThrottleMask, they need - * to be cleared out if this function isn't implemented; otherwise, those settings will get interpreted as throttling. */ pFrameTiming->m_nReprojectionFlags = 0; } }; @@ -3150,27 +2800,18 @@ namespace vr } -// ivrcameracomponent.h namespace vr { - //----------------------------------------------------------------------------- - //----------------------------------------------------------------------------- class ICameraVideoSinkCallback { public: virtual void OnCameraVideoSinkCallback() = 0; }; - // ---------------------------------------------------------------------------------------------- - // Purpose: The camera on a single tracked device - // ---------------------------------------------------------------------------------------------- class IVRCameraComponent { public: - // ------------------------------------ - // Camera Methods - // ------------------------------------ virtual bool GetCameraFrameDimensions( vr::ECameraVideoStreamFormat nVideoStreamFormat, uint32_t *pWidth, uint32_t *pHeight ) = 0; virtual bool GetCameraFrameBufferingRequirements( int *pDefaultFrameQueueSize, uint32_t *pFrameBufferDataSize ) = 0; virtual bool SetCameraFrameBuffering( int nFrameBufferCount, void **ppFrameBuffers, uint32_t nFrameBufferDataSize ) = 0; @@ -3197,7 +2838,6 @@ namespace vr static const char *IVRCameraComponent_Version = "IVRCameraComponent_003"; } -// itrackeddevicedriverprovider.h namespace vr { @@ -3206,58 +2846,32 @@ class ITrackedDeviceServerDriver; struct TrackedDeviceDriverInfo_t; struct DriverPose_t; -/** This interface is provided by vrserver to allow the driver to notify -* the system when something changes about a device. These changes must -* not change the serial number or class of the device because those values -* are permanently associated with the device's index. */ class IVRDriverContext { public: - /** Returns the requested interface. If the interface was not available it will return NULL and fill - * out the error. */ virtual void *GetGenericInterface( const char *pchInterfaceVersion, EVRInitError *peError = nullptr ) = 0; - /** Returns the property container handle for this driver */ virtual DriverHandle_t GetDriverHandle() = 0; }; -/** This interface must be implemented in each driver. It will be loaded in vrserver.exe */ class IServerTrackedDeviceProvider { public: - /** initializes the driver. This will be called before any other methods are called. - * If Init returns anything other than VRInitError_None the driver DLL will be unloaded. - * - * pDriverHost will never be NULL, and will always be a pointer to a IServerDriverHost interface - * - * pchUserDriverConfigDir - The absolute path of the directory where the driver should store user - * config files. - * pchDriverInstallDir - The absolute path of the root directory for the driver. - */ virtual EVRInitError Init( IVRDriverContext *pDriverContext ) = 0; - /** cleans up the driver right before it is unloaded */ virtual void Cleanup() = 0; - /** Returns the version of the ITrackedDeviceServerDriver interface used by this driver */ virtual const char * const *GetInterfaceVersions() = 0; - /** Allows the driver do to some work in the main loop of the server. */ virtual void RunFrame() = 0; - // ------------ Power State Functions ----------------------- // - /** Returns true if the driver wants to block Standby mode. */ virtual bool ShouldBlockStandbyMode() = 0; - /** Called when the system is entering Standby mode. The driver should switch itself into whatever sort of low-power - * state it has. */ virtual void EnterStandby() = 0; - /** Called when the system is leaving Standby mode. The driver should switch itself back to - full operation. */ virtual void LeaveStandby() = 0; }; @@ -3268,14 +2882,11 @@ static const char *IServerTrackedDeviceProvider_Version = "IServerTrackedDeviceP -/** This interface must be implemented in each driver. It will be loaded in vrclient.dll */ class IVRWatchdogProvider { public: - /** initializes the driver in watchdog mode. */ virtual EVRInitError Init( IVRDriverContext *pDriverContext ) = 0; - /** cleans up the driver right before it is unloaded */ virtual void Cleanup() = 0; }; @@ -3284,21 +2895,15 @@ static const char *IVRWatchdogProvider_Version = "IVRWatchdogProvider_001"; -/** This is an optional interface drivers may implement. It will be loaded in vrcompositor.exe */ class IVRCompositorPluginProvider { public: - /** initializes the driver when used to load compositor plugins */ virtual EVRInitError Init( IVRDriverContext *pDriverContext ) = 0; - /** cleans up the driver right before it is unloaded */ virtual void Cleanup() = 0; - /** Returns the versions of interfaces used by this driver */ virtual const char * const *GetInterfaceVersions() = 0; - /** Requests a component interface of the driver for specific functionality. The driver should return NULL - * if the requested interface or version is not supported. */ virtual void *GetComponent( const char *pchComponentNameAndVersion ) = 0; }; @@ -3306,7 +2911,6 @@ static const char *IVRCompositorPluginProvider_Version = "IVRCompositorPluginPro } -// ivrproperties.h #include <string> #include <vector> @@ -3314,9 +2918,6 @@ static const char *IVRCompositorPluginProvider_Version = "IVRCompositorPluginPro namespace vr { - /** This container is automatically created before a display redirect device is activated. - * Any properties in this container will be returned when that property is read from the HMD's - * property container. */ static const PropertyContainerHandle_t k_ulDisplayRedirectContainer = 0x600000003; enum EPropertyWriteType @@ -3352,17 +2953,12 @@ class IVRProperties { public: - /** Reads a set of properties atomically. See the PropertyReadBatch_t struct for more information. */ virtual ETrackedPropertyError ReadPropertyBatch( PropertyContainerHandle_t ulContainerHandle, PropertyRead_t *pBatch, uint32_t unBatchEntryCount ) = 0; - /** Writes a set of properties atomically. See the PropertyWriteBatch_t struct for more information. */ virtual ETrackedPropertyError WritePropertyBatch( PropertyContainerHandle_t ulContainerHandle, PropertyWrite_t *pBatch, uint32_t unBatchEntryCount ) = 0; - /** returns a string that corresponds with the specified property error. The string will be the name - * of the error enum value for all valid error codes */ virtual const char *GetPropErrorNameFromEnum( ETrackedPropertyError error ) = 0; - /** Returns a container handle given a tracked device index */ virtual PropertyContainerHandle_t TrackedDeviceToPropertyContainer( TrackedDeviceIndex_t nDevice ) = 0; }; @@ -3373,12 +2969,9 @@ class CVRPropertyHelpers public: CVRPropertyHelpers( IVRProperties * pProperties ) : m_pProperties( pProperties ) {} - /** Gets the typed property according to the caller's expected return type. */ template <typename T> T GetTypedProperty( vr::PropertyContainerHandle_t ulContainer, vr::ETrackedDeviceProperty prop, vr::ETrackedPropertyError *peError ); - /** Returns a scaler property. If the device index is not valid or the property value type does not match, - * this function will return false. */ bool GetBoolProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError *pError = 0L ); float GetFloatProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError *pError = 0L ); int32_t GetInt32Property( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError *pError = 0L ); @@ -3388,25 +2981,15 @@ public: HmdVector4_t GetVec4Property( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError *pError = 0L ); double GetDoubleProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError *pError = 0L ); - /** Returns a single typed property. If the device index is not valid or the property is not a string type this function will - * return 0. Otherwise it returns the length of the number of bytes necessary to hold this string including the trailing - * null. Strings will always fit in buffers of k_unMaxPropertyStringSize characters. */ uint32_t GetProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, VR_OUT_STRING() void *pvBuffer, uint32_t unBufferSize, PropertyTypeTag_t *punTag, ETrackedPropertyError *pError = 0L ); - /** Returns a string property. If the device index is not valid or the property is not a string type this function will - * return 0. Otherwise it returns the length of the number of bytes necessary to hold this string including the trailing - * null. Strings will always fit in buffers of k_unMaxPropertyStringSize characters. */ uint32_t GetStringProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, VR_OUT_STRING() char *pchValue, uint32_t unBufferSize, ETrackedPropertyError *pError = 0L ); - /** Returns a string property as a std::string. If the device index is not valid or the property is not a string type this function will - * return an empty string. */ std::string GetStringProperty( vr::PropertyContainerHandle_t ulContainer, vr::ETrackedDeviceProperty prop, vr::ETrackedPropertyError *peError = nullptr ); - /** Reads a std::vector of data from a property. */ template< typename T> ETrackedPropertyError GetPropertyVector( PropertyContainerHandle_t ulContainer, ETrackedDeviceProperty prop, PropertyTypeTag_t unExpectedTag, std::vector<T> *pvecResults ); - /** Sets a scaler property. The new value will be returned on any subsequent call to get this property in any process. */ ETrackedPropertyError SetBoolProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, bool bNewValue ); ETrackedPropertyError SetFloatProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, float fNewValue ); ETrackedPropertyError SetInt32Property( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, int32_t nNewValue ); @@ -3416,26 +2999,19 @@ public: ETrackedPropertyError SetVec4Property( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, const HmdVector4_t & vNewValue ); ETrackedPropertyError SetDoubleProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, double vNewValue ); - /** Sets a string property. The new value will be returned on any subsequent call to get this property in any process. */ ETrackedPropertyError SetStringProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, const char *pchNewValue ); - /** Sets a single typed property. The new value will be returned on any subsequent call to get this property in any process. */ ETrackedPropertyError SetProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, void *pvNewValue, uint32_t unNewValueSize, PropertyTypeTag_t unTag ); - /** Sets the error return value for a property. This value will be returned on all subsequent requests to get the property */ ETrackedPropertyError SetPropertyError( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError eError ); - /** Clears any value or error set for the property. */ ETrackedPropertyError EraseProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop ); - /* Turns a device index into a property container handle. */ PropertyContainerHandle_t TrackedDeviceToPropertyContainer( TrackedDeviceIndex_t nDevice ) { return m_pProperties->TrackedDeviceToPropertyContainer( nDevice ); } - /** Sets a std::vector of typed data to a property. */ template< typename T> ETrackedPropertyError SetPropertyVector( PropertyContainerHandle_t ulContainer, ETrackedDeviceProperty prop, PropertyTypeTag_t unExpectedTag, std::vector<T> *vecProperties ); - /** Returns true if the specified property is set on the specified container */ bool IsPropertySet( PropertyContainerHandle_t ulContainer, ETrackedDeviceProperty prop, ETrackedPropertyError *peError = nullptr ); private: template<typename T> @@ -3445,8 +3021,6 @@ private: }; -/** Returns a string property as a std::string. If the device index is not valid or the property is not a string type this function will -* return an empty string. */ template <> inline std::string CVRPropertyHelpers::GetTypedProperty( vr::PropertyContainerHandle_t ulContainer, vr::ETrackedDeviceProperty prop, vr::ETrackedPropertyError *peError ) { @@ -3533,7 +3107,6 @@ inline uint32_t CVRPropertyHelpers::GetProperty( PropertyContainerHandle_t ulCon } -/** Sets a single typed property. The new value will be returned on any subsequent call to get this property in any process. */ inline ETrackedPropertyError CVRPropertyHelpers::SetProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, void *pvNewValue, uint32_t unNewValueSize, PropertyTypeTag_t unTag ) { PropertyWrite_t batch; @@ -3549,9 +3122,6 @@ inline ETrackedPropertyError CVRPropertyHelpers::SetProperty( PropertyContainerH } -/** Returns a string property. If the device index is not valid or the property is not a string type this function will -* return 0. Otherwise it returns the length of the number of bytes necessary to hold this string including the trailing -* null. Strings will always fit in buffers of k_unMaxPropertyStringSize characters. */ inline uint32_t CVRPropertyHelpers::GetStringProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, VR_OUT_STRING() char *pchValue, uint32_t unBufferSize, ETrackedPropertyError *pError ) { PropertyTypeTag_t unTag; @@ -3579,8 +3149,6 @@ inline uint32_t CVRPropertyHelpers::GetStringProperty( PropertyContainerHandle_t } -/** Returns a string property as a std::string. If the device index is not valid or the property is not a string type this function will -* return an empty string. */ inline std::string CVRPropertyHelpers::GetStringProperty( vr::PropertyContainerHandle_t ulContainer, vr::ETrackedDeviceProperty prop, vr::ETrackedPropertyError *peError ) { char buf[1024]; @@ -3610,13 +3178,11 @@ inline std::string CVRPropertyHelpers::GetStringProperty( vr::PropertyContainerH } -/** Sets a string property. The new value will be returned on any subsequent call to get this property in any process. */ inline ETrackedPropertyError CVRPropertyHelpers::SetStringProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, const char *pchNewValue ) { if ( !pchNewValue ) return TrackedProp_InvalidOperation; - // this is strlen without the dependency on string.h const char *pchCurr = pchNewValue; while ( *pchCurr ) { @@ -3735,7 +3301,6 @@ inline ETrackedPropertyError CVRPropertyHelpers::SetVec4Property( PropertyContai return SetProperty( ulContainerHandle, prop, ( void * ) &vNewValue, sizeof( HmdVector4_t ), k_unHmdVector4PropertyTag ); } -/** Sets the error return value for a property. This value will be returned on all subsequent requests to get the property */ inline ETrackedPropertyError CVRPropertyHelpers::SetPropertyError( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop, ETrackedPropertyError eError ) { PropertyWrite_t batch; @@ -3748,7 +3313,6 @@ inline ETrackedPropertyError CVRPropertyHelpers::SetPropertyError( PropertyConta return batch.eError; } -/** Clears any value or error set for the property. */ inline ETrackedPropertyError CVRPropertyHelpers::EraseProperty( PropertyContainerHandle_t ulContainerHandle, ETrackedDeviceProperty prop ) { PropertyWrite_t batch; @@ -3821,7 +3385,6 @@ inline bool CVRPropertyHelpers::IsPropertySet( PropertyContainerHandle_t ulConta -// ivrdriverinput.h namespace vr { @@ -3834,53 +3397,41 @@ namespace vr enum EVRScalarUnits { - VRScalarUnits_NormalizedOneSided = 0, // Value ranges from 0 to 1 - VRScalarUnits_NormalizedTwoSided = 1, // Value ranges from -1 to 1 + VRScalarUnits_NormalizedOneSided = 0, + VRScalarUnits_NormalizedTwoSided = 1, }; class IVRDriverInput { public: - /** Creates a boolean input component for the device */ virtual EVRInputError CreateBooleanComponent( PropertyContainerHandle_t ulContainer, const char *pchName, VRInputComponentHandle_t *pHandle ) = 0; - /** Updates a boolean component */ virtual EVRInputError UpdateBooleanComponent( VRInputComponentHandle_t ulComponent, bool bNewValue, double fTimeOffset ) = 0; - /** Creates a scalar input component for the device */ virtual EVRInputError CreateScalarComponent( PropertyContainerHandle_t ulContainer, const char *pchName, VRInputComponentHandle_t *pHandle, EVRScalarType eType, EVRScalarUnits eUnits ) = 0; - /** Updates a boolean component */ virtual EVRInputError UpdateScalarComponent( VRInputComponentHandle_t ulComponent, float fNewValue, double fTimeOffset ) = 0; - /** Creates a haptic component for the device */ virtual EVRInputError CreateHapticComponent( PropertyContainerHandle_t ulContainer, const char *pchName, VRInputComponentHandle_t *pHandle ) = 0; - /** Creates a skeleton component. */ virtual EVRInputError CreateSkeletonComponent( PropertyContainerHandle_t ulContainer, const char *pchName, const char *pchSkeletonPath, const char *pchBasePosePath, EVRSkeletalTrackingLevel eSkeletalTrackingLevel, const VRBoneTransform_t *pGripLimitTransforms, uint32_t unGripLimitTransformCount, VRInputComponentHandle_t *pHandle ) = 0; - /** Updates a skeleton component. */ virtual EVRInputError UpdateSkeletonComponent( VRInputComponentHandle_t ulComponent, EVRSkeletalMotionRange eMotionRange, const VRBoneTransform_t *pTransforms, uint32_t unTransformCount ) = 0; - /** Creates a pose component */ virtual EVRInputError CreatePoseComponent( PropertyContainerHandle_t ulContainer, const char *pchName, VRInputComponentHandle_t *pHandle ) = 0; - /** Updates a pose component. */ virtual EVRInputError UpdatePoseComponent( VRInputComponentHandle_t ulComponent, const HmdMatrix34_t *pMatPoseOffset, double fTimeOffset ) = 0; - /** Creates an eye tracking component **/ virtual EVRInputError CreateEyeTrackingComponent( PropertyContainerHandle_t ulContainer, const char *pchName, VRInputComponentHandle_t *pHandle ) = 0; - /** Updates an eye tracking component. */ virtual EVRInputError UpdateEyeTrackingComponent( VRInputComponentHandle_t ulComponent, const VREyeTrackingData_t *pEyeTrackingData, double fTimeOffset ) = 0; }; static const char * const IVRDriverInput_Version = "IVRDriverInput_004"; -} // namespace vr +} -// ivrdriverlog.h namespace vr { @@ -3888,7 +3439,6 @@ namespace vr class IVRDriverLog { public: - /** Writes a log message to the log file prefixed with the driver name */ virtual void Log( const char *pchLogMessage ) = 0; }; @@ -3897,7 +3447,6 @@ static const char *IVRDriverLog_Version = "IVRDriverLog_001"; } -// ivrserverdriverhost.h namespace vr { @@ -3906,58 +3455,31 @@ class ITrackedDeviceServerDriver; struct TrackedDeviceDriverInfo_t; struct DriverPose_t; -/** This interface is provided by vrserver to allow the driver to notify -* the system when something changes about a device. These changes must -* not change the serial number or class of the device because those values -* are permanently associated with the device's index. */ class IVRServerDriverHost { public: - /** Notifies the server that a tracked device has been added. If this function returns true - * the server will call Activate on the device. If it returns false some kind of error - * has occurred and the device will not be activated. */ virtual bool TrackedDeviceAdded( const char *pchDeviceSerialNumber, ETrackedDeviceClass eDeviceClass, ITrackedDeviceServerDriver *pDriver ) = 0; - /** Notifies the server that a tracked device's pose has been updated */ virtual void TrackedDevicePoseUpdated( uint32_t unWhichDevice, const DriverPose_t & newPose, uint32_t unPoseStructSize ) = 0; - /** Notifies the server that vsync has occurred on the the display attached to the device. This is - * only permitted on devices of the HMD class. */ virtual void VsyncEvent( double vsyncTimeOffsetSeconds ) = 0; - /** Sends a vendor specific event (VREvent_VendorSpecific_Reserved_Start..VREvent_VendorSpecific_Reserved_End */ virtual void VendorSpecificEvent( uint32_t unWhichDevice, vr::EVREventType eventType, const VREvent_Data_t & eventData, double eventTimeOffset ) = 0; - /** Returns true if SteamVR is exiting */ virtual bool IsExiting() = 0; - /** Returns true and fills the event with the next event on the queue if there is one. If there are no events - * this method returns false. uncbVREvent should be the size in bytes of the VREvent_t struct */ virtual bool PollNextEvent( VREvent_t *pEvent, uint32_t uncbVREvent ) = 0; - /** Provides access to device poses for drivers. Poses are in their "raw" tracking space which is uniquely - * defined by each driver providing poses for its devices. It is up to clients of this function to correlate - * poses across different drivers. Poses are indexed by their device id, and their associated driver and - * other properties can be looked up via IVRProperties. */ virtual void GetRawTrackedDevicePoses( float fPredictedSecondsFromNow, TrackedDevicePose_t *pTrackedDevicePoseArray, uint32_t unTrackedDevicePoseArrayCount ) = 0; - /** Requests that SteamVR be restarted. The provided reason will be displayed to the user and should be in the current locale. */ virtual void RequestRestart( const char *pchLocalizedReason, const char *pchExecutableToStart, const char *pchArguments, const char *pchWorkingDirectory ) = 0; - /** Interface for copying a range of timing data. Frames are returned in ascending order (oldest to newest) with the last being the most recent frame. - * Only the first entry's m_nSize needs to be set, as the rest will be inferred from that. Returns total number of entries filled out. */ virtual uint32_t GetFrameTimings( Compositor_FrameTiming *pTiming, uint32_t nFrames ) = 0; - /** Notifies the server that a tracked device's display component transforms have been updated. - * only permitted on devices of the HMD class. */ virtual void SetDisplayEyeToHead( uint32_t unWhichDevice, const HmdMatrix34_t & eyeToHeadLeft, const HmdMatrix34_t & eyeToHeadRight ) = 0; - /** Notifies the server that a tracked device's display projection has changed. - * only permitted on devices of the HMD class. */ virtual void SetDisplayProjectionRaw( uint32_t unWhichDevice, const HmdRect2_t & eyeLeft, const HmdRect2_t & eyeRight ) = 0; - /** Notifies the server that a tracked device's recommended render target resolution has changed. - * only permitted on devices of the HMD class. */ virtual void SetRecommendedRenderTargetSize( uint32_t unWhichDevice, uint32_t nWidth, uint32_t nHeight ) = 0; }; @@ -3965,7 +3487,6 @@ static const char *IVRServerDriverHost_Version = "IVRServerDriverHost_006"; } -// ivrcompositordriverhost.h namespace vr { @@ -3973,8 +3494,6 @@ namespace vr class IVRCompositorDriverHost { public: - /** Returns true and fills the event with the next event on the queue if there is one. If there are no events - * this method returns false. uncbVREvent should be the size in bytes of the VREvent_t struct */ virtual bool PollNextEvent( VREvent_t *pEvent, uint32_t uncbVREvent ) = 0; }; @@ -3982,7 +3501,6 @@ static const char *IVRCompositorDriverHost_Version = "IVRCompositorDriverHost_00 } -// ivrhiddenarea.h namespace vr { @@ -3992,10 +3510,8 @@ class CVRHiddenAreaHelpers public: CVRHiddenAreaHelpers( IVRProperties *pProperties ) : m_pProperties( pProperties ) {} - /** Stores a hidden area mesh in a property */ ETrackedPropertyError SetHiddenArea( EVREye eEye, EHiddenAreaMeshType type, HmdVector2_t *pVerts, uint32_t unVertCount ); - /** retrieves a hidden area mesh from a property. Returns the vert count read out of the property. */ uint32_t GetHiddenArea( EVREye eEye, EHiddenAreaMeshType type, HmdVector2_t *pVerts, uint32_t unVertCount, ETrackedPropertyError *peError ); private: @@ -4039,17 +3555,13 @@ inline uint32_t CVRHiddenAreaHelpers::GetHiddenArea( EVREye eEye, EHiddenAreaMes } -// ivrwatchdoghost.h namespace vr { -/** This interface is provided by vrclient to allow the driver to make everything wake up */ class IVRWatchdogHost { public: - /** Client drivers in watchdog mode should call this when they have received a signal from hardware that should - * cause SteamVR to start */ virtual void WatchdogWakeUp( vr::ETrackedDeviceClass eDeviceClass ) = 0; }; @@ -4059,7 +3571,6 @@ static const char *IVRWatchdogHost_Version = "IVRWatchdogHost_002"; -// ivrvirtualdisplay.h namespace vr { @@ -4071,20 +3582,14 @@ namespace vr double flVSyncTimeInSeconds; }; - // ---------------------------------------------------------------------------------------------- - // Purpose: This component is used for drivers that implement a virtual display (e.g. wireless). - // ---------------------------------------------------------------------------------------------- class IVRVirtualDisplay { public: - /** Submits final backbuffer for display. */ virtual void Present( const PresentInfo_t *pPresentInfo, uint32_t unPresentInfoSize ) = 0; - /** Block until the last presented buffer start scanning out. */ virtual void WaitForPresent() = 0; - /** Provides timing data for synchronizing with display. */ virtual bool GetTimeSinceLastVsync( float *pfSecondsSinceLastVsync, uint64_t *pulFrameCounter ) = 0; }; @@ -4092,7 +3597,6 @@ namespace vr } -// ivrresources.h namespace vr { @@ -4101,17 +3605,9 @@ class IVRResources { public: - // ------------------------------------ - // Shared Resource Methods - // ------------------------------------ - /** Loads the specified resource into the provided buffer if large enough. - * Returns the size in bytes of the buffer required to hold the specified resource. */ virtual uint32_t LoadSharedResource( const char *pchResourceName, char *pchBuffer, uint32_t unBufferLen ) = 0; - /** Provides the full path to the specified resource. Resource names can include named directories for - * drivers and other things, and this resolves all of those and returns the actual physical path. - * pchResourceTypeDirectory is the subdirectory of resources to look in. */ virtual uint32_t GetResourceFullPath( const char *pchResourceName, const char *pchResourceTypeDirectory, VR_OUT_STRING() char *pchPathBuffer, uint32_t unBufferLen ) = 0; }; @@ -4120,7 +3616,6 @@ static const char * const IVRResources_Version = "IVRResources_001"; } -// ivriobuffer.h namespace vr { @@ -4146,35 +3641,25 @@ static const uint64_t k_ulInvalidIOBufferHandle = 0; IOBufferMode_Create = 0x0200, }; - // ---------------------------------------------------------------------------------------------- - // Purpose: - // ---------------------------------------------------------------------------------------------- class IVRIOBuffer { public: - /** opens an existing or creates a new IOBuffer of unSize bytes */ virtual vr::EIOBufferError Open( const char *pchPath, vr::EIOBufferMode mode, uint32_t unElementSize, uint32_t unElements, vr::IOBufferHandle_t *pulBuffer ) = 0; - /** closes a previously opened or created buffer */ virtual vr::EIOBufferError Close( vr::IOBufferHandle_t ulBuffer ) = 0; - /** reads up to unBytes from buffer into *pDst, returning number of bytes read in *punRead */ virtual vr::EIOBufferError Read( vr::IOBufferHandle_t ulBuffer, void *pDst, uint32_t unBytes, uint32_t *punRead ) = 0; - /** writes unBytes of data from *pSrc into a buffer. */ virtual vr::EIOBufferError Write( vr::IOBufferHandle_t ulBuffer, void *pSrc, uint32_t unBytes ) = 0; - /** retrieves the property container of an buffer. */ virtual vr::PropertyContainerHandle_t PropertyContainer( vr::IOBufferHandle_t ulBuffer ) = 0; - /** inexpensively checks for readers to allow writers to fast-fail potentially expensive copies and writes. */ virtual bool HasReaders( vr::IOBufferHandle_t ulBuffer ) = 0; }; static const char * const IVRIOBuffer_Version = "IVRIOBuffer_002"; } -// ivrdrivermanager.h namespace vr { @@ -4184,7 +3669,6 @@ class IVRDriverManager public: virtual uint32_t GetDriverCount() const = 0; - /** Returns the length of the number of bytes necessary to hold this string including the trailing null. */ virtual uint32_t GetDriverName( vr::DriverId_t nDriver, VR_OUT_STRING() char *pchValue, uint32_t unBufferSize ) = 0; virtual DriverHandle_t GetDriverHandle( const char *pchDriverName ) = 0; @@ -4194,173 +3678,86 @@ public: static const char * const IVRDriverManager_Version = "IVRDriverManager_001"; -} // namespace vr +} -// ivrdriverspatialanchors.h namespace vr { struct SpatialAnchorDriverPose_t { - /** This position is in the same "world" space (+Y up) as provided by DriverPose_t. */ vr::HmdQuaternion_t qWorldRotation; vr::HmdVector3d_t vWorldTranslation; - /** The pose will automatically start returning VRSpatialAnchorError_NotAvailableInThisUniverse - * if this is nonzero and does not match the current universe ID. */ uint64_t ulRequiredUniverseId; - /** When this time expires, SteamVR will start generating - * VREvent_SpatialAnchors_RequestPoseUpdate when the pose is read by an application - * to let the driver know it is still worth updating. - * You can use this facility in several ways: - * 1. Set to -1 to never receive an update request for this pose. The driver - * may still update poses at any time. - * 2. Set to 0 to always receive an update request *after* each time the pose - * is read. The rate of requests could be very high if the application gets - * the pose at framerate. - * 3. If the driver knows there is no reason to update the pose for some amount of - * time, it can set that time here and receive the update request reminder later. - * 4. If the driver plans to automatically update this pose for some amount of time - * (as it gets better information about the virtual location of this anchor) it can - * set that duration here to indicate that no "update requested" reminders are needed. - * When that automatic update period expires, any future interest in the pose will - * be indicated by a pose update request. - * The driver may always update the pose, including during the valid duration. */ - double fValidDuration; // seconds + + + + + + + + + + + + + + + + + + double fValidDuration; }; class IVRDriverSpatialAnchors { public: - /* NOTE: You must declare support for spatial anchors in your driver manifest. Add - * "spatialAnchorsSupport": true to your manifest. Without that setting, SteamVR - * will short-circuit anchor requests from applications and provide a generic descriptor - * that does not have any of the advantages of true spatial anchors. */ - - /* The driver should monitor for events VREvent_SpatialAnchors_RequestPoseUpdate (for new - * descriptors from applications that need UpdateSpatialAnchorPose()) and for - * VREvent_SpatialAnchors_RequestDescriptorUpdate (for new poses that need UpdateSpatialAnchorDescriptor()). - * For automatic pose updates over time, the driver should keep track of the handles it - * has seen and provide updates when conditions change. If the driver uses fValidDuration, - * it may wait for VREvent_SpatialAnchors_RequestPoseUpdate instead of keeping track itself. */ - - /** Update a pose for a spatial anchor. Should be called when an event notifies the driver that a - * new descriptor has been registered by an application. May be called for any anchor whenever the - * driver has better information about the best virtual coordinate to represent the anchor. Should - * be called on all active handles whenever driver state changes in a way that changes how physical - * world locations map to virtual coordinates (e.g. anything that would cause a universe ID change). - * This fires an event when it is called for the first time (to alert whoever submitted the descriptor). */ virtual EVRSpatialAnchorError UpdateSpatialAnchorPose( SpatialAnchorHandle_t unHandle, const SpatialAnchorDriverPose_t *pPose ) = 0; - /** Invalidate any pose associated with the handle and cause future calls to GetSpatialAnchorPose (on - * both the client and driver side) to return the specified error. eError must be one of - * VRSpatialAnchorError_NotYetAvailable, VRSpatialAnchorError_NotAvailableInThisUniverse, or - * VRSpatialAnchorError_PermanentlyUnavailable */ virtual EVRSpatialAnchorError SetSpatialAnchorPoseError( SpatialAnchorHandle_t unHandle, EVRSpatialAnchorError eError, double fValidDuration ) = 0; - /** Update the descriptor for a spatial anchor. Should be called when an event notifies the driver - * that a new pose has been registered by an application. May be called for any anchor whenever the - * driver has better or additional information it wants to include in the anchor descriptor. Note, - * however, that the application may never fetch the updated anchor descriptor and may request the - * original descriptor in a future session having ignored the update. - * The supplied descriptor should be only the driver's opaque internal data, not the decorated form that - * is used by clients (wrapped by runtime metadata). The descriptor must not contain non-ASCII characters or - * the two special characters ~ or " - * This fires an event every time it is called. */ virtual EVRSpatialAnchorError UpdateSpatialAnchorDescriptor( SpatialAnchorHandle_t unHandle, const char *pchDescriptor ) = 0; - /** Get the pose for a given handle. */ virtual EVRSpatialAnchorError GetSpatialAnchorPose( SpatialAnchorHandle_t unHandle, SpatialAnchorDriverPose_t *pDriverPoseOut ) = 0; - /** Get the descriptor for a given handle. This will be VRSpatialAnchorError_NotYetAvailable for handles - * where the driver has not yet built a descriptor. It will be the application-supplied descriptor for previously - * saved anchors that the application is requesting poses for. If the driver has called UpdateSpatialAnchorDescriptor() - * already in this session, it will be the descriptor provided by the driver. - * If bDecorated, returns the descriptor wrapped with runtime metadata suitable for a client to save. Else returns only - * the driver's opaque internal data. - */ virtual EVRSpatialAnchorError GetSpatialAnchorDescriptor( SpatialAnchorHandle_t unHandle, VR_OUT_STRING() char *pchDescriptorOut, uint32_t *punDescriptorBufferLenInOut, bool bDecorated ) = 0; }; static const char * const IVRDriverSpatialAnchors_Version = "IVRDriverSpatialAnchors_001"; -} // namespace vr +} -// ivripcresourcemanagerclient.h namespace vr { -// ----------------------------------------------------------------------------- -// Purpose: Interact with the IPCResourceManager -// ----------------------------------------------------------------------------- class IVRIPCResourceManagerClient { public: - /** Create a new tracked Vulkan Image - * - * nImageFormat: in VkFormat - */ virtual bool NewSharedVulkanImage( uint32_t nImageFormat, uint32_t nWidth, uint32_t nHeight, bool bRenderable, bool bMappable, bool bComputeAccess, uint32_t unMipLevels, uint32_t unArrayLayerCount, uint32_t unAdditionalVkCreateFlags, uint32_t unAdditionalVkUsageFlags, vr::SharedTextureHandle_t *pSharedHandle ) = 0; - /** Create a new tracked Vulkan Buffer */ virtual bool NewSharedVulkanBuffer( uint32_t nSize, uint32_t nUsageFlags, vr::SharedTextureHandle_t *pSharedHandle ) = 0; - /** Create a new tracked Vulkan Semaphore */ virtual bool NewSharedVulkanSemaphore( bool bCounting, vr::SharedTextureHandle_t *pSharedHandle ) = 0; - /** Grab a reference to hSharedHandle, and optionally generate a new IPC handle if pNewIpcHandle is not nullptr */ virtual bool RefResource( vr::SharedTextureHandle_t hSharedHandle, uint64_t *pNewIpcHandle ) = 0; - /** Drop a reference to hSharedHandle */ virtual bool UnrefResource( vr::SharedTextureHandle_t hSharedHandle ) = 0; - /* Get all the DRM formats we support using DMA-BUF images for. - * - * pOutFormatCount and pOutFormats function like Vulkan: - * - If pOutFormats is NULL, then pOutFormatCount will be overwritten with the format count. - * - If pOutFormats is not NULL, then pOutFormatCount specifies the size of the pOutFormats array, - * and will be overwritten with the number of formats written to the array. - * - * If the function fails, false is returned, and pOutFormatCount will be 0. - * Supported on Linux only. - */ virtual bool GetDmabufFormats( uint32_t *pOutFormatCount, uint32_t *pOutFormats ) = 0; - /** Get dmabuf modifiers we are allowed to use. - * - * pOutModifierCount and pOutModifiers function like Vulkan: - * - If pOutModifiers is NULL, then pOutModifierCount will be overwritten with the modifier count. - * - If pOutModifiers is not NULL, then pOutModifierCount specifies the size of the pOutModifiers array, - * and will be overwritten with the number of modifiers written to the array. - * - * If modifiers are not supported, a single DRM_FORMAT_MOD_INVALID entry will be returned. - * - * If the function fails, false is returned, and pOutModifierCount will be 0. - * Supported on Linux only. - */ virtual bool GetDmabufModifiers( vr::EVRApplicationType eApplicationType, uint32_t unDRMFormat, uint32_t *pOutModifierCount, uint64_t *pOutModifiers ) = 0; - /** Import a dmabuf directly. - * Note: the FD you pass in will be dup'ed, so you must close it yourself. - * This function does NOT take ownership of the fd you pass in. - * Supported on Linux only. - */ virtual bool ImportDmabuf( vr::EVRApplicationType eApplicationType, vr::DmabufAttributes_t *pDmabufAttributes, vr::SharedTextureHandle_t *pSharedHandle ) = 0; - /** Consumes an IPC handle (eg. from RefResource) and returns a file-descriptor. - * Caller acquires ownership of fd and is responsible for closing it. - * Supported on Linux only. - */ virtual bool ReceiveSharedFd( uint64_t ulIpcHandle, int *pOutFd ) = 0; protected: - /** Non-deletable */ virtual ~IVRIPCResourceManagerClient() {}; }; @@ -4566,7 +3963,7 @@ namespace vr inline COpenVRDriverContext &OpenVRInternal_ModuleServerDriverContext() { static void *ctx[sizeof( COpenVRDriverContext ) / sizeof( void * )]; - return *(COpenVRDriverContext *)ctx; // bypass zero-init constructor + return *(COpenVRDriverContext *)ctx; } inline IVRSettings *VR_CALLTYPE VRSettings() { return OpenVRInternal_ModuleServerDriverContext().VRSettings(); } @@ -4604,7 +4001,6 @@ namespace vr inline EVRInitError COpenVRDriverContext::InitServer() { Clear(); - // VRIPCResourceManager initialized async. if ( !VRServerDriverHost() || !VRSettings() || !VRProperties() @@ -4693,11 +4089,11 @@ namespace vr vr::CleanupDriverContext(); -#endif // OPENVR_INTERFACE_INTERNAL +#endif } -// End -#endif // _OPENVR_DRIVER_API + +#endif diff --git a/tools/vr/src/presenter.cpp b/tools/vr/src/presenter.cpp index d807c0764..2b0d446ec 100644 --- a/tools/vr/src/presenter.cpp +++ b/tools/vr/src/presenter.cpp @@ -1,4 +1,3 @@ -// See presenter.h. #define VK_NO_PROTOTYPES #include <vulkan/vulkan_core.h> typedef struct _XDisplay Display; @@ -41,7 +40,6 @@ void Log(const char *fmt, ...) { fprintf(stderr, "[droiddeck] %s\n", buf); } -// ---------------------------------------------------------------- Xlib, loaded at runtime struct X11 { void *lib = nullptr; @@ -68,7 +66,6 @@ struct X11 { } }; -// ---------------------------------------------------------------- Vulkan, loaded at runtime #define VK_GLOBAL(X) X(CreateInstance) #define VK_INSTANCE(X) \ @@ -124,7 +121,7 @@ public: std::lock_guard<std::mutex> lock(mutex_); for (int i = 0; i < 3; i++) { if (fds[i] < 0) continue; - int fd = dup(fds[i]); // the import owns its fd; the driver keeps its own + int fd = dup(fds[i]); if (fd >= 0) pending_.push_back({handles[i], fd, format, w, h}); } EnsureThread(); @@ -163,7 +160,6 @@ private: } } - // ------------------------------------------------------------ the presenting thread void Run() { const char *mode = getenv("DROIDDECK_VR_PRESENT"); @@ -198,9 +194,6 @@ private: if (wl_) { wl_display_dispatch_pending(wl_); wl_display_flush(wl_); } for (auto &p : imports) Import(p); for (auto h : removals) Forget(h); - // SteamVR hands the textures over at Present with no fence the driver can wait on: give the - // compositor's GPU work a moment to land before copying, as ALVR does (a torn or half-drawn - // frame otherwise). if (f.texture[0]) std::this_thread::sleep_until(at + guard_); if (f.texture[0] && Draw(f)) shown++; auto now = std::chrono::steady_clock::now(); @@ -216,10 +209,6 @@ private: std::chrono::microseconds guard_{4000}; - // ------------------------------------------------------------ Wayland: straight to DroidDeck's compositor - // The compositor the app runs (wayland-0 in the session's runtime dir, beside gamescope's socket) - // takes a fullscreen toplevel at the panel's size: one hop and one scale fewer than the X11 overlay - // through gamescope, whose output is the session's (often smaller) resolution. struct wl_display *wl_ = nullptr; struct wl_compositor *wlComp_ = nullptr; struct xdg_wm_base *wm_ = nullptr; @@ -272,9 +261,6 @@ private: xdg_toplevel_set_fullscreen(toplevel_, nullptr); wl_surface_commit(wlSurf_); for (int i = 0; i < 20 && !configured_; i++) wl_display_roundtrip(wl_); - // The size: the panel the Cardboard pass draws (two eyes, from the shared pose file), so nothing - // scales the picture down on the way. The compositor's configure carries the session's output - // size, which is often smaller; it is the fallback. width_ = height_ = 0; int fd = open("/run/droiddeck/droiddeck-headpose", O_RDONLY | O_CLOEXEC); if (fd >= 0) { @@ -291,8 +277,6 @@ private: bool InitWindow() { if (const char *g = getenv("DROIDDECK_VR_PRESENT_GUARD_MS")) guard_ = std::chrono::microseconds((long)(atof(g) * 1000)); if (!x_.Load()) { Log("presenter: no libX11"); return false; } - // gamescope's first Xwayland (:0) is where its overlays (mangoapp, Steam's) live; SteamVR's - // processes inherit the games' display (:1), so it is not taken from DISPLAY. const char *name = getenv("DROIDDECK_VR_DISPLAY"); if (!name || !*name) name = ":0"; dpy_ = x_.OpenDisplay(name); @@ -302,14 +286,11 @@ private: height_ = (uint32_t)x_.DisplayHeight(dpy_, scr); win_ = x_.CreateSimpleWindow(dpy_, x_.DefaultRootWindow(dpy_), 0, 0, width_, height_, 0, 0, 0); x_.StoreName(dpy_, win_, "DroidDeck Cardboard VR"); - // gamescope shows an external overlay above whatever has focus (mangoapp is one). - // gamescope composites one external overlay (mangoapp, the perf HUD, usually holds it) and one Steam - // overlay; the Steam overlay slot is free while nothing is shown over a game. const char *atom = getenv("DROIDDECK_VR_OVERLAY_ATOM"); if (!atom || !*atom) atom = "STEAM_OVERLAY"; - unsigned long cardinal = 6; // XA_CARDINAL - unsigned long one = 1; // format 32 properties are longs in Xlib - x_.ChangeProperty(dpy_, win_, x_.InternAtom(dpy_, atom, 0), cardinal, 32, 0 /* PropModeReplace */, + unsigned long cardinal = 6; + unsigned long one = 1; + x_.ChangeProperty(dpy_, win_, x_.InternAtom(dpy_, atom, 0), cardinal, 32, 0 , (const unsigned char *)&one, 1); x_.MapWindow(dpy_, win_); x_.Flush(dpy_); @@ -438,15 +419,10 @@ private: return -1; } - // Imports one of SteamVR's shared images with the parameters it was made with (ALVR's recipe): - // optimal tiling, mutable format (which keeps it uncompressed), opaque fd, dedicated memory. UNORM so a - // blit copies the bytes rather than decoding sRGB. void Import(const PendingImport &p) { Imported im{p.handle}; im.width = p.width; im.height = p.height; - // The same format list SteamVR's mutable sRGB images carry: with it Turnip keeps UBWC compression on, - // and the import has to agree on that or it reads compressed data as plain pixels. const char *fl = getenv("DROIDDECK_VR_IMPORT_FORMAT_LIST"); const bool formatList = !fl || strcmp(fl, "0") != 0; VkFormat formats[2] = {(VkFormat)p.format, UnormOf(p.format)}; @@ -516,8 +492,6 @@ private: VkCommandBufferBeginInfo bi = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, nullptr, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT}; vk_.BeginCommandBuffer(cmd_, &bi); VkImage dst = images_[idx]; - // The eye textures come from SteamVR's compositor (another process): take them from the external - // queue family as they are (GENERAL keeps the contents; they are uncompressed). VkImageMemoryBarrier pre[3] = { Barrier(dst, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT), Barrier(eye[0]->image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, 0, VK_ACCESS_TRANSFER_READ_BIT, @@ -597,11 +571,11 @@ private: std::vector<Imported> textures_; }; -} // namespace +} void Presenter::AddTextures(const uint64_t h[3], const int fds[3], uint32_t f, uint32_t w, uint32_t hh) { Impl::Get().Add(h, fds, f, w, hh); } void Presenter::RemoveTexture(uint64_t h) { Impl::Get().Remove(h); } void Presenter::Submit(const EyeFrame &f) { Impl::Get().Submit(f); } void Presenter::Shutdown() { Impl::Get().Shutdown(); } -} // namespace droiddeck +} diff --git a/tools/vr/src/presenter.h b/tools/vr/src/presenter.h index 47c6c8ac7..abbf455d6 100644 --- a/tools/vr/src/presenter.h +++ b/tools/vr/src/presenter.h @@ -1,34 +1,19 @@ -// Puts SteamVR's eye textures on the screen for the Cardboard driver (driver_droiddeck.cpp). -// -// The driver-direct component hands each frame over as two eye textures: shared Vulkan images, -// dmabufs on Linux. The presenter imports them into a Vulkan device of its own and draws them side by -// side into a fullscreen X11 window on gamescope's Xwayland, marked as an external overlay so gamescope -// shows it above whatever app has focus. DroidDeck's compositor, in Cardboard mode with a -// side-by-side source, then puts the lens distortion on it for the viewer. -// -// SteamVR compositor -> eye textures -> [presenter: blit L|R] -> X11 overlay -> gamescope -// -> DroidDeck compositor -> Cardboard lens pass -> panel -// -// Everything is loaded at runtime (libvulkan.so.1, libX11.so.6): the driver links against nothing -// the cross build image lacks. #pragma once #include <cstdint> namespace droiddeck { struct EyeFrame { - uint64_t texture[2]; // SharedTextureHandle_t per eye - float bounds[2][4]; // uMin, vMin, uMax, vMax per eye + uint64_t texture[2]; + float bounds[2][4]; }; class Presenter { public: - // Called from CreateSwapTextureSet: the three images of one set, as SteamVR made them. static void AddTextures(const uint64_t handles[3], const int fds[3], uint32_t vkFormat, uint32_t width, uint32_t height); static void RemoveTexture(uint64_t handle); - // Called from Present: the frame's eye textures; returns at once (a thread of its own presents). static void Submit(const EyeFrame &frame); static void Shutdown(); }; -} // namespace droiddeck +} diff --git a/tools/vr/src/vk_layer_droiddeck_vr.c b/tools/vr/src/vk_layer_droiddeck_vr.c index 8ac54888f..d1bb2bd21 100644 --- a/tools/vr/src/vk_layer_droiddeck_vr.c +++ b/tools/vr/src/vk_layer_droiddeck_vr.c @@ -1,33 +1,4 @@ #define _GNU_SOURCE -/* - * VK_LAYER_DROIDDECK_vr: lets SteamVR's vrcompositor create its Vulkan instance on Turnip/KGSL. - * - * vrcompositor always asks for VK_KHR_display and the direct-mode extensions around it (it opens a - * direct mode window even when a driver-direct-mode driver - tools/vr/src/driver_droiddeck.cpp - - * will receive the frames). Turnip's KGSL backend has no displays and refuses to enumerate any GPU - * on an instance with VK_KHR_display enabled. This layer takes those extensions out of the instance - * it creates below itself and answers the display queries on its own: there are no displays. - * - * Shared semaphores: SteamVR shares a timeline semaphore with every image it shares between its - * processes, exported as an opaque fd. Turnip/KGSL cannot export one. Such a semaphore is created - * unshared here instead, its "fd" is a placeholder (a memfd named droiddeck-detached-sem), and a - * process importing a placeholder keeps a detached semaphore whose GPU waits are dropped and whose - * host waits return at once. Each process still orders its own work; what is lost is only the GPU - * ordering between processes, which SteamVR's own IPC (one frame at a time) mostly covers. - * - * Where it runs: droiddeck-steamvr enables it explicitly in SteamVR's own processes (VK_INSTANCE_LAYERS - * + VK_LAYER_PATH). It is also an implicit layer for the whole session, because processes SteamVR did - * not start share textures with it too: Steam's web helper draws its in-VR overlay through SteamVR, - * needs the exportable semaphores, and without them crashes until Steam quits. As an implicit layer - * it stays out of the way: - * - the display stand-in (taking display extensions off, the fake display) happens only while - * SteamVR runs (droiddeck-steamvr's marker file) or where the layer was enabled by name, never in - * an ordinary session: gamescope and games see the driver's extensions unchanged; - * - the semaphore bridge is active in those same processes, and in Steam's web helper once SteamVR - * is installed (its GPU process outlives SteamVR runs); - * - everything else - every process of a session without SteamVR installed - gets the next layer's - * functions untouched. - */ #include <vulkan/vulkan.h> #include <vulkan/vk_layer.h> @@ -51,10 +22,7 @@ static void say(const char *fmt, const char *arg) { fputc('\n', stderr); } -/* ------------------------------------------------------------------ crash trace (debug) */ -/* DROIDDECK_VR_CRASHTRACE=1: print a backtrace on a fatal signal. vrcompositor's launcher drops - * LD_PRELOAD, but this layer is loaded into it, so this is the way to see where it dies. */ #include <dlfcn.h> #include <execinfo.h> #include <signal.h> @@ -86,7 +54,6 @@ static void crash_trace_install(void) { static int done; if (done++) return; if (!getenv("DROIDDECK_VR_CRASHTRACE")) { - /* Or a marker beside droiddeck-steamvr's, for processes whose environment is not ours (Steam). */ const char *rt = getenv("XDG_RUNTIME_DIR"); char path[512]; snprintf(path, sizeof(path), "%s/droiddeck-steamvr/crashtrace", rt && *rt ? rt : "/run/droiddeck"); @@ -100,33 +67,28 @@ static void crash_trace_install(void) { sa.sa_flags = SA_SIGINFO | SA_ONSTACK; int sigs[] = {SIGSEGV, SIGABRT, SIGBUS, SIGILL, SIGFPE}; for (unsigned i = 0; i < sizeof(sigs) / sizeof(*sigs); i++) { - /* Only where nothing handles the signal yet: Wine and FEX (Proton games) take SIGSEGV as part - * of their normal work, and replacing their handler crashes the game. */ struct sigaction old; if (sigaction(sigs[i], NULL, &old) == 0 && !(old.sa_flags & SA_SIGINFO) && old.sa_handler == SIG_DFL) sigaction(sigs[i], &sa, NULL); } } -/* ------------------------------------------------------------------ dispatch */ struct inst { VkInstance handle; PFN_vkGetInstanceProcAddr gipa; PFN_vkDestroyInstance destroy; - int stripped; /* the app asked for display extensions we took out */ - int active; /* see the top of the file */ + int stripped; + int active; }; struct dev { VkDevice handle; PFN_vkGetDeviceProcAddr gdpa; PFN_vkDestroyDevice destroy; int active; - int display; /* its instance has the pretend display */ + int display; }; -/* SteamVR is running (droiddeck-steamvr start/stop keeps this file), or the layer was asked for by - * name. Only then does the layer stand in for a display. */ static int steamvr_running(void) { const char *layers = getenv("VK_INSTANCE_LAYERS"); if (layers && strstr(layers, "VK_LAYER_DROIDDECK_vr")) return 1; @@ -137,10 +99,6 @@ static int steamvr_running(void) { return access(path, F_OK) == 0; } -/* Whether an instance gets the semaphore bridge. Steam's web helper shares its in-VR overlays with - * SteamVR, and its GPU process lives across SteamVR runs: one created before SteamVR started crashes - * once SteamVR shows the overlay, and after a few of those Steam quits. So it is bridged whenever - * SteamVR is installed, not only while it runs. */ static int bridge_wanted(void) { if (steamvr_running()) return 1; if (strcmp(program_invocation_short_name, "steamwebhelper")) return 0; @@ -155,7 +113,6 @@ static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; static struct inst g_inst[MAX_OBJS]; static struct dev g_dev[MAX_OBJS]; -/* The dispatch key: the loader's dispatch table pointer at the start of every dispatchable object. */ static void *key(const void *obj) { return *(void **)obj; } static struct inst *inst_for(const void *obj) { @@ -178,7 +135,6 @@ static struct dev *dev_for(const void *obj) { #define NEXT(dev, fn) ((PFN_vk##fn)dev_for(dev)->gdpa(dev, "vk" #fn)) #define HOOK_AS(str, fn) if (!strcmp(name, str)) return (PFN_vkVoidFunction)fn; -/* Extensions this layer takes off the instance and stands in for. */ static const char *const DISPLAY_EXTS[] = { "VK_KHR_display", "VK_EXT_direct_mode_display", "VK_EXT_display_surface_counter", "VK_KHR_get_display_properties2", "VK_EXT_acquire_xlib_display", "VK_EXT_acquire_drm_display", @@ -189,7 +145,6 @@ static int is_display_ext(const char *name) { return 0; } -/* ------------------------------------------------------------------ instance */ static VkResult VKAPI_CALL CreateInstance(const VkInstanceCreateInfo *ci, const VkAllocationCallbacks *alloc, VkInstance *out) { @@ -203,7 +158,6 @@ static VkResult VKAPI_CALL CreateInstance(const VkInstanceCreateInfo *ci, const PFN_vkCreateInstance next_create = (PFN_vkCreateInstance)next_gipa(VK_NULL_HANDLE, "vkCreateInstance"); if (!next_create) return VK_ERROR_INITIALIZATION_FAILED; - /* Outside SteamVR the instance is created exactly as asked: display extensions stay on. */ const int vr = steamvr_running(); VkInstanceCreateInfo mine = *ci; const char **names = calloc(ci->enabledExtensionCount + 1, sizeof(*names)); @@ -216,7 +170,6 @@ static VkResult VKAPI_CALL CreateInstance(const VkInstanceCreateInfo *ci, const if (stripped) { int have = 0; for (uint32_t i = 0; i < n; i++) have |= !strcmp(names[i], "VK_EXT_headless_surface"); - /* The pretend display's surface is a headless one; it needs VK_KHR_surface too. */ const char *more[] = {"VK_EXT_headless_surface", "VK_KHR_surface"}; names = realloc(names, (n + 3) * sizeof(*names)); if (!have) names[n++] = more[0]; @@ -253,7 +206,6 @@ static void VKAPI_CALL DestroyInstance(VkInstance instance, const VkAllocationCa if (d) d(instance, alloc); } -/* ------------------------------------------------------------------ device */ static VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceCreateInfo *ci, const VkAllocationCallbacks *alloc, VkDevice *out) { @@ -270,7 +222,6 @@ static VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceCreat const int active = in && in->active; const int display = in && in->stripped; - /* VK_EXT_display_control (vblank events on a display) goes too: there is no display. */ VkDeviceCreateInfo mine = *ci; const char **names = calloc(ci->enabledExtensionCount + 1, sizeof(*names)); uint32_t n = 0; @@ -307,13 +258,6 @@ static void VKAPI_CALL DestroyDevice(VkDevice device, const VkAllocationCallback if (f) f(device, alloc); } -/* ------------------------------------------------------------------ the display stand-in - * - * One pretend display with one mode and one plane. vrcompositor keeps a direct mode window on it even - * when a driver-direct driver takes the frames, and waits on that window's presents, so the display - * needs a working swapchain: its surface is a headless one (VK_EXT_headless_surface, Mesa's WSI). - * The vblank side of VK_EXT_display_control is a 60 Hz clock. Size and rate: - * DROIDDECK_VR_DISPLAY_MODE=WIDTHxHEIGHT@HZ (default 1920x1080@60). */ #include <time.h> @@ -452,8 +396,6 @@ static VkResult VKAPI_CALL GetRandROutputDisplayEXT(VkPhysicalDevice pd, void *d *d = FAKE_DISPLAY; return VK_SUCCESS; } -/* VK_EXT_display_control on the device: a 60 Hz clock. A vblank event's fence comes back signalled - * once the next tick has passed (the caller's vblank thread waits right after registering). */ static VkResult VKAPI_CALL DisplayPowerControlEXT(VkDevice d, VkDisplayKHR disp, const VkDisplayPowerInfoEXT *info) { return VK_SUCCESS; } static VkResult signalled_fence(VkDevice d, const VkAllocationCallbacks *a, VkFence *fence) { VkFenceCreateInfo fci = {VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, NULL, VK_FENCE_CREATE_SIGNALED_BIT}; @@ -477,14 +419,10 @@ static VkResult VKAPI_CALL GetSwapchainCounterEXT(VkDevice d, VkSwapchainKHR sc, return VK_SUCCESS; } -/* The pretend display's swapchain, on the headless surface: no swapchain counter (that belongs to - * VK_EXT_display_control, which the device does not have), and a request the headless WSI accepts. */ static VkResult VKAPI_CALL CreateSwapchainKHR(VkDevice d, const VkSwapchainCreateInfoKHR *ci, const VkAllocationCallbacks *a, VkSwapchainKHR *out) { PFN_vkCreateSwapchainKHR next = NEXT(d, CreateSwapchainKHR); VkSwapchainCreateInfoKHR sized = *ci; - /* A headless surface has no size of its own (UINT32_MAX), and SteamVR passes that through: the - * pretend display's mode is the size. */ mode_init(); if (sized.imageExtent.width == UINT32_MAX || sized.imageExtent.height == UINT32_MAX) sized.imageExtent = (VkExtent2D){g_mode_w, g_mode_h}; @@ -498,13 +436,12 @@ static VkResult VKAPI_CALL CreateSwapchainKHR(VkDevice d, const VkSwapchainCreat for (const VkBaseInStructure *p = ci->pNext; p && n < (int)sizeof(line) - 16; p = p->pNext) n += snprintf(line + n, sizeof(line) - n, " %d", p->sType); say("vkCreateSwapchainKHR refused (%s); retrying for the headless surface", line); VkSwapchainCreateInfoKHR mine = *ci; - mine.pNext = NULL; /* drops VkSwapchainCounterCreateInfoEXT and friends */ + mine.pNext = NULL; mine.presentMode = VK_PRESENT_MODE_FIFO_KHR; mine.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; if (mine.minImageCount < 2) mine.minImageCount = 2; r = next(d, &mine, a, out); if (r != VK_SUCCESS) { - /* Last resort: the plainest swapchain the surface can take. */ mine.imageFormat = VK_FORMAT_B8G8R8A8_UNORM; mine.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; mine.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; @@ -524,16 +461,6 @@ static PFN_vkVoidFunction display_device_hook(const char *name) { return NULL; } -/* ------------------------------------------------------------------ bridged shared semaphores - * - * SteamVR shares a timeline semaphore with every image it shares between processes, exported as an - * opaque fd; Turnip/KGSL cannot export one. Here such a semaphore is created unshared, and its "fd" is - * a memfd (named droiddeck-shared-sem) holding a 64-bit counter. Every process that has the semaphore - - * the creator and each importer - keeps its own real timeline semaphore, and a poller thread in each - * process carries values both ways: a value reached locally goes up into the counter, a value the - * counter shows (another process got there) is signalled locally from the host. GPU waits then work - * as written, about a poll period late. A binary semaphore cannot be bridged that way; GPU waits on an - * imported binary one are dropped (each process still orders its own work). */ #include <sched.h> #include <stdatomic.h> @@ -547,14 +474,14 @@ struct bridged { VkSemaphore sem; VkDevice dev; _Atomic uint64_t *counter; - int fd; /* the memfd, kept by the creator so every export can dup it; -1 for an importer */ - int timeline; /* 0: binary, bridged only by dropping its waits */ - unsigned long ino; /* the memfd's inode, which names the semaphore across processes in traces */ - uint64_t submitted; /* the highest value this process has queued a signal of */ + int fd; + int timeline; + unsigned long ino; + uint64_t submitted; }; static pthread_mutex_t g_br_lock = PTHREAD_MUTEX_INITIALIZER; static struct bridged g_br[MAX_SEMS]; -static VkSemaphore g_timeline_sems[MAX_TYPED]; /* every timeline semaphore this process made */ +static VkSemaphore g_timeline_sems[MAX_TYPED]; static pthread_t g_poller; static int g_poller_started; @@ -573,7 +500,6 @@ static void timeline_note(VkSemaphore s, int add) { pthread_mutex_unlock(&g_br_lock); } -/* Under g_br_lock. */ static struct bridged *bridged_find(VkSemaphore s) { for (int i = 0; i < MAX_SEMS; i++) if (g_br[i].sem == s) return &g_br[i]; return NULL; @@ -585,7 +511,6 @@ static void bridged_drop(struct bridged *b) { b->fd = -1; } -/* A `semtrace` file beside droiddeck-steamvr's marker: log every bridged semaphore every 2 s. */ static int sem_trace_on(void) { static int checked, on; if (!checked++) { @@ -673,7 +598,6 @@ static VkResult VKAPI_CALL CreateSemaphore(VkDevice d, const VkSemaphoreCreateIn return r; } if (!export_info) return r; - /* Rebuild the chain without the export request. */ VkSemaphoreTypeCreateInfo type = {0}; VkSemaphoreCreateInfo mine = *ci; mine.pNext = NULL; @@ -688,7 +612,7 @@ static VkResult VKAPI_CALL CreateSemaphore(VkDevice d, const VkSemaphoreCreateIn int fd = memfd_create(SHARED_NAME, MFD_CLOEXEC); _Atomic uint64_t *counter = NULL; if (fd >= 0 && ftruncate(fd, 4096) == 0) counter = map_counter(fd); - if (!counter) { if (fd >= 0) close(fd); return r; } /* unshared, unbridged */ + if (!counter) { if (fd >= 0) close(fd); return r; } atomic_store(counter, timeline ? type_info->initialValue : 0); pthread_mutex_lock(&g_br_lock); struct bridged *slot = bridged_find(VK_NULL_HANDLE); @@ -732,7 +656,7 @@ static VkResult VKAPI_CALL ImportSemaphoreFdKHR(VkDevice d, const VkImportSemaph if (info->fd < 0 || !is_shared_counter(info->fd)) return NEXT(d, ImportSemaphoreFdKHR)(d, info); _Atomic uint64_t *counter = map_counter(info->fd); const unsigned long ino = fd_ino(info->fd); - close(info->fd); /* a successful import owns the fd */ + close(info->fd); const int timeline = timeline_known(info->semaphore); pthread_mutex_lock(&g_br_lock); struct bridged *b = bridged_find(info->semaphore); @@ -748,7 +672,6 @@ static VkResult VKAPI_CALL ImportSemaphoreFdKHR(VkDevice d, const VkImportSemaph return VK_SUCCESS; } -/* An imported binary one: nothing here will ever signal it. */ static int drop_wait(VkSemaphore s) { pthread_mutex_lock(&g_br_lock); struct bridged *b = bridged_find(s); @@ -763,17 +686,8 @@ static uint64_t now_us(void) { return (uint64_t)ts.tv_sec * 1000000u + (uint64_t)ts.tv_nsec / 1000u; } -/* How long a submit may wait for another process to reach a shared value before the wait is dropped. */ #define SUBMIT_GRACE_US 3000 -/* Whether a submit should drop its GPU wait on s for value. - * - * Turnip on KGSL does not defer a submit that waits on a timeline value nobody has signalled yet ("wait - * before signal"): vkQueueSubmit blocks the calling thread until the value is signalled. SteamVR's - * client waits on a value of the compositor's that the compositor signals only after the client has - * submitted and told it so, which on Turnip deadlocks the client before its first frame (helloxr's - * acquire, DXVK's submit thread in Proton games). For an imported timeline semaphore whose value is - * neither reached nor queued here, the counter gets a short grace period, then the wait goes. */ static int submit_drop_wait(VkSemaphore s, uint64_t value) { pthread_mutex_lock(&g_br_lock); struct bridged *b = bridged_find(s); @@ -787,7 +701,6 @@ static int submit_drop_wait(VkSemaphore s, uint64_t value) { if (!imported || !value || value <= queued) return 0; struct dev *d = dev_for(dev); if (!d) return 0; - /* A Vulkan 1.0 device (SteamVR's client interop) has only the KHR names. */ PFN_vkGetSemaphoreCounterValue get = (PFN_vkGetSemaphoreCounterValue)d->gdpa(dev, "vkGetSemaphoreCounterValue"); if (!get) get = (PFN_vkGetSemaphoreCounterValue)d->gdpa(dev, "vkGetSemaphoreCounterValueKHR"); PFN_vkSignalSemaphore sig = (PFN_vkSignalSemaphore)d->gdpa(dev, "vkSignalSemaphore"); @@ -812,7 +725,6 @@ static int submit_drop_wait(VkSemaphore s, uint64_t value) { return 1; } -/* Notes the values this process queues signals of, so its own later waits on them are kept. */ static void note_signal(VkSemaphore s, uint64_t value) { pthread_mutex_lock(&g_br_lock); struct bridged *b = bridged_find(s); @@ -836,7 +748,6 @@ static void note_submit2_signals(uint32_t count, const VkSubmitInfo2 *submits) { note_signal(submits[i].pSignalSemaphoreInfos[j].semaphore, submits[i].pSignalSemaphoreInfos[j].value); } -/* Drops waits on detached semaphores from a list; returns the new count. */ static uint32_t keep_waits(const VkSemaphore *in, const VkPipelineStageFlags *stages, const uint64_t *values, uint32_t n, VkSemaphore *out, VkPipelineStageFlags *out_stages, uint64_t *out_values) { uint32_t k = 0; @@ -850,7 +761,6 @@ static uint32_t keep_waits(const VkSemaphore *in, const VkPipelineStageFlags *st return k; } -/* semtrace: the first wait and the first signal of each shared semaphore, and its first few values. */ static void trace_sem(const char *what, VkSemaphore sem, uint64_t value) { if (!sem_trace_on() || value > 3) return; pthread_mutex_lock(&g_br_lock); @@ -876,8 +786,6 @@ static VkResult VKAPI_CALL QueueSubmit(VkQueue q, uint32_t count, const VkSubmit for (uint32_t j = 0; j < submits[i].signalSemaphoreCount; j++) trace_sem("signals", submits[i].pSignalSemaphores[j], t && j < t->signalSemaphoreValueCount ? t->pSignalSemaphoreValues[j] : 0); } - /* Waits are judged before this submit's own signals count as queued: an acquire may wait on one - * value and signal the next on the same semaphore. */ int any = 0; for (uint32_t i = 0; i < count && !any; i++) { const VkTimelineSemaphoreSubmitInfo *t = NULL; @@ -911,7 +819,6 @@ static VkResult VKAPI_CALL QueueSubmit(VkQueue q, uint32_t count, const VkSubmit copy[i].pWaitSemaphores = sems; copy[i].pWaitDstStageMask = stages; if (t) { - /* Only this struct is replaced; the rest of the chain is kept behind it. */ tl[i] = *t; tl[i].waitSemaphoreValueCount = t->waitSemaphoreValueCount ? k : 0; tl[i].pWaitSemaphoreValues = vals; @@ -920,7 +827,7 @@ static VkResult VKAPI_CALL QueueSubmit(VkQueue q, uint32_t count, const VkSubmit for (const VkBaseInStructure *p = submits[i].pNext; p; prev = p, p = p->pNext) if (p == (const void *)t) { rest = p->pNext; break; } (void)prev; tl[i].pNext = rest; - copy[i].pNext = &tl[i]; /* other structs before it are dropped: SteamVR chains only this one */ + copy[i].pNext = &tl[i]; } } VkResult r = next(q, count, copy, fence); @@ -989,7 +896,6 @@ static VkResult VKAPI_CALL GetSemaphoreCounterValue(VkDevice d, VkSemaphore s, u return next(d, s, value); } -/* semtrace also logs how shared (external memory) images are created, to compare both sides. */ static VkResult VKAPI_CALL CreateImage(VkDevice d, const VkImageCreateInfo *ci, const VkAllocationCallbacks *a, VkImage *out) { VkResult r = NEXT(d, CreateImage)(d, ci, a, out); if (!sem_trace_on()) return r; @@ -1026,7 +932,6 @@ static PFN_vkVoidFunction semaphore_hook(const char *name) { return NULL; } -/* ------------------------------------------------------------------ proc addr */ #define HOOK(fn) if (!strcmp(name, "vk" #fn)) return (PFN_vkVoidFunction)fn; @@ -1085,7 +990,6 @@ EXPORT VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL DroidDeckVr_GetDeviceProcAddr(Vk if (f) return f; } if (d) { - /* Only where the driver has the function: the hooks call through to it. */ PFN_vkVoidFunction mine = semaphore_hook(name); if (mine && d->gdpa(device, name)) return mine; } diff --git a/tools/vr/src/wayland_client_stub.c b/tools/vr/src/wayland_client_stub.c index 8b1b6cdff..d81a07fda 100644 --- a/tools/vr/src/wayland_client_stub.c +++ b/tools/vr/src/wayland_client_stub.c @@ -1,6 +1,3 @@ -/* Link-time stand-in for libwayland-client.so.0 (tools/vr/build.sh): the symbols the presenter uses, so - * the driver links with -z defs against a library the build machine does not have. Never loaded: the - * runtime's real libwayland-client.so.0 satisfies the dependency on the device. */ #include <stdint.h> #include <stddef.h> #include "wayland-util.h" diff --git a/tools/vr/third_party/wayland/wayland-client-core.h b/tools/vr/third_party/wayland/wayland-client-core.h index e0523e49d..3f55d23c7 100644 --- a/tools/vr/third_party/wayland/wayland-client-core.h +++ b/tools/vr/third_party/wayland/wayland-client-core.h @@ -36,94 +36,12 @@ extern "C" { struct timespec; -/** \class wl_proxy - * - * \brief Represents a protocol object on the client side. - * - * A wl_proxy acts as a client side proxy to an object existing in the - * compositor. The proxy is responsible for converting requests made by the - * clients with \ref wl_proxy_marshal() into Wayland's wire format. Events - * coming from the compositor are also handled by the proxy, which will in - * turn call the handler set with \ref wl_proxy_add_listener(). - * - * \note With the exception of function \ref wl_proxy_set_queue(), functions - * accessing a wl_proxy are not normally used by client code. Clients - * should normally use the higher level interface generated by the scanner to - * interact with compositor objects. - * - */ struct wl_proxy; -/** \class wl_display - * - * \brief Represents a connection to the compositor and acts as a proxy to - * the wl_display singleton object. - * - * A wl_display object represents a client connection to a Wayland - * compositor. It is created with either \ref wl_display_connect() or - * \ref wl_display_connect_to_fd(). A connection is terminated using - * \ref wl_display_disconnect(). - * - * A wl_display is also used as the \ref wl_proxy for the wl_display - * singleton object on the compositor side. - * - * A wl_display object handles all the data sent from and to the - * compositor. When a \ref wl_proxy marshals a request, it will write its wire - * representation to the display's write buffer. The data is sent to the - * compositor when the client calls \ref wl_display_flush(). - * - * Incoming data is handled in two steps: queueing and dispatching. In the - * queue step, the data coming from the display fd is interpreted and - * added to a queue. On the dispatch step, the handler for the incoming - * event set by the client on the corresponding \ref wl_proxy is called. - * - * A wl_display has at least one event queue, called the <em>default - * queue</em>. Clients can create additional event queues with \ref - * wl_display_create_queue() and assign \ref wl_proxy's to it. Events - * occurring in a particular proxy are always queued in its assigned queue. - * A client can ensure that a certain assumption, such as holding a lock - * or running from a given thread, is true when a proxy event handler is - * called by assigning that proxy to an event queue and making sure that - * this queue is only dispatched when the assumption holds. - * - * The default queue is dispatched by calling \ref wl_display_dispatch(). - * This will dispatch any events queued on the default queue and attempt - * to read from the display fd if it's empty. Events read are then queued - * on the appropriate queues according to the proxy assignment. - * - * A user created queue is dispatched with \ref wl_display_dispatch_queue(). - * This function behaves exactly the same as wl_display_dispatch() - * but it dispatches given queue instead of the default queue. - * - * A real world example of event queue usage is Mesa's implementation of - * eglSwapBuffers() for the Wayland platform. This function might need - * to block until a frame callback is received, but dispatching the default - * queue could cause an event handler on the client to start drawing - * again. This problem is solved using another event queue, so that only - * the events handled by the EGL code are dispatched during the block. - * - * This creates a problem where a thread dispatches a non-default - * queue, reading all the data from the display fd. If the application - * would call \em poll(2) after that it would block, even though there - * might be events queued on the default queue. Those events should be - * dispatched with \ref wl_display_dispatch_pending() or \ref - * wl_display_dispatch_queue_pending() before flushing and blocking. - */ struct wl_display; -/** \class wl_event_queue - * - * \brief A queue for \ref wl_proxy object events. - * - * Event queues allows the events on a display to be handled in a thread-safe - * manner. See \ref wl_display for details. - * - */ struct wl_event_queue; -/** Destroy proxy after marshalling - * \relates wl_proxy - */ #define WL_MARSHAL_FLAG_DESTROY (1 << 0) void diff --git a/tools/vr/third_party/wayland/wayland-client-protocol.h b/tools/vr/third_party/wayland/wayland-client-protocol.h index 43a1e4e55..8c99c054f 100644 --- a/tools/vr/third_party/wayland/wayland-client-protocol.h +++ b/tools/vr/third_party/wayland/wayland-client-protocol.h @@ -1,4 +1,3 @@ -/* Generated by wayland-scanner 1.26.0 */ #ifndef WAYLAND_CLIENT_PROTOCOL_H #define WAYLAND_CLIENT_PROTOCOL_H @@ -11,34 +10,8 @@ extern "C" { #endif -/** - * @page page_wayland The wayland protocol - * @section page_ifaces_wayland Interfaces - * - @subpage page_iface_wl_display - core global object - * - @subpage page_iface_wl_registry - global registry object - * - @subpage page_iface_wl_callback - callback object - * - @subpage page_iface_wl_compositor - the compositor singleton - * - @subpage page_iface_wl_shm_pool - a shared memory pool - * - @subpage page_iface_wl_shm - shared memory support - * - @subpage page_iface_wl_buffer - content for a wl_surface - * - @subpage page_iface_wl_data_offer - offer to transfer data - * - @subpage page_iface_wl_data_source - offer to transfer data - * - @subpage page_iface_wl_data_device - data transfer device - * - @subpage page_iface_wl_data_device_manager - data transfer interface - * - @subpage page_iface_wl_shell - create desktop-style surfaces - * - @subpage page_iface_wl_shell_surface - desktop-style metadata interface - * - @subpage page_iface_wl_surface - an onscreen surface - * - @subpage page_iface_wl_seat - group of input devices - * - @subpage page_iface_wl_pointer - pointer input device - * - @subpage page_iface_wl_keyboard - keyboard input device - * - @subpage page_iface_wl_touch - touchscreen input device - * - @subpage page_iface_wl_output - compositor output region - * - @subpage page_iface_wl_region - region interface - * - @subpage page_iface_wl_subcompositor - sub-surface compositing - * - @subpage page_iface_wl_subsurface - sub-surface interface to a wl_surface - * - @subpage page_iface_wl_fixes - wayland protocol fixes +/* * @section page_copyright_wayland Copyright - * <pre> * * Copyright © 2008-2011 Kristian Høgsberg * Copyright © 2010-2011 Intel Corporation @@ -64,8 +37,34 @@ extern "C" { * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. - * </pre> */ + + + + + + + + + + + + + + + + + + + + + + + + + + + struct wl_buffer; struct wl_callback; struct wl_compositor; @@ -92,942 +91,118 @@ struct wl_touch; #ifndef WL_DISPLAY_INTERFACE #define WL_DISPLAY_INTERFACE -/** - * @page page_iface_wl_display wl_display - * @section page_iface_wl_display_desc Description - * - * The core global object. This is a special singleton object. It - * is used for internal Wayland protocol features. - * @section page_iface_wl_display_api API - * See @ref iface_wl_display. - */ -/** - * @defgroup iface_wl_display The wl_display interface - * - * The core global object. This is a special singleton object. It - * is used for internal Wayland protocol features. - */ extern const struct wl_interface wl_display_interface; #endif #ifndef WL_REGISTRY_INTERFACE #define WL_REGISTRY_INTERFACE -/** - * @page page_iface_wl_registry wl_registry - * @section page_iface_wl_registry_desc Description - * - * The singleton global registry object. The server has a number of - * global objects that are available to all clients. These objects - * typically represent an actual object in the server (for example, - * an input device) or they are singleton objects that provide - * extension functionality. - * - * When a client creates a registry object, the registry object - * will emit a global event for each global currently in the - * registry. Globals come and go as a result of device or - * monitor hotplugs, reconfiguration or other events, and the - * registry will send out global and global_remove events to - * keep the client up to date with the changes. To mark the end - * of the initial burst of events, the client can use the - * wl_display.sync request immediately after calling - * wl_display.get_registry. - * - * A client can bind to a global object by using the bind - * request. This creates a client-side handle that lets the object - * emit events to the client and lets the client invoke requests on - * the object. - * @section page_iface_wl_registry_api API - * See @ref iface_wl_registry. - */ -/** - * @defgroup iface_wl_registry The wl_registry interface - * - * The singleton global registry object. The server has a number of - * global objects that are available to all clients. These objects - * typically represent an actual object in the server (for example, - * an input device) or they are singleton objects that provide - * extension functionality. - * - * When a client creates a registry object, the registry object - * will emit a global event for each global currently in the - * registry. Globals come and go as a result of device or - * monitor hotplugs, reconfiguration or other events, and the - * registry will send out global and global_remove events to - * keep the client up to date with the changes. To mark the end - * of the initial burst of events, the client can use the - * wl_display.sync request immediately after calling - * wl_display.get_registry. - * - * A client can bind to a global object by using the bind - * request. This creates a client-side handle that lets the object - * emit events to the client and lets the client invoke requests on - * the object. - */ extern const struct wl_interface wl_registry_interface; #endif #ifndef WL_CALLBACK_INTERFACE #define WL_CALLBACK_INTERFACE -/** - * @page page_iface_wl_callback wl_callback - * @section page_iface_wl_callback_desc Description - * - * Clients can handle the 'done' event to get notified when - * the related request is done. - * - * Note, because wl_callback objects are created from multiple independent - * factory interfaces, the wl_callback interface is frozen at version 1. - * @section page_iface_wl_callback_api API - * See @ref iface_wl_callback. - */ -/** - * @defgroup iface_wl_callback The wl_callback interface - * - * Clients can handle the 'done' event to get notified when - * the related request is done. - * - * Note, because wl_callback objects are created from multiple independent - * factory interfaces, the wl_callback interface is frozen at version 1. - */ extern const struct wl_interface wl_callback_interface; #endif #ifndef WL_COMPOSITOR_INTERFACE #define WL_COMPOSITOR_INTERFACE -/** - * @page page_iface_wl_compositor wl_compositor - * @section page_iface_wl_compositor_desc Description - * - * A compositor. This object is a singleton global. The - * compositor is in charge of combining the contents of multiple - * surfaces into one displayable output. - * @section page_iface_wl_compositor_api API - * See @ref iface_wl_compositor. - */ -/** - * @defgroup iface_wl_compositor The wl_compositor interface - * - * A compositor. This object is a singleton global. The - * compositor is in charge of combining the contents of multiple - * surfaces into one displayable output. - */ extern const struct wl_interface wl_compositor_interface; #endif #ifndef WL_SHM_POOL_INTERFACE #define WL_SHM_POOL_INTERFACE -/** - * @page page_iface_wl_shm_pool wl_shm_pool - * @section page_iface_wl_shm_pool_desc Description - * - * The wl_shm_pool object encapsulates a piece of memory shared - * between the compositor and client. Through the wl_shm_pool - * object, the client can allocate shared memory wl_buffer objects. - * All objects created through the same pool share the same - * underlying mapped memory. Reusing the mapped memory avoids the - * setup/teardown overhead and is useful when interactively resizing - * a surface or for many small buffers. - * @section page_iface_wl_shm_pool_api API - * See @ref iface_wl_shm_pool. - */ -/** - * @defgroup iface_wl_shm_pool The wl_shm_pool interface - * - * The wl_shm_pool object encapsulates a piece of memory shared - * between the compositor and client. Through the wl_shm_pool - * object, the client can allocate shared memory wl_buffer objects. - * All objects created through the same pool share the same - * underlying mapped memory. Reusing the mapped memory avoids the - * setup/teardown overhead and is useful when interactively resizing - * a surface or for many small buffers. - */ extern const struct wl_interface wl_shm_pool_interface; #endif #ifndef WL_SHM_INTERFACE #define WL_SHM_INTERFACE -/** - * @page page_iface_wl_shm wl_shm - * @section page_iface_wl_shm_desc Description - * - * A singleton global object that provides support for shared - * memory. - * - * Clients can create wl_shm_pool objects using the create_pool - * request. - * - * On binding the wl_shm object one or more format events - * are emitted to inform clients about the valid pixel formats - * that can be used for buffers. - * @section page_iface_wl_shm_api API - * See @ref iface_wl_shm. - */ -/** - * @defgroup iface_wl_shm The wl_shm interface - * - * A singleton global object that provides support for shared - * memory. - * - * Clients can create wl_shm_pool objects using the create_pool - * request. - * - * On binding the wl_shm object one or more format events - * are emitted to inform clients about the valid pixel formats - * that can be used for buffers. - */ extern const struct wl_interface wl_shm_interface; #endif #ifndef WL_BUFFER_INTERFACE #define WL_BUFFER_INTERFACE -/** - * @page page_iface_wl_buffer wl_buffer - * @section page_iface_wl_buffer_desc Description - * - * A buffer provides the content for a wl_surface. Buffers are - * created through factory interfaces such as wl_shm, wp_linux_buffer_params - * (from the linux-dmabuf protocol extension) or similar. It has a width and - * a height and can be attached to a wl_surface, but the mechanism by which a - * client provides and updates the contents is defined by the buffer factory - * interface. - * - * Color channels are assumed to be electrical rather than optical (in other - * words, encoded with a transfer function) unless otherwise specified. If - * the buffer uses a format that has an alpha channel, the alpha channel is - * assumed to be premultiplied into the electrical color channel values - * (after transfer function encoding) unless otherwise specified. - * - * Note, because wl_buffer objects are created from multiple independent - * factory interfaces, the wl_buffer interface is frozen at version 1. - * @section page_iface_wl_buffer_api API - * See @ref iface_wl_buffer. - */ -/** - * @defgroup iface_wl_buffer The wl_buffer interface - * - * A buffer provides the content for a wl_surface. Buffers are - * created through factory interfaces such as wl_shm, wp_linux_buffer_params - * (from the linux-dmabuf protocol extension) or similar. It has a width and - * a height and can be attached to a wl_surface, but the mechanism by which a - * client provides and updates the contents is defined by the buffer factory - * interface. - * - * Color channels are assumed to be electrical rather than optical (in other - * words, encoded with a transfer function) unless otherwise specified. If - * the buffer uses a format that has an alpha channel, the alpha channel is - * assumed to be premultiplied into the electrical color channel values - * (after transfer function encoding) unless otherwise specified. - * - * Note, because wl_buffer objects are created from multiple independent - * factory interfaces, the wl_buffer interface is frozen at version 1. - */ extern const struct wl_interface wl_buffer_interface; #endif #ifndef WL_DATA_OFFER_INTERFACE #define WL_DATA_OFFER_INTERFACE -/** - * @page page_iface_wl_data_offer wl_data_offer - * @section page_iface_wl_data_offer_desc Description - * - * A wl_data_offer represents a piece of data offered for transfer - * by another client (the source client). It is used by the - * copy-and-paste and drag-and-drop mechanisms. The offer - * describes the different mime types that the data can be - * converted to and provides the mechanism for transferring the - * data directly from the source client. - * @section page_iface_wl_data_offer_api API - * See @ref iface_wl_data_offer. - */ -/** - * @defgroup iface_wl_data_offer The wl_data_offer interface - * - * A wl_data_offer represents a piece of data offered for transfer - * by another client (the source client). It is used by the - * copy-and-paste and drag-and-drop mechanisms. The offer - * describes the different mime types that the data can be - * converted to and provides the mechanism for transferring the - * data directly from the source client. - */ extern const struct wl_interface wl_data_offer_interface; #endif #ifndef WL_DATA_SOURCE_INTERFACE #define WL_DATA_SOURCE_INTERFACE -/** - * @page page_iface_wl_data_source wl_data_source - * @section page_iface_wl_data_source_desc Description - * - * The wl_data_source object is the source side of a wl_data_offer. - * It is created by the source client in a data transfer and - * provides a way to describe the offered data and a way to respond - * to requests to transfer the data. - * @section page_iface_wl_data_source_api API - * See @ref iface_wl_data_source. - */ -/** - * @defgroup iface_wl_data_source The wl_data_source interface - * - * The wl_data_source object is the source side of a wl_data_offer. - * It is created by the source client in a data transfer and - * provides a way to describe the offered data and a way to respond - * to requests to transfer the data. - */ extern const struct wl_interface wl_data_source_interface; #endif #ifndef WL_DATA_DEVICE_INTERFACE #define WL_DATA_DEVICE_INTERFACE -/** - * @page page_iface_wl_data_device wl_data_device - * @section page_iface_wl_data_device_desc Description - * - * There is one wl_data_device per seat which can be obtained - * from the global wl_data_device_manager singleton. - * - * A wl_data_device provides access to inter-client data transfer - * mechanisms such as copy-and-paste and drag-and-drop. - * @section page_iface_wl_data_device_api API - * See @ref iface_wl_data_device. - */ -/** - * @defgroup iface_wl_data_device The wl_data_device interface - * - * There is one wl_data_device per seat which can be obtained - * from the global wl_data_device_manager singleton. - * - * A wl_data_device provides access to inter-client data transfer - * mechanisms such as copy-and-paste and drag-and-drop. - */ extern const struct wl_interface wl_data_device_interface; #endif #ifndef WL_DATA_DEVICE_MANAGER_INTERFACE #define WL_DATA_DEVICE_MANAGER_INTERFACE -/** - * @page page_iface_wl_data_device_manager wl_data_device_manager - * @section page_iface_wl_data_device_manager_desc Description - * - * The wl_data_device_manager is a singleton global object that - * provides access to inter-client data transfer mechanisms such as - * copy-and-paste and drag-and-drop. These mechanisms are tied to - * a wl_seat and this interface lets a client get a wl_data_device - * corresponding to a wl_seat. - * - * Depending on the version bound, the objects created from the bound - * wl_data_device_manager object will have different requirements for - * functioning properly. See wl_data_source.set_actions, - * wl_data_offer.accept and wl_data_offer.finish for details. - * @section page_iface_wl_data_device_manager_api API - * See @ref iface_wl_data_device_manager. - */ -/** - * @defgroup iface_wl_data_device_manager The wl_data_device_manager interface - * - * The wl_data_device_manager is a singleton global object that - * provides access to inter-client data transfer mechanisms such as - * copy-and-paste and drag-and-drop. These mechanisms are tied to - * a wl_seat and this interface lets a client get a wl_data_device - * corresponding to a wl_seat. - * - * Depending on the version bound, the objects created from the bound - * wl_data_device_manager object will have different requirements for - * functioning properly. See wl_data_source.set_actions, - * wl_data_offer.accept and wl_data_offer.finish for details. - */ extern const struct wl_interface wl_data_device_manager_interface; #endif #ifndef WL_SHELL_INTERFACE #define WL_SHELL_INTERFACE -/** - * @page page_iface_wl_shell wl_shell - * @section page_iface_wl_shell_desc Description - * - * This interface is implemented by servers that provide - * desktop-style user interfaces. - * - * It allows clients to associate a wl_shell_surface with - * a basic surface. - * - * Note! This protocol is deprecated and not intended for production use. - * For desktop-style user interfaces, use xdg_shell. Compositors and clients - * should not implement this interface. - * @section page_iface_wl_shell_api API - * See @ref iface_wl_shell. - */ -/** - * @defgroup iface_wl_shell The wl_shell interface - * - * This interface is implemented by servers that provide - * desktop-style user interfaces. - * - * It allows clients to associate a wl_shell_surface with - * a basic surface. - * - * Note! This protocol is deprecated and not intended for production use. - * For desktop-style user interfaces, use xdg_shell. Compositors and clients - * should not implement this interface. - */ extern const struct wl_interface wl_shell_interface; #endif #ifndef WL_SHELL_SURFACE_INTERFACE #define WL_SHELL_SURFACE_INTERFACE -/** - * @page page_iface_wl_shell_surface wl_shell_surface - * @section page_iface_wl_shell_surface_desc Description - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides requests to treat surfaces like toplevel, fullscreen - * or popup windows, move, resize or maximize them, associate - * metadata like title and class, etc. - * - * On the server side the object is automatically destroyed when - * the related wl_surface is destroyed. On the client side, - * wl_shell_surface_destroy() must be called before destroying - * the wl_surface object. - * @section page_iface_wl_shell_surface_api API - * See @ref iface_wl_shell_surface. - */ -/** - * @defgroup iface_wl_shell_surface The wl_shell_surface interface - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides requests to treat surfaces like toplevel, fullscreen - * or popup windows, move, resize or maximize them, associate - * metadata like title and class, etc. - * - * On the server side the object is automatically destroyed when - * the related wl_surface is destroyed. On the client side, - * wl_shell_surface_destroy() must be called before destroying - * the wl_surface object. - */ extern const struct wl_interface wl_shell_surface_interface; #endif #ifndef WL_SURFACE_INTERFACE #define WL_SURFACE_INTERFACE -/** - * @page page_iface_wl_surface wl_surface - * @section page_iface_wl_surface_desc Description - * - * A surface is a rectangular area that may be displayed on zero - * or more outputs, and shown any number of times at the compositor's - * discretion. They can present wl_buffers, receive user input, and - * define a local coordinate system. - * - * The size of a surface (and relative positions on it) is described - * in surface-local coordinates, which may differ from the buffer - * coordinates of the pixel content, in case a buffer_transform - * or a buffer_scale is used. - * - * A surface without a "role" is fairly useless: a compositor does - * not know where, when or how to present it. The role is the - * purpose of a wl_surface. Examples of roles are a cursor for a - * pointer (as set by wl_pointer.set_cursor), a drag icon - * (wl_data_device.start_drag), a sub-surface - * (wl_subcompositor.get_subsurface), and a window as defined by a - * shell protocol (e.g. wl_shell.get_shell_surface). - * - * A surface can have only one role at a time. Initially a - * wl_surface does not have a role. Once a wl_surface is given a - * role, it is set permanently for the whole lifetime of the - * wl_surface object. Giving the current role again is allowed, - * unless explicitly forbidden by the relevant interface - * specification. - * - * Surface roles are given by requests in other interfaces such as - * wl_pointer.set_cursor. The request should explicitly mention - * that this request gives a role to a wl_surface. Often, this - * request also creates a new protocol object that represents the - * role and adds additional functionality to wl_surface. When a - * client wants to destroy a wl_surface, they must destroy this role - * object before the wl_surface, otherwise a defunct_role_object error is - * sent. - * - * Destroying the role object does not remove the role from the - * wl_surface, but it may stop the wl_surface from "playing the role". - * For instance, if a wl_subsurface object is destroyed, the wl_surface - * it was created for will be unmapped and forget its position and - * z-order. It is allowed to create a wl_subsurface for the same - * wl_surface again, but it is not allowed to use the wl_surface as - * a cursor (cursor is a different role than sub-surface, and role - * switching is not allowed). - * @section page_iface_wl_surface_api API - * See @ref iface_wl_surface. - */ -/** - * @defgroup iface_wl_surface The wl_surface interface - * - * A surface is a rectangular area that may be displayed on zero - * or more outputs, and shown any number of times at the compositor's - * discretion. They can present wl_buffers, receive user input, and - * define a local coordinate system. - * - * The size of a surface (and relative positions on it) is described - * in surface-local coordinates, which may differ from the buffer - * coordinates of the pixel content, in case a buffer_transform - * or a buffer_scale is used. - * - * A surface without a "role" is fairly useless: a compositor does - * not know where, when or how to present it. The role is the - * purpose of a wl_surface. Examples of roles are a cursor for a - * pointer (as set by wl_pointer.set_cursor), a drag icon - * (wl_data_device.start_drag), a sub-surface - * (wl_subcompositor.get_subsurface), and a window as defined by a - * shell protocol (e.g. wl_shell.get_shell_surface). - * - * A surface can have only one role at a time. Initially a - * wl_surface does not have a role. Once a wl_surface is given a - * role, it is set permanently for the whole lifetime of the - * wl_surface object. Giving the current role again is allowed, - * unless explicitly forbidden by the relevant interface - * specification. - * - * Surface roles are given by requests in other interfaces such as - * wl_pointer.set_cursor. The request should explicitly mention - * that this request gives a role to a wl_surface. Often, this - * request also creates a new protocol object that represents the - * role and adds additional functionality to wl_surface. When a - * client wants to destroy a wl_surface, they must destroy this role - * object before the wl_surface, otherwise a defunct_role_object error is - * sent. - * - * Destroying the role object does not remove the role from the - * wl_surface, but it may stop the wl_surface from "playing the role". - * For instance, if a wl_subsurface object is destroyed, the wl_surface - * it was created for will be unmapped and forget its position and - * z-order. It is allowed to create a wl_subsurface for the same - * wl_surface again, but it is not allowed to use the wl_surface as - * a cursor (cursor is a different role than sub-surface, and role - * switching is not allowed). - */ extern const struct wl_interface wl_surface_interface; #endif #ifndef WL_SEAT_INTERFACE #define WL_SEAT_INTERFACE -/** - * @page page_iface_wl_seat wl_seat - * @section page_iface_wl_seat_desc Description - * - * A seat is a group of keyboards, pointer and touch devices. This - * object is published as a global during start up, or when such a - * device is hot plugged. A seat typically has a pointer and - * maintains a keyboard focus and a pointer focus. - * @section page_iface_wl_seat_api API - * See @ref iface_wl_seat. - */ -/** - * @defgroup iface_wl_seat The wl_seat interface - * - * A seat is a group of keyboards, pointer and touch devices. This - * object is published as a global during start up, or when such a - * device is hot plugged. A seat typically has a pointer and - * maintains a keyboard focus and a pointer focus. - */ extern const struct wl_interface wl_seat_interface; #endif #ifndef WL_POINTER_INTERFACE #define WL_POINTER_INTERFACE -/** - * @page page_iface_wl_pointer wl_pointer - * @section page_iface_wl_pointer_desc Description - * - * The wl_pointer interface represents one or more input devices, - * such as mice, which control the pointer location and pointer_focus - * of a seat. - * - * The wl_pointer interface generates motion, enter and leave - * events for the surfaces that the pointer is located over, - * and button and axis events for button presses, button releases - * and scrolling. - * @section page_iface_wl_pointer_api API - * See @ref iface_wl_pointer. - */ -/** - * @defgroup iface_wl_pointer The wl_pointer interface - * - * The wl_pointer interface represents one or more input devices, - * such as mice, which control the pointer location and pointer_focus - * of a seat. - * - * The wl_pointer interface generates motion, enter and leave - * events for the surfaces that the pointer is located over, - * and button and axis events for button presses, button releases - * and scrolling. - */ extern const struct wl_interface wl_pointer_interface; #endif #ifndef WL_KEYBOARD_INTERFACE #define WL_KEYBOARD_INTERFACE -/** - * @page page_iface_wl_keyboard wl_keyboard - * @section page_iface_wl_keyboard_desc Description - * - * The wl_keyboard interface represents one or more keyboards - * associated with a seat. - * - * Each wl_keyboard has the following logical state: - * - * - an active surface (possibly null), - * - the keys currently logically down, - * - the active modifiers, - * - the active group. - * - * By default, the active surface is null, the keys currently logically down - * are empty, the active modifiers and the active group are 0. - * @section page_iface_wl_keyboard_api API - * See @ref iface_wl_keyboard. - */ -/** - * @defgroup iface_wl_keyboard The wl_keyboard interface - * - * The wl_keyboard interface represents one or more keyboards - * associated with a seat. - * - * Each wl_keyboard has the following logical state: - * - * - an active surface (possibly null), - * - the keys currently logically down, - * - the active modifiers, - * - the active group. - * - * By default, the active surface is null, the keys currently logically down - * are empty, the active modifiers and the active group are 0. - */ extern const struct wl_interface wl_keyboard_interface; #endif #ifndef WL_TOUCH_INTERFACE #define WL_TOUCH_INTERFACE -/** - * @page page_iface_wl_touch wl_touch - * @section page_iface_wl_touch_desc Description - * - * The wl_touch interface represents a touchscreen - * associated with a seat. - * - * Touch interactions can consist of one or more contacts. - * For each contact, a series of events is generated, starting - * with a down event, followed by zero or more motion events, - * and ending with an up event. Events relating to the same - * contact point can be identified by the ID of the sequence. - * @section page_iface_wl_touch_api API - * See @ref iface_wl_touch. - */ -/** - * @defgroup iface_wl_touch The wl_touch interface - * - * The wl_touch interface represents a touchscreen - * associated with a seat. - * - * Touch interactions can consist of one or more contacts. - * For each contact, a series of events is generated, starting - * with a down event, followed by zero or more motion events, - * and ending with an up event. Events relating to the same - * contact point can be identified by the ID of the sequence. - */ extern const struct wl_interface wl_touch_interface; #endif #ifndef WL_OUTPUT_INTERFACE #define WL_OUTPUT_INTERFACE -/** - * @page page_iface_wl_output wl_output - * @section page_iface_wl_output_desc Description - * - * An output describes part of the compositor geometry. The - * compositor works in the 'compositor coordinate system' and an - * output corresponds to a rectangular area in that space that is - * actually visible. This typically corresponds to a monitor that - * displays part of the compositor space. This object is published - * as global during start up, or when a monitor is hotplugged. - * @section page_iface_wl_output_api API - * See @ref iface_wl_output. - */ -/** - * @defgroup iface_wl_output The wl_output interface - * - * An output describes part of the compositor geometry. The - * compositor works in the 'compositor coordinate system' and an - * output corresponds to a rectangular area in that space that is - * actually visible. This typically corresponds to a monitor that - * displays part of the compositor space. This object is published - * as global during start up, or when a monitor is hotplugged. - */ extern const struct wl_interface wl_output_interface; #endif #ifndef WL_REGION_INTERFACE #define WL_REGION_INTERFACE -/** - * @page page_iface_wl_region wl_region - * @section page_iface_wl_region_desc Description - * - * A region object describes an area. - * - * Region objects are used to describe the opaque and input - * regions of a surface. - * @section page_iface_wl_region_api API - * See @ref iface_wl_region. - */ -/** - * @defgroup iface_wl_region The wl_region interface - * - * A region object describes an area. - * - * Region objects are used to describe the opaque and input - * regions of a surface. - */ extern const struct wl_interface wl_region_interface; #endif #ifndef WL_SUBCOMPOSITOR_INTERFACE #define WL_SUBCOMPOSITOR_INTERFACE -/** - * @page page_iface_wl_subcompositor wl_subcompositor - * @section page_iface_wl_subcompositor_desc Description - * - * The global interface exposing sub-surface compositing capabilities. - * A wl_surface, that has sub-surfaces associated, is called the - * parent surface. Sub-surfaces can be arbitrarily nested and create - * a tree of sub-surfaces. - * - * The root surface in a tree of sub-surfaces is the main - * surface. The main surface cannot be a sub-surface, because - * sub-surfaces must always have a parent. - * - * A main surface with its sub-surfaces forms a (compound) window. - * For window management purposes, this set of wl_surface objects is - * to be considered as a single window, and it should also behave as - * such. - * - * The aim of sub-surfaces is to offload some of the compositing work - * within a window from clients to the compositor. A prime example is - * a video player with decorations and video in separate wl_surface - * objects. This should allow the compositor to pass YUV video buffer - * processing to dedicated overlay hardware when possible. - * @section page_iface_wl_subcompositor_api API - * See @ref iface_wl_subcompositor. - */ -/** - * @defgroup iface_wl_subcompositor The wl_subcompositor interface - * - * The global interface exposing sub-surface compositing capabilities. - * A wl_surface, that has sub-surfaces associated, is called the - * parent surface. Sub-surfaces can be arbitrarily nested and create - * a tree of sub-surfaces. - * - * The root surface in a tree of sub-surfaces is the main - * surface. The main surface cannot be a sub-surface, because - * sub-surfaces must always have a parent. - * - * A main surface with its sub-surfaces forms a (compound) window. - * For window management purposes, this set of wl_surface objects is - * to be considered as a single window, and it should also behave as - * such. - * - * The aim of sub-surfaces is to offload some of the compositing work - * within a window from clients to the compositor. A prime example is - * a video player with decorations and video in separate wl_surface - * objects. This should allow the compositor to pass YUV video buffer - * processing to dedicated overlay hardware when possible. - */ extern const struct wl_interface wl_subcompositor_interface; #endif #ifndef WL_SUBSURFACE_INTERFACE #define WL_SUBSURFACE_INTERFACE -/** - * @page page_iface_wl_subsurface wl_subsurface - * @section page_iface_wl_subsurface_desc Description - * - * An additional interface to a wl_surface object, which has been - * made a sub-surface. A sub-surface has one parent surface. A - * sub-surface's size and position are not limited to that of the parent. - * Particularly, a sub-surface is not automatically clipped to its - * parent's area. - * - * A sub-surface becomes mapped, when a non-NULL wl_buffer is applied - * and the parent surface is mapped. The order of which one happens - * first is irrelevant. A sub-surface is hidden if the parent becomes - * hidden, or if a NULL wl_buffer is applied. These rules apply - * recursively through the tree of surfaces. - * - * A sub-surface can be in one of two modes. The possible modes are - * synchronized and desynchronized, see methods wl_subsurface.set_sync and - * wl_subsurface.set_desync. - * - * The main surface can be thought to be always in desynchronized mode, - * since it does not have a parent in the sub-surfaces sense. - * - * Even if a sub-surface is in desynchronized mode, it will behave as - * in synchronized mode, if its parent surface behaves as in - * synchronized mode. This rule is applied recursively throughout the - * tree of surfaces. This means, that one can set a sub-surface into - * synchronized mode, and then assume that all its child and grand-child - * sub-surfaces are synchronized, too, without explicitly setting them. - * - * If a surface behaves as in synchronized mode, it is effectively - * synchronized, otherwise it is effectively desynchronized. - * - * A sub-surface is initially in the synchronized mode. - * - * The wl_subsurface interface has requests which modify double-buffered - * state of the parent surface (wl_subsurface.set_position, .place_above and - * .place_below). - * - * Destroying a sub-surface takes effect immediately. If you need to - * synchronize the removal of a sub-surface to the parent surface update, - * unmap the sub-surface first by attaching a NULL wl_buffer, update parent, - * and then destroy the sub-surface. - * - * If the parent wl_surface object is destroyed, the sub-surface is - * unmapped. - * - * A sub-surface never has the keyboard focus of any seat. - * - * The wl_surface.offset request is ignored: clients must use set_position - * instead to move the sub-surface. - * @section page_iface_wl_subsurface_api API - * See @ref iface_wl_subsurface. - */ -/** - * @defgroup iface_wl_subsurface The wl_subsurface interface - * - * An additional interface to a wl_surface object, which has been - * made a sub-surface. A sub-surface has one parent surface. A - * sub-surface's size and position are not limited to that of the parent. - * Particularly, a sub-surface is not automatically clipped to its - * parent's area. - * - * A sub-surface becomes mapped, when a non-NULL wl_buffer is applied - * and the parent surface is mapped. The order of which one happens - * first is irrelevant. A sub-surface is hidden if the parent becomes - * hidden, or if a NULL wl_buffer is applied. These rules apply - * recursively through the tree of surfaces. - * - * A sub-surface can be in one of two modes. The possible modes are - * synchronized and desynchronized, see methods wl_subsurface.set_sync and - * wl_subsurface.set_desync. - * - * The main surface can be thought to be always in desynchronized mode, - * since it does not have a parent in the sub-surfaces sense. - * - * Even if a sub-surface is in desynchronized mode, it will behave as - * in synchronized mode, if its parent surface behaves as in - * synchronized mode. This rule is applied recursively throughout the - * tree of surfaces. This means, that one can set a sub-surface into - * synchronized mode, and then assume that all its child and grand-child - * sub-surfaces are synchronized, too, without explicitly setting them. - * - * If a surface behaves as in synchronized mode, it is effectively - * synchronized, otherwise it is effectively desynchronized. - * - * A sub-surface is initially in the synchronized mode. - * - * The wl_subsurface interface has requests which modify double-buffered - * state of the parent surface (wl_subsurface.set_position, .place_above and - * .place_below). - * - * Destroying a sub-surface takes effect immediately. If you need to - * synchronize the removal of a sub-surface to the parent surface update, - * unmap the sub-surface first by attaching a NULL wl_buffer, update parent, - * and then destroy the sub-surface. - * - * If the parent wl_surface object is destroyed, the sub-surface is - * unmapped. - * - * A sub-surface never has the keyboard focus of any seat. - * - * The wl_surface.offset request is ignored: clients must use set_position - * instead to move the sub-surface. - */ extern const struct wl_interface wl_subsurface_interface; #endif #ifndef WL_FIXES_INTERFACE #define WL_FIXES_INTERFACE -/** - * @page page_iface_wl_fixes wl_fixes - * @section page_iface_wl_fixes_desc Description - * - * This global fixes problems with other core-protocol interfaces that - * cannot be fixed in these interfaces themselves. - * @section page_iface_wl_fixes_api API - * See @ref iface_wl_fixes. - */ -/** - * @defgroup iface_wl_fixes The wl_fixes interface - * - * This global fixes problems with other core-protocol interfaces that - * cannot be fixed in these interfaces themselves. - */ extern const struct wl_interface wl_fixes_interface; #endif #ifndef WL_DISPLAY_ERROR_ENUM #define WL_DISPLAY_ERROR_ENUM -/** - * @ingroup iface_wl_display - * global error values - * - * These errors are global and can be emitted in response to any - * server request. - */ enum wl_display_error { - /** - * server couldn't find object - */ WL_DISPLAY_ERROR_INVALID_OBJECT = 0, - /** - * method doesn't exist on the specified interface or malformed request - */ WL_DISPLAY_ERROR_INVALID_METHOD = 1, - /** - * server is out of memory - */ WL_DISPLAY_ERROR_NO_MEMORY = 2, - /** - * implementation error in compositor - */ WL_DISPLAY_ERROR_IMPLEMENTATION = 3, }; -#endif /* WL_DISPLAY_ERROR_ENUM */ +#endif -/** - * @ingroup iface_wl_display - * @struct wl_display_listener - */ struct wl_display_listener { - /** - * fatal error event - * - * The error event is sent out when a fatal (non-recoverable) - * error has occurred. The object_id argument is the object where - * the error occurred, most often in response to a request to that - * object. The code identifies the error and is defined by the - * object interface. As such, each interface defines its own set of - * error codes. The message is a brief description of the error, - * for (debugging) convenience. - * @param object_id object where the error occurred - * @param code error code - * @param message error description - */ void (*error)(void *data, struct wl_display *wl_display, void *object_id, uint32_t code, const char *message); - /** - * acknowledge object ID deletion - * - * This event is used internally by the object ID management - * logic. - * - * When the server stops using an object created by the client, the - * server sends this event. In particular, after sending this - * event, the server will no longer send any events that contain - * the object as the receiver or as an argument. - * - * When the client receives this event, it knows that it can reuse - * the object ID. - * @param id deleted object ID - */ void (*delete_id)(void *data, struct wl_display *wl_display, uint32_t id); }; -/** - * @ingroup iface_wl_display - */ static inline int wl_display_add_listener(struct wl_display *wl_display, const struct wl_display_listener *listener, void *data) @@ -1039,32 +214,18 @@ wl_display_add_listener(struct wl_display *wl_display, #define WL_DISPLAY_SYNC 0 #define WL_DISPLAY_GET_REGISTRY 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_ERROR_SINCE_VERSION 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_DELETE_ID_SINCE_VERSION 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_SYNC_SINCE_VERSION 1 -/** - * @ingroup iface_wl_display - */ #define WL_DISPLAY_GET_REGISTRY_SINCE_VERSION 1 -/** @ingroup iface_wl_display */ static inline void wl_display_set_user_data(struct wl_display *wl_display, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_display, user_data); } -/** @ingroup iface_wl_display */ static inline void * wl_display_get_user_data(struct wl_display *wl_display) { @@ -1077,21 +238,6 @@ wl_display_get_version(struct wl_display *wl_display) return wl_proxy_get_version((struct wl_proxy *) wl_display); } -/** - * @ingroup iface_wl_display - * - * The sync request asks the server to emit the 'done' event - * on the returned wl_callback object. Since requests are - * handled in-order and events are delivered in-order, this can - * be used as a barrier to ensure all previous requests and the - * resulting events have been handled. - * - * The object returned by this request will be destroyed by the - * compositor after the callback is fired and as such the client must not - * attempt to use it after that point. - * - * The callback_data passed in the callback is undefined and should be ignored. - */ static inline struct wl_callback * wl_display_sync(struct wl_display *wl_display) { @@ -1103,19 +249,6 @@ wl_display_sync(struct wl_display *wl_display) return (struct wl_callback *) callback; } -/** - * @ingroup iface_wl_display - * - * This request creates a registry object that allows the client - * to list and bind the global objects available from the - * compositor. - * - * It should be noted that the server side resources consumed in - * response to a get_registry request can only be released when the - * client disconnects, not when the client side proxy is destroyed. - * Therefore, clients should invoke get_registry as infrequently as - * possible to avoid wasting memory. - */ static inline struct wl_registry * wl_display_get_registry(struct wl_display *wl_display) { @@ -1127,51 +260,17 @@ wl_display_get_registry(struct wl_display *wl_display) return (struct wl_registry *) registry; } -/** - * @ingroup iface_wl_registry - * @struct wl_registry_listener - */ struct wl_registry_listener { - /** - * announce global object - * - * Notify the client of global objects. - * - * The event notifies the client that a global object with the - * given name is now available, and it implements the given version - * of the given interface. - * @param name numeric name of the global object - * @param interface interface implemented by the object - * @param version interface version - */ void (*global)(void *data, struct wl_registry *wl_registry, uint32_t name, const char *interface, uint32_t version); - /** - * announce removal of global object - * - * Notify the client of removed global objects. - * - * This event notifies the client that the global identified by - * name is no longer available. If the client bound to the global - * using the bind request, the client should now destroy that - * object. - * - * The object remains valid and requests to the object will be - * ignored until the client destroys it, to avoid races between the - * global going away and a client sending a request to it. - * @param name numeric name of the global object - */ void (*global_remove)(void *data, struct wl_registry *wl_registry, uint32_t name); }; -/** - * @ingroup iface_wl_registry - */ static inline int wl_registry_add_listener(struct wl_registry *wl_registry, const struct wl_registry_listener *listener, void *data) @@ -1182,28 +281,17 @@ wl_registry_add_listener(struct wl_registry *wl_registry, #define WL_REGISTRY_BIND 0 -/** - * @ingroup iface_wl_registry - */ #define WL_REGISTRY_GLOBAL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_registry - */ #define WL_REGISTRY_GLOBAL_REMOVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_registry - */ #define WL_REGISTRY_BIND_SINCE_VERSION 1 -/** @ingroup iface_wl_registry */ static inline void wl_registry_set_user_data(struct wl_registry *wl_registry, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_registry, user_data); } -/** @ingroup iface_wl_registry */ static inline void * wl_registry_get_user_data(struct wl_registry *wl_registry) { @@ -1216,19 +304,12 @@ wl_registry_get_version(struct wl_registry *wl_registry) return wl_proxy_get_version((struct wl_proxy *) wl_registry); } -/** @ingroup iface_wl_registry */ static inline void wl_registry_destroy(struct wl_registry *wl_registry) { wl_proxy_destroy((struct wl_proxy *) wl_registry); } -/** - * @ingroup iface_wl_registry - * - * Binds a new, client-created object to the server using the - * specified name as the identifier. - */ static inline void * wl_registry_bind(struct wl_registry *wl_registry, uint32_t name, const struct wl_interface *interface, uint32_t version) { @@ -1240,25 +321,12 @@ wl_registry_bind(struct wl_registry *wl_registry, uint32_t name, const struct wl return (void *) id; } -/** - * @ingroup iface_wl_callback - * @struct wl_callback_listener - */ struct wl_callback_listener { - /** - * done event - * - * Notify the client when the related request is done. - * @param callback_data request-specific data for the callback - */ void (*done)(void *data, struct wl_callback *wl_callback, uint32_t callback_data); }; -/** - * @ingroup iface_wl_callback - */ static inline int wl_callback_add_listener(struct wl_callback *wl_callback, const struct wl_callback_listener *listener, void *data) @@ -1267,20 +335,15 @@ wl_callback_add_listener(struct wl_callback *wl_callback, (void (**)(void)) listener, data); } -/** - * @ingroup iface_wl_callback - */ #define WL_CALLBACK_DONE_SINCE_VERSION 1 -/** @ingroup iface_wl_callback */ static inline void wl_callback_set_user_data(struct wl_callback *wl_callback, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_callback, user_data); } -/** @ingroup iface_wl_callback */ static inline void * wl_callback_get_user_data(struct wl_callback *wl_callback) { @@ -1293,7 +356,6 @@ wl_callback_get_version(struct wl_callback *wl_callback) return wl_proxy_get_version((struct wl_proxy *) wl_callback); } -/** @ingroup iface_wl_callback */ static inline void wl_callback_destroy(struct wl_callback *wl_callback) { @@ -1305,27 +367,16 @@ wl_callback_destroy(struct wl_callback *wl_callback) #define WL_COMPOSITOR_RELEASE 2 -/** - * @ingroup iface_wl_compositor - */ #define WL_COMPOSITOR_CREATE_SURFACE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_compositor - */ #define WL_COMPOSITOR_CREATE_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_compositor - */ #define WL_COMPOSITOR_RELEASE_SINCE_VERSION 7 -/** @ingroup iface_wl_compositor */ static inline void wl_compositor_set_user_data(struct wl_compositor *wl_compositor, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_compositor, user_data); } -/** @ingroup iface_wl_compositor */ static inline void * wl_compositor_get_user_data(struct wl_compositor *wl_compositor) { @@ -1338,18 +389,12 @@ wl_compositor_get_version(struct wl_compositor *wl_compositor) return wl_proxy_get_version((struct wl_proxy *) wl_compositor); } -/** @ingroup iface_wl_compositor */ static inline void wl_compositor_destroy(struct wl_compositor *wl_compositor) { wl_proxy_destroy((struct wl_proxy *) wl_compositor); } -/** - * @ingroup iface_wl_compositor - * - * Ask the compositor to create a new surface. - */ static inline struct wl_surface * wl_compositor_create_surface(struct wl_compositor *wl_compositor) { @@ -1361,11 +406,6 @@ wl_compositor_create_surface(struct wl_compositor *wl_compositor) return (struct wl_surface *) id; } -/** - * @ingroup iface_wl_compositor - * - * Ask the compositor to create a new region. - */ static inline struct wl_region * wl_compositor_create_region(struct wl_compositor *wl_compositor) { @@ -1377,11 +417,6 @@ wl_compositor_create_region(struct wl_compositor *wl_compositor) return (struct wl_region *) id; } -/** - * @ingroup iface_wl_compositor - * - * This request destroys the wl_compositor. This has no effect on any other objects. - */ static inline void wl_compositor_release(struct wl_compositor *wl_compositor) { @@ -1391,50 +426,27 @@ wl_compositor_release(struct wl_compositor *wl_compositor) #ifndef WL_SHM_POOL_ERROR_ENUM #define WL_SHM_POOL_ERROR_ENUM -/** - * @ingroup iface_wl_shm_pool - * wl_shm_pool error values - * - * These errors can be emitted in response to wl_shm_pool requests. - */ enum wl_shm_pool_error { - /** - * buffer format is not known - */ WL_SHM_POOL_ERROR_INVALID_FORMAT = 0, - /** - * invalid size or stride during buffer creation - */ WL_SHM_POOL_ERROR_INVALID_STRIDE = 1, }; -#endif /* WL_SHM_POOL_ERROR_ENUM */ +#endif #define WL_SHM_POOL_CREATE_BUFFER 0 #define WL_SHM_POOL_DESTROY 1 #define WL_SHM_POOL_RESIZE 2 -/** - * @ingroup iface_wl_shm_pool - */ #define WL_SHM_POOL_CREATE_BUFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm_pool - */ #define WL_SHM_POOL_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm_pool - */ #define WL_SHM_POOL_RESIZE_SINCE_VERSION 1 -/** @ingroup iface_wl_shm_pool */ static inline void wl_shm_pool_set_user_data(struct wl_shm_pool *wl_shm_pool, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_shm_pool, user_data); } -/** @ingroup iface_wl_shm_pool */ static inline void * wl_shm_pool_get_user_data(struct wl_shm_pool *wl_shm_pool) { @@ -1447,21 +459,6 @@ wl_shm_pool_get_version(struct wl_shm_pool *wl_shm_pool) return wl_proxy_get_version((struct wl_proxy *) wl_shm_pool); } -/** - * @ingroup iface_wl_shm_pool - * - * Create a wl_buffer object from the pool. - * - * The buffer is created offset bytes into the pool and has - * width and height as specified. The stride argument specifies - * the number of bytes from the beginning of one row to the beginning - * of the next. The format is the pixel format of the buffer and - * must be one of those advertised through the wl_shm.format event. - * - * A buffer will keep a reference to the pool it was created from - * so it is valid to destroy the pool immediately after creating - * a buffer from it. - */ static inline struct wl_buffer * wl_shm_pool_create_buffer(struct wl_shm_pool *wl_shm_pool, int32_t offset, int32_t width, int32_t height, int32_t stride, uint32_t format) { @@ -1473,15 +470,6 @@ wl_shm_pool_create_buffer(struct wl_shm_pool *wl_shm_pool, int32_t offset, int32 return (struct wl_buffer *) id; } -/** - * @ingroup iface_wl_shm_pool - * - * Destroy the shared memory pool. - * - * The mmapped memory will be released when all - * buffers that have been created from this pool - * are gone. - */ static inline void wl_shm_pool_destroy(struct wl_shm_pool *wl_shm_pool) { @@ -1489,20 +477,6 @@ wl_shm_pool_destroy(struct wl_shm_pool *wl_shm_pool) WL_SHM_POOL_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shm_pool), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_shm_pool - * - * This request will cause the server to remap the backing memory - * for the pool from the file descriptor passed when the pool was - * created, but using the new size. This request can only be - * used to make the pool bigger. - * - * This request only changes the amount of bytes that are mmapped - * by the server and does not touch the file corresponding to the - * file descriptor passed at creation time. It is the client's - * responsibility to ensure that the file is at least as big as - * the new pool size. - */ static inline void wl_shm_pool_resize(struct wl_shm_pool *wl_shm_pool, int32_t size) { @@ -1512,385 +486,99 @@ wl_shm_pool_resize(struct wl_shm_pool *wl_shm_pool, int32_t size) #ifndef WL_SHM_ERROR_ENUM #define WL_SHM_ERROR_ENUM -/** - * @ingroup iface_wl_shm - * wl_shm error values - * - * These errors can be emitted in response to wl_shm requests. - */ enum wl_shm_error { - /** - * buffer format is not known - * @deprecated Deprecated since version 3 - */ WL_SHM_ERROR_INVALID_FORMAT = 0, - /** - * invalid size or stride during pool creation - */ WL_SHM_ERROR_INVALID_STRIDE = 1, - /** - * mmapping the file descriptor failed - */ WL_SHM_ERROR_INVALID_FD = 2, }; -#endif /* WL_SHM_ERROR_ENUM */ +#endif #ifndef WL_SHM_FORMAT_ENUM #define WL_SHM_FORMAT_ENUM -/** - * @ingroup iface_wl_shm - * pixel formats - * - * This describes the memory layout of an individual pixel. - * - * All renderers should support argb8888 and xrgb8888 but any other - * formats are optional and may not be supported by the particular - * renderer in use. - * - * The drm format codes match the macros defined in drm_fourcc.h, except - * argb8888 and xrgb8888. The formats actually supported by the compositor - * will be reported by the format event. See drm_fourcc.h for more detailed - * format descriptions. - * - * For all wl_shm formats and unless specified in another protocol - * extension, pre-multiplied alpha is used for pixel values. - */ enum wl_shm_format { - /** - * 32-bit ARGB format, [31:0] A:R:G:B 8:8:8:8 little endian - */ WL_SHM_FORMAT_ARGB8888 = 0, - /** - * 32-bit RGB format, [31:0] x:R:G:B 8:8:8:8 little endian - */ WL_SHM_FORMAT_XRGB8888 = 1, - /** - * 8-bit color index format, [7:0] C - */ WL_SHM_FORMAT_C8 = 0x20203843, - /** - * 8-bit RGB format, [7:0] R:G:B 3:3:2 - */ WL_SHM_FORMAT_RGB332 = 0x38424752, - /** - * 8-bit BGR format, [7:0] B:G:R 2:3:3 - */ WL_SHM_FORMAT_BGR233 = 0x38524742, - /** - * 16-bit xRGB format, [15:0] x:R:G:B 4:4:4:4 little endian - */ WL_SHM_FORMAT_XRGB4444 = 0x32315258, - /** - * 16-bit xBGR format, [15:0] x:B:G:R 4:4:4:4 little endian - */ WL_SHM_FORMAT_XBGR4444 = 0x32314258, - /** - * 16-bit RGBx format, [15:0] R:G:B:x 4:4:4:4 little endian - */ WL_SHM_FORMAT_RGBX4444 = 0x32315852, - /** - * 16-bit BGRx format, [15:0] B:G:R:x 4:4:4:4 little endian - */ WL_SHM_FORMAT_BGRX4444 = 0x32315842, - /** - * 16-bit ARGB format, [15:0] A:R:G:B 4:4:4:4 little endian - */ WL_SHM_FORMAT_ARGB4444 = 0x32315241, - /** - * 16-bit ABGR format, [15:0] A:B:G:R 4:4:4:4 little endian - */ WL_SHM_FORMAT_ABGR4444 = 0x32314241, - /** - * 16-bit RGBA format, [15:0] R:G:B:A 4:4:4:4 little endian - */ WL_SHM_FORMAT_RGBA4444 = 0x32314152, - /** - * 16-bit BGRA format, [15:0] B:G:R:A 4:4:4:4 little endian - */ WL_SHM_FORMAT_BGRA4444 = 0x32314142, - /** - * 16-bit xRGB format, [15:0] x:R:G:B 1:5:5:5 little endian - */ WL_SHM_FORMAT_XRGB1555 = 0x35315258, - /** - * 16-bit xBGR 1555 format, [15:0] x:B:G:R 1:5:5:5 little endian - */ WL_SHM_FORMAT_XBGR1555 = 0x35314258, - /** - * 16-bit RGBx 5551 format, [15:0] R:G:B:x 5:5:5:1 little endian - */ WL_SHM_FORMAT_RGBX5551 = 0x35315852, - /** - * 16-bit BGRx 5551 format, [15:0] B:G:R:x 5:5:5:1 little endian - */ WL_SHM_FORMAT_BGRX5551 = 0x35315842, - /** - * 16-bit ARGB 1555 format, [15:0] A:R:G:B 1:5:5:5 little endian - */ WL_SHM_FORMAT_ARGB1555 = 0x35315241, - /** - * 16-bit ABGR 1555 format, [15:0] A:B:G:R 1:5:5:5 little endian - */ WL_SHM_FORMAT_ABGR1555 = 0x35314241, - /** - * 16-bit RGBA 5551 format, [15:0] R:G:B:A 5:5:5:1 little endian - */ WL_SHM_FORMAT_RGBA5551 = 0x35314152, - /** - * 16-bit BGRA 5551 format, [15:0] B:G:R:A 5:5:5:1 little endian - */ WL_SHM_FORMAT_BGRA5551 = 0x35314142, - /** - * 16-bit RGB 565 format, [15:0] R:G:B 5:6:5 little endian - */ WL_SHM_FORMAT_RGB565 = 0x36314752, - /** - * 16-bit BGR 565 format, [15:0] B:G:R 5:6:5 little endian - */ WL_SHM_FORMAT_BGR565 = 0x36314742, - /** - * 24-bit RGB format, [23:0] R:G:B little endian - */ WL_SHM_FORMAT_RGB888 = 0x34324752, - /** - * 24-bit BGR format, [23:0] B:G:R little endian - */ WL_SHM_FORMAT_BGR888 = 0x34324742, - /** - * 32-bit xBGR format, [31:0] x:B:G:R 8:8:8:8 little endian - */ WL_SHM_FORMAT_XBGR8888 = 0x34324258, - /** - * 32-bit RGBx format, [31:0] R:G:B:x 8:8:8:8 little endian - */ WL_SHM_FORMAT_RGBX8888 = 0x34325852, - /** - * 32-bit BGRx format, [31:0] B:G:R:x 8:8:8:8 little endian - */ WL_SHM_FORMAT_BGRX8888 = 0x34325842, - /** - * 32-bit ABGR format, [31:0] A:B:G:R 8:8:8:8 little endian - */ WL_SHM_FORMAT_ABGR8888 = 0x34324241, - /** - * 32-bit RGBA format, [31:0] R:G:B:A 8:8:8:8 little endian - */ WL_SHM_FORMAT_RGBA8888 = 0x34324152, - /** - * 32-bit BGRA format, [31:0] B:G:R:A 8:8:8:8 little endian - */ WL_SHM_FORMAT_BGRA8888 = 0x34324142, - /** - * 32-bit xRGB format, [31:0] x:R:G:B 2:10:10:10 little endian - */ WL_SHM_FORMAT_XRGB2101010 = 0x30335258, - /** - * 32-bit xBGR format, [31:0] x:B:G:R 2:10:10:10 little endian - */ WL_SHM_FORMAT_XBGR2101010 = 0x30334258, - /** - * 32-bit RGBx format, [31:0] R:G:B:x 10:10:10:2 little endian - */ WL_SHM_FORMAT_RGBX1010102 = 0x30335852, - /** - * 32-bit BGRx format, [31:0] B:G:R:x 10:10:10:2 little endian - */ WL_SHM_FORMAT_BGRX1010102 = 0x30335842, - /** - * 32-bit ARGB format, [31:0] A:R:G:B 2:10:10:10 little endian - */ WL_SHM_FORMAT_ARGB2101010 = 0x30335241, - /** - * 32-bit ABGR format, [31:0] A:B:G:R 2:10:10:10 little endian - */ WL_SHM_FORMAT_ABGR2101010 = 0x30334241, - /** - * 32-bit RGBA format, [31:0] R:G:B:A 10:10:10:2 little endian - */ WL_SHM_FORMAT_RGBA1010102 = 0x30334152, - /** - * 32-bit BGRA format, [31:0] B:G:R:A 10:10:10:2 little endian - */ WL_SHM_FORMAT_BGRA1010102 = 0x30334142, - /** - * packed YCbCr format, [31:0] Cr0:Y1:Cb0:Y0 8:8:8:8 little endian - */ WL_SHM_FORMAT_YUYV = 0x56595559, - /** - * packed YCbCr format, [31:0] Cb0:Y1:Cr0:Y0 8:8:8:8 little endian - */ WL_SHM_FORMAT_YVYU = 0x55595659, - /** - * packed YCbCr format, [31:0] Y1:Cr0:Y0:Cb0 8:8:8:8 little endian - */ WL_SHM_FORMAT_UYVY = 0x59565955, - /** - * packed YCbCr format, [31:0] Y1:Cb0:Y0:Cr0 8:8:8:8 little endian - */ WL_SHM_FORMAT_VYUY = 0x59555956, - /** - * packed AYCbCr format, [31:0] A:Y:Cb:Cr 8:8:8:8 little endian - */ WL_SHM_FORMAT_AYUV = 0x56555941, - /** - * 2 plane YCbCr Cr:Cb format, 2x2 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV12 = 0x3231564e, - /** - * 2 plane YCbCr Cb:Cr format, 2x2 subsampled Cb:Cr plane - */ WL_SHM_FORMAT_NV21 = 0x3132564e, - /** - * 2 plane YCbCr Cr:Cb format, 2x1 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV16 = 0x3631564e, - /** - * 2 plane YCbCr Cb:Cr format, 2x1 subsampled Cb:Cr plane - */ WL_SHM_FORMAT_NV61 = 0x3136564e, - /** - * 3 plane YCbCr format, 4x4 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV410 = 0x39565559, - /** - * 3 plane YCbCr format, 4x4 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU410 = 0x39555659, - /** - * 3 plane YCbCr format, 4x1 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV411 = 0x31315559, - /** - * 3 plane YCbCr format, 4x1 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU411 = 0x31315659, - /** - * 3 plane YCbCr format, 2x2 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV420 = 0x32315559, - /** - * 3 plane YCbCr format, 2x2 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU420 = 0x32315659, - /** - * 3 plane YCbCr format, 2x1 subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV422 = 0x36315559, - /** - * 3 plane YCbCr format, 2x1 subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU422 = 0x36315659, - /** - * 3 plane YCbCr format, non-subsampled Cb (1) and Cr (2) planes - */ WL_SHM_FORMAT_YUV444 = 0x34325559, - /** - * 3 plane YCbCr format, non-subsampled Cr (1) and Cb (2) planes - */ WL_SHM_FORMAT_YVU444 = 0x34325659, - /** - * [7:0] R - */ WL_SHM_FORMAT_R8 = 0x20203852, - /** - * [15:0] R little endian - */ WL_SHM_FORMAT_R16 = 0x20363152, - /** - * [15:0] R:G 8:8 little endian - */ WL_SHM_FORMAT_RG88 = 0x38384752, - /** - * [15:0] G:R 8:8 little endian - */ WL_SHM_FORMAT_GR88 = 0x38385247, - /** - * [31:0] R:G 16:16 little endian - */ WL_SHM_FORMAT_RG1616 = 0x32334752, - /** - * [31:0] G:R 16:16 little endian - */ WL_SHM_FORMAT_GR1616 = 0x32335247, - /** - * [63:0] x:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_XRGB16161616F = 0x48345258, - /** - * [63:0] x:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_XBGR16161616F = 0x48344258, - /** - * [63:0] A:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_ARGB16161616F = 0x48345241, - /** - * [63:0] A:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_ABGR16161616F = 0x48344241, - /** - * [31:0] X:Y:Cb:Cr 8:8:8:8 little endian - */ WL_SHM_FORMAT_XYUV8888 = 0x56555958, - /** - * [23:0] Cr:Cb:Y 8:8:8 little endian - */ WL_SHM_FORMAT_VUY888 = 0x34325556, - /** - * Y followed by U then V, 10:10:10. Non-linear modifier only - */ WL_SHM_FORMAT_VUY101010 = 0x30335556, - /** - * [63:0] Cr0:0:Y1:0:Cb0:0:Y0:0 10:6:10:6:10:6:10:6 little endian per 2 Y pixels - */ WL_SHM_FORMAT_Y210 = 0x30313259, - /** - * [63:0] Cr0:0:Y1:0:Cb0:0:Y0:0 12:4:12:4:12:4:12:4 little endian per 2 Y pixels - */ WL_SHM_FORMAT_Y212 = 0x32313259, - /** - * [63:0] Cr0:Y1:Cb0:Y0 16:16:16:16 little endian per 2 Y pixels - */ WL_SHM_FORMAT_Y216 = 0x36313259, - /** - * [31:0] A:Cr:Y:Cb 2:10:10:10 little endian - */ WL_SHM_FORMAT_Y410 = 0x30313459, - /** - * [63:0] A:0:Cr:0:Y:0:Cb:0 12:4:12:4:12:4:12:4 little endian - */ WL_SHM_FORMAT_Y412 = 0x32313459, - /** - * [63:0] A:Cr:Y:Cb 16:16:16:16 little endian - */ WL_SHM_FORMAT_Y416 = 0x36313459, - /** - * [31:0] X:Cr:Y:Cb 2:10:10:10 little endian - */ WL_SHM_FORMAT_XVYU2101010 = 0x30335658, - /** - * [63:0] X:0:Cr:0:Y:0:Cb:0 12:4:12:4:12:4:12:4 little endian - */ WL_SHM_FORMAT_XVYU12_16161616 = 0x36335658, - /** - * [63:0] X:Cr:Y:Cb 16:16:16:16 little endian - */ WL_SHM_FORMAT_XVYU16161616 = 0x38345658, - /** - * [63:0] A3:A2:Y3:0:Cr0:0:Y2:0:A1:A0:Y1:0:Cb0:0:Y0:0 1:1:8:2:8:2:8:2:1:1:8:2:8:2:8:2 little endian - */ WL_SHM_FORMAT_Y0L0 = 0x304c3059, - /** - * [63:0] X3:X2:Y3:0:Cr0:0:Y2:0:X1:X0:Y1:0:Cb0:0:Y0:0 1:1:8:2:8:2:8:2:1:1:8:2:8:2:8:2 little endian - */ WL_SHM_FORMAT_X0L0 = 0x304c3058, - /** - * [63:0] A3:A2:Y3:Cr0:Y2:A1:A0:Y1:Cb0:Y0 1:1:10:10:10:1:1:10:10:10 little endian - */ WL_SHM_FORMAT_Y0L2 = 0x324c3059, - /** - * [63:0] X3:X2:Y3:Cr0:Y2:X1:X0:Y1:Cb0:Y0 1:1:10:10:10:1:1:10:10:10 little endian - */ WL_SHM_FORMAT_X0L2 = 0x324c3058, WL_SHM_FORMAT_YUV420_8BIT = 0x38305559, WL_SHM_FORMAT_YUV420_10BIT = 0x30315559, @@ -1902,241 +590,69 @@ enum wl_shm_format { WL_SHM_FORMAT_BGR888_A8 = 0x38413842, WL_SHM_FORMAT_RGB565_A8 = 0x38413552, WL_SHM_FORMAT_BGR565_A8 = 0x38413542, - /** - * non-subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV24 = 0x3432564e, - /** - * non-subsampled Cb:Cr plane - */ WL_SHM_FORMAT_NV42 = 0x3234564e, - /** - * 2x1 subsampled Cr:Cb plane, 10 bit per channel - */ WL_SHM_FORMAT_P210 = 0x30313250, - /** - * 2x2 subsampled Cr:Cb plane 10 bits per channel - */ WL_SHM_FORMAT_P010 = 0x30313050, - /** - * 2x2 subsampled Cr:Cb plane 12 bits per channel - */ WL_SHM_FORMAT_P012 = 0x32313050, - /** - * 2x2 subsampled Cr:Cb plane 16 bits per channel - */ WL_SHM_FORMAT_P016 = 0x36313050, - /** - * [63:0] A:x:B:x:G:x:R:x 10:6:10:6:10:6:10:6 little endian - */ WL_SHM_FORMAT_AXBXGXRX106106106106 = 0x30314241, - /** - * 2x2 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV15 = 0x3531564e, WL_SHM_FORMAT_Q410 = 0x30313451, WL_SHM_FORMAT_Q401 = 0x31303451, - /** - * [63:0] x:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_XRGB16161616 = 0x38345258, - /** - * [63:0] x:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_XBGR16161616 = 0x38344258, - /** - * [63:0] A:R:G:B 16:16:16:16 little endian - */ WL_SHM_FORMAT_ARGB16161616 = 0x38345241, - /** - * [63:0] A:B:G:R 16:16:16:16 little endian - */ WL_SHM_FORMAT_ABGR16161616 = 0x38344241, - /** - * [7:0] C0:C1:C2:C3:C4:C5:C6:C7 1:1:1:1:1:1:1:1 eight pixels/byte - */ WL_SHM_FORMAT_C1 = 0x20203143, - /** - * [7:0] C0:C1:C2:C3 2:2:2:2 four pixels/byte - */ WL_SHM_FORMAT_C2 = 0x20203243, - /** - * [7:0] C0:C1 4:4 two pixels/byte - */ WL_SHM_FORMAT_C4 = 0x20203443, - /** - * [7:0] D0:D1:D2:D3:D4:D5:D6:D7 1:1:1:1:1:1:1:1 eight pixels/byte - */ WL_SHM_FORMAT_D1 = 0x20203144, - /** - * [7:0] D0:D1:D2:D3 2:2:2:2 four pixels/byte - */ WL_SHM_FORMAT_D2 = 0x20203244, - /** - * [7:0] D0:D1 4:4 two pixels/byte - */ WL_SHM_FORMAT_D4 = 0x20203444, - /** - * [7:0] D - */ WL_SHM_FORMAT_D8 = 0x20203844, - /** - * [7:0] R0:R1:R2:R3:R4:R5:R6:R7 1:1:1:1:1:1:1:1 eight pixels/byte - */ WL_SHM_FORMAT_R1 = 0x20203152, - /** - * [7:0] R0:R1:R2:R3 2:2:2:2 four pixels/byte - */ WL_SHM_FORMAT_R2 = 0x20203252, - /** - * [7:0] R0:R1 4:4 two pixels/byte - */ WL_SHM_FORMAT_R4 = 0x20203452, - /** - * [15:0] x:R 6:10 little endian - */ WL_SHM_FORMAT_R10 = 0x20303152, - /** - * [15:0] x:R 4:12 little endian - */ WL_SHM_FORMAT_R12 = 0x20323152, - /** - * [31:0] A:Cr:Cb:Y 8:8:8:8 little endian - */ WL_SHM_FORMAT_AVUY8888 = 0x59555641, - /** - * [31:0] X:Cr:Cb:Y 8:8:8:8 little endian - */ WL_SHM_FORMAT_XVUY8888 = 0x59555658, - /** - * 2x2 subsampled Cr:Cb plane 10 bits per channel packed - */ WL_SHM_FORMAT_P030 = 0x30333050, - /** - * [47:0] R:G:B 16:16:16 little endian - */ WL_SHM_FORMAT_RGB161616 = 0x38344752, - /** - * [47:0] B:G:R 16:16:16 little endian - */ WL_SHM_FORMAT_BGR161616 = 0x38344742, - /** - * [15:0] R 16 little endian - */ WL_SHM_FORMAT_R16F = 0x48202052, - /** - * [31:0] G:R 16:16 little endian - */ WL_SHM_FORMAT_GR1616F = 0x48205247, - /** - * [47:0] B:G:R 16:16:16 little endian - */ WL_SHM_FORMAT_BGR161616F = 0x48524742, - /** - * [31:0] R 32 little endian - */ WL_SHM_FORMAT_R32F = 0x46202052, - /** - * [63:0] G:R 32:32 little endian - */ WL_SHM_FORMAT_GR3232F = 0x46205247, - /** - * [95:0] B:G:R 32:32:32 little endian - */ WL_SHM_FORMAT_BGR323232F = 0x46524742, - /** - * [127:0] A:B:G:R 32:32:32:32 little endian - */ WL_SHM_FORMAT_ABGR32323232F = 0x46384241, - /** - * 2x1 subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV20 = 0x3032564e, - /** - * non-subsampled Cr:Cb plane - */ WL_SHM_FORMAT_NV30 = 0x3033564e, - /** - * 2x2 subsampled Cb (1) and Cr (2) planes 10 bits per channel - */ WL_SHM_FORMAT_S010 = 0x30313053, - /** - * 2x1 subsampled Cb (1) and Cr (2) planes 10 bits per channel - */ WL_SHM_FORMAT_S210 = 0x30313253, - /** - * non-subsampled Cb (1) and Cr (2) planes 10 bits per channel - */ WL_SHM_FORMAT_S410 = 0x30313453, - /** - * 2x2 subsampled Cb (1) and Cr (2) planes 12 bits per channel - */ WL_SHM_FORMAT_S012 = 0x32313053, - /** - * 2x1 subsampled Cb (1) and Cr (2) planes 12 bits per channel - */ WL_SHM_FORMAT_S212 = 0x32313253, - /** - * non-subsampled Cb (1) and Cr (2) planes 12 bits per channel - */ WL_SHM_FORMAT_S412 = 0x32313453, - /** - * 2x2 subsampled Cb (1) and Cr (2) planes 16 bits per channel - */ WL_SHM_FORMAT_S016 = 0x36313053, - /** - * 2x1 subsampled Cb (1) and Cr (2) planes 16 bits per channel - */ WL_SHM_FORMAT_S216 = 0x36313253, - /** - * non-subsampled Cb (1) and Cr (2) planes 16 bits per channel - */ WL_SHM_FORMAT_S416 = 0x36313453, - /** - * [31:0] x:Cr:Cb:Y 2:10:10:10 little endian - */ WL_SHM_FORMAT_XVUY2101010 = 0x30335958, - /** - * 2x1 subsampled Cr:Cb plane 10 bits per channel packed - */ WL_SHM_FORMAT_P230 = 0x30333250, WL_SHM_FORMAT_T430 = 0x30333454, - /** - * 8-bit Y-only - */ WL_SHM_FORMAT_Y8 = 0x59455247, - /** - * [31:0] x:Y2:Y1:Y0 2:10:10:10 little endian - */ WL_SHM_FORMAT_XYYY2101010 = 0x34415059, }; -#endif /* WL_SHM_FORMAT_ENUM */ +#endif -/** - * @ingroup iface_wl_shm - * @struct wl_shm_listener - */ struct wl_shm_listener { - /** - * pixel format description - * - * Informs the client about a valid pixel format that can be used - * for buffers. Known formats include argb8888 and xrgb8888. - * - * Extensions to drm_fourcc.h (or the format enum) do not require - * increasing the wl_shm version; as a result, clients may receive - * format codes which were not in the list at the time the client - * was made. - * @param format buffer pixel format - */ void (*format)(void *data, struct wl_shm *wl_shm, uint32_t format); }; -/** - * @ingroup iface_wl_shm - */ static inline int wl_shm_add_listener(struct wl_shm *wl_shm, const struct wl_shm_listener *listener, void *data) @@ -2148,28 +664,17 @@ wl_shm_add_listener(struct wl_shm *wl_shm, #define WL_SHM_CREATE_POOL 0 #define WL_SHM_RELEASE 1 -/** - * @ingroup iface_wl_shm - */ #define WL_SHM_FORMAT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm - */ #define WL_SHM_CREATE_POOL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shm - */ #define WL_SHM_RELEASE_SINCE_VERSION 2 -/** @ingroup iface_wl_shm */ static inline void wl_shm_set_user_data(struct wl_shm *wl_shm, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_shm, user_data); } -/** @ingroup iface_wl_shm */ static inline void * wl_shm_get_user_data(struct wl_shm *wl_shm) { @@ -2182,22 +687,12 @@ wl_shm_get_version(struct wl_shm *wl_shm) return wl_proxy_get_version((struct wl_proxy *) wl_shm); } -/** @ingroup iface_wl_shm */ static inline void wl_shm_destroy(struct wl_shm *wl_shm) { wl_proxy_destroy((struct wl_proxy *) wl_shm); } -/** - * @ingroup iface_wl_shm - * - * Create a new wl_shm_pool object. - * - * The pool can be used to create shared memory based buffer - * objects. The server will mmap size bytes of the passed file - * descriptor, to use as backing memory for the pool. - */ static inline struct wl_shm_pool * wl_shm_create_pool(struct wl_shm *wl_shm, int32_t fd, int32_t size) { @@ -2209,14 +704,6 @@ wl_shm_create_pool(struct wl_shm *wl_shm, int32_t fd, int32_t size) return (struct wl_shm_pool *) id; } -/** - * @ingroup iface_wl_shm - * - * Using this request a client can tell the server that it is not going to - * use the shm object anymore. - * - * Objects created via this interface remain unaffected. - */ static inline void wl_shm_release(struct wl_shm *wl_shm) { @@ -2224,36 +711,11 @@ wl_shm_release(struct wl_shm *wl_shm) WL_SHM_RELEASE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shm), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_buffer - * @struct wl_buffer_listener - */ struct wl_buffer_listener { - /** - * compositor releases buffer - * - * Sent when this wl_buffer is no longer used by the compositor. - * - * For more information on when release events may or may not be - * sent, and what consequences it has, please see the description - * of wl_surface.attach. - * - * If a client receives a release event before the frame callback - * requested in the same wl_surface.commit that attaches this - * wl_buffer to a surface, then the client is immediately free to - * reuse the buffer and its backing storage, and does not need a - * second buffer for the next surface content update. Typically - * this is possible, when the compositor maintains a copy of the - * wl_surface contents, e.g. as a GL texture. This is an important - * optimization for GL(ES) compositors with wl_shm clients. - */ void (*release)(void *data, struct wl_buffer *wl_buffer); }; -/** - * @ingroup iface_wl_buffer - */ static inline int wl_buffer_add_listener(struct wl_buffer *wl_buffer, const struct wl_buffer_listener *listener, void *data) @@ -2264,24 +726,16 @@ wl_buffer_add_listener(struct wl_buffer *wl_buffer, #define WL_BUFFER_DESTROY 0 -/** - * @ingroup iface_wl_buffer - */ #define WL_BUFFER_RELEASE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_buffer - */ #define WL_BUFFER_DESTROY_SINCE_VERSION 1 -/** @ingroup iface_wl_buffer */ static inline void wl_buffer_set_user_data(struct wl_buffer *wl_buffer, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_buffer, user_data); } -/** @ingroup iface_wl_buffer */ static inline void * wl_buffer_get_user_data(struct wl_buffer *wl_buffer) { @@ -2294,14 +748,6 @@ wl_buffer_get_version(struct wl_buffer *wl_buffer) return wl_proxy_get_version((struct wl_proxy *) wl_buffer); } -/** - * @ingroup iface_wl_buffer - * - * Destroy a buffer. If and how you need to release the backing - * storage is defined by the buffer factory interface. - * - * For possible side-effects to a surface, see wl_surface.attach. - */ static inline void wl_buffer_destroy(struct wl_buffer *wl_buffer) { @@ -2312,104 +758,25 @@ wl_buffer_destroy(struct wl_buffer *wl_buffer) #ifndef WL_DATA_OFFER_ERROR_ENUM #define WL_DATA_OFFER_ERROR_ENUM enum wl_data_offer_error { - /** - * finish request was called untimely - */ WL_DATA_OFFER_ERROR_INVALID_FINISH = 0, - /** - * action mask contains invalid values - */ WL_DATA_OFFER_ERROR_INVALID_ACTION_MASK = 1, - /** - * action argument has an invalid value - */ WL_DATA_OFFER_ERROR_INVALID_ACTION = 2, - /** - * offer doesn't accept this request - */ WL_DATA_OFFER_ERROR_INVALID_OFFER = 3, }; -#endif /* WL_DATA_OFFER_ERROR_ENUM */ +#endif -/** - * @ingroup iface_wl_data_offer - * @struct wl_data_offer_listener - */ struct wl_data_offer_listener { - /** - * advertise offered mime type - * - * Sent immediately after creating the wl_data_offer object. One - * event per offered mime type. - * @param mime_type offered mime type - */ void (*offer)(void *data, struct wl_data_offer *wl_data_offer, const char *mime_type); - /** - * notify the source-side available actions - * - * This event indicates the actions offered by the data source. - * It will be sent immediately after creating the wl_data_offer - * object, or anytime the source side changes its offered actions - * through wl_data_source.set_actions. - * @param source_actions actions offered by the data source - * @since 3 - */ void (*source_actions)(void *data, struct wl_data_offer *wl_data_offer, uint32_t source_actions); - /** - * notify the selected action - * - * This event indicates the action selected by the compositor - * after matching the source/destination side actions. Only one - * action (or none) will be offered here. - * - * This event can be emitted multiple times during the - * drag-and-drop operation in response to destination side action - * changes through wl_data_offer.set_actions. - * - * This event will no longer be emitted after wl_data_device.drop - * happened on the drag-and-drop destination, the client must honor - * the last action received, or the last preferred one set through - * wl_data_offer.set_actions when handling an "ask" action. - * - * Compositors may also change the selected action on the fly, - * mainly in response to keyboard modifier changes during the - * drag-and-drop operation. - * - * The most recent action received is always the valid one. Prior - * to receiving wl_data_device.drop, the chosen action may change - * (e.g. due to keyboard modifiers being pressed). At the time of - * receiving wl_data_device.drop the drag-and-drop destination must - * honor the last action received. - * - * Action changes may still happen after wl_data_device.drop, - * especially on "ask" actions, where the drag-and-drop destination - * may choose another action afterwards. Action changes happening - * at this stage are always the result of inter-client negotiation, - * the compositor shall no longer be able to induce a different - * action. - * - * Upon "ask" actions, it is expected that the drag-and-drop - * destination may potentially choose a different action and/or - * mime type, based on wl_data_offer.source_actions and finally - * chosen by the user (e.g. popping up a menu with the available - * options). The final wl_data_offer.set_actions and - * wl_data_offer.accept requests must happen before the call to - * wl_data_offer.finish. - * @param dnd_action action selected by the compositor - * @since 3 - */ void (*action)(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action); }; -/** - * @ingroup iface_wl_data_offer - */ static inline int wl_data_offer_add_listener(struct wl_data_offer *wl_data_offer, const struct wl_data_offer_listener *listener, void *data) @@ -2424,48 +791,22 @@ wl_data_offer_add_listener(struct wl_data_offer *wl_data_offer, #define WL_DATA_OFFER_FINISH 3 #define WL_DATA_OFFER_SET_ACTIONS 4 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_SOURCE_ACTIONS_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_ACTION_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_ACCEPT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_RECEIVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_FINISH_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_offer - */ #define WL_DATA_OFFER_SET_ACTIONS_SINCE_VERSION 3 -/** @ingroup iface_wl_data_offer */ static inline void wl_data_offer_set_user_data(struct wl_data_offer *wl_data_offer, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_data_offer, user_data); } -/** @ingroup iface_wl_data_offer */ static inline void * wl_data_offer_get_user_data(struct wl_data_offer *wl_data_offer) { @@ -2478,24 +819,6 @@ wl_data_offer_get_version(struct wl_data_offer *wl_data_offer) return wl_proxy_get_version((struct wl_proxy *) wl_data_offer); } -/** - * @ingroup iface_wl_data_offer - * - * Indicate that the client can accept the given mime type, or - * NULL for not accepted. - * - * For objects of version 2 or older, this request is used by the - * client to give feedback whether the client can receive the given - * mime type, or NULL if none is accepted; the feedback does not - * determine whether the drag-and-drop operation succeeds or not. - * - * For objects of version 3 or newer, this request determines the - * final result of the drag-and-drop operation. If the end result - * is that no mime types were accepted, the drag-and-drop operation - * will be cancelled and the corresponding drag source will receive - * wl_data_source.cancelled. Clients may still use this event in - * conjunction with wl_data_source.action for feedback. - */ static inline void wl_data_offer_accept(struct wl_data_offer *wl_data_offer, uint32_t serial, const char *mime_type) { @@ -2503,25 +826,6 @@ wl_data_offer_accept(struct wl_data_offer *wl_data_offer, uint32_t serial, const WL_DATA_OFFER_ACCEPT, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_offer), 0, serial, mime_type); } -/** - * @ingroup iface_wl_data_offer - * - * To transfer the offered data, the client issues this request - * and indicates the mime type it wants to receive. The transfer - * happens through the passed file descriptor (typically created - * with the pipe system call). The source client writes the data - * in the mime type representation requested and then closes the - * file descriptor. - * - * The receiving client reads from the read end of the pipe until - * EOF and then closes its end, at which point the transfer is - * complete. - * - * This request may happen multiple times for different mime types, - * both before and after wl_data_device.drop. Drag-and-drop destination - * clients may preemptively fetch data or examine it more closely to - * determine acceptance. - */ static inline void wl_data_offer_receive(struct wl_data_offer *wl_data_offer, const char *mime_type, int32_t fd) { @@ -2529,11 +833,6 @@ wl_data_offer_receive(struct wl_data_offer *wl_data_offer, const char *mime_type WL_DATA_OFFER_RECEIVE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_offer), 0, mime_type, fd); } -/** - * @ingroup iface_wl_data_offer - * - * Destroy the data offer. - */ static inline void wl_data_offer_destroy(struct wl_data_offer *wl_data_offer) { @@ -2541,24 +840,6 @@ wl_data_offer_destroy(struct wl_data_offer *wl_data_offer) WL_DATA_OFFER_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_offer), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_data_offer - * - * Notifies the compositor that the drag destination successfully - * finished the drag-and-drop operation. - * - * Upon receiving this request, the compositor will emit - * wl_data_source.dnd_finished on the drag source client. - * - * It is a client error to perform other requests than - * wl_data_offer.destroy after this one. It is also an error to perform - * this request after a NULL mime type has been set in - * wl_data_offer.accept or no action was received through - * wl_data_offer.action. - * - * If wl_data_offer.finish request is received for a non drag and drop - * operation, the invalid_finish protocol error is raised. - */ static inline void wl_data_offer_finish(struct wl_data_offer *wl_data_offer) { @@ -2566,41 +847,6 @@ wl_data_offer_finish(struct wl_data_offer *wl_data_offer) WL_DATA_OFFER_FINISH, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_offer), 0); } -/** - * @ingroup iface_wl_data_offer - * - * Sets the actions that the destination side client supports for - * this operation. This request may trigger the emission of - * wl_data_source.action and wl_data_offer.action events if the compositor - * needs to change the selected action. - * - * This request can be called multiple times throughout the - * drag-and-drop operation, typically in response to wl_data_device.enter - * or wl_data_device.motion events. - * - * This request determines the final result of the drag-and-drop - * operation. If the end result is that no action is accepted, - * the drag source will receive wl_data_source.cancelled. - * - * The dnd_actions argument must contain only values expressed in the - * wl_data_device_manager.dnd_actions enum, and the preferred_action - * argument must only contain one of those values set, otherwise it - * will result in a protocol error. - * - * While managing an "ask" action, the destination drag-and-drop client - * may perform further wl_data_offer.receive requests, and is expected - * to perform one last wl_data_offer.set_actions request with a preferred - * action other than "ask" (and optionally wl_data_offer.accept) before - * requesting wl_data_offer.finish, in order to convey the action selected - * by the user. If the preferred action is not in the - * wl_data_offer.source_actions mask, an error will be raised. - * - * If the "ask" action is dismissed (e.g. user cancellation), the client - * is expected to perform wl_data_offer.destroy right away. - * - * This request can only be made on drag-and-drop offers, a protocol error - * will be raised otherwise. - */ static inline void wl_data_offer_set_actions(struct wl_data_offer *wl_data_offer, uint32_t dnd_actions, uint32_t preferred_action) { @@ -2611,144 +857,30 @@ wl_data_offer_set_actions(struct wl_data_offer *wl_data_offer, uint32_t dnd_acti #ifndef WL_DATA_SOURCE_ERROR_ENUM #define WL_DATA_SOURCE_ERROR_ENUM enum wl_data_source_error { - /** - * action mask contains invalid values - */ WL_DATA_SOURCE_ERROR_INVALID_ACTION_MASK = 0, - /** - * source doesn't accept this request - */ WL_DATA_SOURCE_ERROR_INVALID_SOURCE = 1, }; -#endif /* WL_DATA_SOURCE_ERROR_ENUM */ +#endif -/** - * @ingroup iface_wl_data_source - * @struct wl_data_source_listener - */ struct wl_data_source_listener { - /** - * a target accepts an offered mime type - * - * Sent when a target accepts pointer_focus or motion events. If - * a target does not accept any of the offered types, type is NULL. - * - * Used for feedback during drag-and-drop. - * @param mime_type mime type accepted by the target - */ void (*target)(void *data, struct wl_data_source *wl_data_source, const char *mime_type); - /** - * send the data - * - * Request for data from the client. Send the data as the - * specified mime type over the passed file descriptor, then close - * it. - * @param mime_type mime type for the data - * @param fd file descriptor for the data - */ void (*send)(void *data, struct wl_data_source *wl_data_source, const char *mime_type, int32_t fd); - /** - * selection was cancelled - * - * This data source is no longer valid. There are several reasons - * why this could happen: - * - * - The data source has been replaced by another data source. - - * The drag-and-drop operation was performed, but the drop - * destination did not accept any of the mime types offered through - * wl_data_source.target. - The drag-and-drop operation was - * performed, but the drop destination did not select any of the - * actions present in the mask offered through - * wl_data_source.action. - The drag-and-drop operation was - * performed but didn't happen over a surface. - The compositor - * cancelled the drag-and-drop operation (e.g. compositor dependent - * timeouts to avoid stale drag-and-drop transfers). - * - * The client should clean up and destroy this data source. - * - * For objects of version 2 or older, wl_data_source.cancelled will - * only be emitted if the data source was replaced by another data - * source. - */ void (*cancelled)(void *data, struct wl_data_source *wl_data_source); - /** - * the drag-and-drop operation physically finished - * - * The user performed the drop action. This event does not - * indicate acceptance, wl_data_source.cancelled may still be - * emitted afterwards if the drop destination does not accept any - * mime type. - * - * However, this event might not be received if the compositor - * cancelled the drag-and-drop operation before this event could - * happen. - * - * Note that the data_source may still be used in the future and - * should not be destroyed here. - * @since 3 - */ void (*dnd_drop_performed)(void *data, struct wl_data_source *wl_data_source); - /** - * the drag-and-drop operation concluded - * - * The drop destination finished interoperating with this data - * source, so the client is now free to destroy this data source - * and free all associated data. - * - * If the action used to perform the operation was "move", the - * source can now delete the transferred data. - * @since 3 - */ void (*dnd_finished)(void *data, struct wl_data_source *wl_data_source); - /** - * notify the selected action - * - * This event indicates the action selected by the compositor - * after matching the source/destination side actions. Only one - * action (or none) will be offered here. - * - * This event can be emitted multiple times during the - * drag-and-drop operation, mainly in response to destination side - * changes through wl_data_offer.set_actions, and as the data - * device enters/leaves surfaces. - * - * It is only possible to receive this event after - * wl_data_source.dnd_drop_performed if the drag-and-drop operation - * ended in an "ask" action, in which case the final - * wl_data_source.action event will happen immediately before - * wl_data_source.dnd_finished. - * - * Compositors may also change the selected action on the fly, - * mainly in response to keyboard modifier changes during the - * drag-and-drop operation. - * - * The most recent action received is always the valid one. The - * chosen action may change alongside negotiation (e.g. an "ask" - * action can turn into a "move" operation), so the effects of the - * final action must always be applied in - * wl_data_source.dnd_finished. - * - * Clients can trigger cursor surface changes from this point, so - * they reflect the current action. - * @param dnd_action action selected by the compositor - * @since 3 - */ void (*action)(void *data, struct wl_data_source *wl_data_source, uint32_t dnd_action); }; -/** - * @ingroup iface_wl_data_source - */ static inline int wl_data_source_add_listener(struct wl_data_source *wl_data_source, const struct wl_data_source_listener *listener, void *data) @@ -2761,52 +893,23 @@ wl_data_source_add_listener(struct wl_data_source *wl_data_source, #define WL_DATA_SOURCE_DESTROY 1 #define WL_DATA_SOURCE_SET_ACTIONS 2 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_TARGET_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_SEND_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_CANCELLED_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_DND_DROP_PERFORMED_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_DND_FINISHED_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_ACTION_SINCE_VERSION 3 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_source - */ #define WL_DATA_SOURCE_SET_ACTIONS_SINCE_VERSION 3 -/** @ingroup iface_wl_data_source */ static inline void wl_data_source_set_user_data(struct wl_data_source *wl_data_source, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_data_source, user_data); } -/** @ingroup iface_wl_data_source */ static inline void * wl_data_source_get_user_data(struct wl_data_source *wl_data_source) { @@ -2819,13 +922,6 @@ wl_data_source_get_version(struct wl_data_source *wl_data_source) return wl_proxy_get_version((struct wl_proxy *) wl_data_source); } -/** - * @ingroup iface_wl_data_source - * - * This request adds a mime type to the set of mime types - * advertised to targets. Can be called several times to offer - * multiple types. - */ static inline void wl_data_source_offer(struct wl_data_source *wl_data_source, const char *mime_type) { @@ -2833,11 +929,6 @@ wl_data_source_offer(struct wl_data_source *wl_data_source, const char *mime_typ WL_DATA_SOURCE_OFFER, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_source), 0, mime_type); } -/** - * @ingroup iface_wl_data_source - * - * Destroy the data source. - */ static inline void wl_data_source_destroy(struct wl_data_source *wl_data_source) { @@ -2845,23 +936,6 @@ wl_data_source_destroy(struct wl_data_source *wl_data_source) WL_DATA_SOURCE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_source), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_data_source - * - * Sets the actions that the source side client supports for this - * operation. This request may trigger wl_data_source.action and - * wl_data_offer.action events if the compositor needs to change the - * selected action. - * - * The dnd_actions argument must contain only values expressed in the - * wl_data_device_manager.dnd_actions enum, otherwise it will result - * in a protocol error. - * - * This request must be made once only, and can only be made on sources - * used in drag-and-drop, so it must be performed before - * wl_data_device.start_drag. Attempting to use the source other than - * for drag-and-drop will raise a protocol error. - */ static inline void wl_data_source_set_actions(struct wl_data_source *wl_data_source, uint32_t dnd_actions) { @@ -2872,50 +946,15 @@ wl_data_source_set_actions(struct wl_data_source *wl_data_source, uint32_t dnd_a #ifndef WL_DATA_DEVICE_ERROR_ENUM #define WL_DATA_DEVICE_ERROR_ENUM enum wl_data_device_error { - /** - * given wl_surface has another role - */ WL_DATA_DEVICE_ERROR_ROLE = 0, - /** - * source has already been used - */ WL_DATA_DEVICE_ERROR_USED_SOURCE = 1, }; -#endif /* WL_DATA_DEVICE_ERROR_ENUM */ +#endif -/** - * @ingroup iface_wl_data_device - * @struct wl_data_device_listener - */ struct wl_data_device_listener { - /** - * introduce a new wl_data_offer - * - * The data_offer event introduces a new wl_data_offer object, - * which will subsequently be used in either the data_device.enter - * event (for drag-and-drop) or the data_device.selection event - * (for selections). Immediately following the - * data_device.data_offer event, the new data_offer object will - * send out data_offer.offer events to describe the mime types it - * offers. - * @param id the new data_offer object - */ void (*data_offer)(void *data, struct wl_data_device *wl_data_device, struct wl_data_offer *id); - /** - * initiate drag-and-drop session - * - * This event is sent when an active drag-and-drop pointer enters - * a surface owned by the client. The position of the pointer at - * enter time is provided by the x and y arguments, in - * surface-local coordinates. - * @param serial serial number of the enter event - * @param surface client surface entered - * @param x surface-local x coordinate - * @param y surface-local y coordinate - * @param id source data_offer object - */ void (*enter)(void *data, struct wl_data_device *wl_data_device, uint32_t serial, @@ -2923,75 +962,20 @@ struct wl_data_device_listener { wl_fixed_t x, wl_fixed_t y, struct wl_data_offer *id); - /** - * end drag-and-drop session - * - * This event is sent when the drag-and-drop pointer leaves the - * surface and the session ends. The client must destroy the - * wl_data_offer introduced at enter time at this point. - */ void (*leave)(void *data, struct wl_data_device *wl_data_device); - /** - * drag-and-drop session motion - * - * This event is sent when the drag-and-drop pointer moves within - * the currently focused surface. The new position of the pointer - * is provided by the x and y arguments, in surface-local - * coordinates. - * @param time timestamp with millisecond granularity - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ void (*motion)(void *data, struct wl_data_device *wl_data_device, uint32_t time, wl_fixed_t x, wl_fixed_t y); - /** - * end drag-and-drop session successfully - * - * The event is sent when a drag-and-drop operation is ended - * because the implicit grab is removed. - * - * The drag-and-drop destination is expected to honor the last - * action received through wl_data_offer.action, if the resulting - * action is "copy" or "move", the destination can still perform - * wl_data_offer.receive requests, and is expected to end all - * transfers with a wl_data_offer.finish request. - * - * If the resulting action is "ask", the action will not be - * considered final. The drag-and-drop destination is expected to - * perform one last wl_data_offer.set_actions request, or - * wl_data_offer.destroy in order to cancel the operation. - */ void (*drop)(void *data, struct wl_data_device *wl_data_device); - /** - * advertise new selection - * - * The selection event is sent out to notify the client of a new - * wl_data_offer for the selection for this device. The - * data_device.data_offer and the data_offer.offer events are sent - * out immediately before this event to introduce the data offer - * object. The selection event is sent to a client immediately - * before receiving keyboard focus and when a new selection is set - * while the client has keyboard focus. The data_offer is valid - * until a new data_offer or NULL is received or until the client - * loses keyboard focus. Switching surface with keyboard focus - * within the same client doesn't mean a new selection will be - * sent. The client must destroy the previous selection data_offer, - * if any, upon receiving this event. - * @param id selection data_offer object - */ void (*selection)(void *data, struct wl_data_device *wl_data_device, struct wl_data_offer *id); }; -/** - * @ingroup iface_wl_data_device - */ static inline int wl_data_device_add_listener(struct wl_data_device *wl_data_device, const struct wl_data_device_listener *listener, void *data) @@ -3004,52 +988,23 @@ wl_data_device_add_listener(struct wl_data_device *wl_data_device, #define WL_DATA_DEVICE_SET_SELECTION 1 #define WL_DATA_DEVICE_RELEASE 2 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_DATA_OFFER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_DROP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_SELECTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_START_DRAG_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_SET_SELECTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device - */ #define WL_DATA_DEVICE_RELEASE_SINCE_VERSION 2 -/** @ingroup iface_wl_data_device */ static inline void wl_data_device_set_user_data(struct wl_data_device *wl_data_device, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_data_device, user_data); } -/** @ingroup iface_wl_data_device */ static inline void * wl_data_device_get_user_data(struct wl_data_device *wl_data_device) { @@ -3062,46 +1017,12 @@ wl_data_device_get_version(struct wl_data_device *wl_data_device) return wl_proxy_get_version((struct wl_proxy *) wl_data_device); } -/** @ingroup iface_wl_data_device */ static inline void wl_data_device_destroy(struct wl_data_device *wl_data_device) { wl_proxy_destroy((struct wl_proxy *) wl_data_device); } -/** - * @ingroup iface_wl_data_device - * - * This request asks the compositor to start a drag-and-drop - * operation on behalf of the client. - * - * The source argument is the data source that provides the data - * for the eventual data transfer. If source is NULL, enter, leave - * and motion events are sent only to the client that initiated the - * drag and the client is expected to handle the data passing - * internally. If source is destroyed, the drag-and-drop session will be - * cancelled. - * - * The origin surface is the surface where the drag originates and - * the client must have an active implicit grab that matches the - * serial. - * - * The icon surface is an optional (can be NULL) surface that - * provides an icon to be moved around with the cursor. Initially, - * the top-left corner of the icon surface is placed at the cursor - * hotspot, but subsequent wl_surface.offset requests can move the - * relative position. Attach requests must be confirmed with - * wl_surface.commit as usual. The icon surface is given the role of - * a drag-and-drop icon. If the icon surface already has another role, - * it raises a protocol error. - * - * The input region is ignored for wl_surfaces with the role of a - * drag-and-drop icon. - * - * The given source may not be used in any further set_selection or - * start_drag requests. Attempting to reuse a previously-used source - * may send a used_source error. - */ static inline void wl_data_device_start_drag(struct wl_data_device *wl_data_device, struct wl_data_source *source, struct wl_surface *origin, struct wl_surface *icon, uint32_t serial) { @@ -3109,18 +1030,6 @@ wl_data_device_start_drag(struct wl_data_device *wl_data_device, struct wl_data_ WL_DATA_DEVICE_START_DRAG, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_device), 0, source, origin, icon, serial); } -/** - * @ingroup iface_wl_data_device - * - * This request asks the compositor to set the selection - * to the data from the source on behalf of the client. - * - * To unset the selection, set the source to NULL. - * - * The given source may not be used in any further set_selection or - * start_drag requests. Attempting to reuse a previously-used source - * may send a used_source error. - */ static inline void wl_data_device_set_selection(struct wl_data_device *wl_data_device, struct wl_data_source *source, uint32_t serial) { @@ -3128,11 +1037,6 @@ wl_data_device_set_selection(struct wl_data_device *wl_data_device, struct wl_da WL_DATA_DEVICE_SET_SELECTION, NULL, wl_proxy_get_version((struct wl_proxy *) wl_data_device), 0, source, serial); } -/** - * @ingroup iface_wl_data_device - * - * This request destroys the data device. - */ static inline void wl_data_device_release(struct wl_data_device *wl_data_device) { @@ -3142,80 +1046,29 @@ wl_data_device_release(struct wl_data_device *wl_data_device) #ifndef WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM #define WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM -/** - * @ingroup iface_wl_data_device_manager - * drag and drop actions - * - * This is a bitmask of the available/preferred actions in a - * drag-and-drop operation. - * - * In the compositor, the selected action is a result of matching the - * actions offered by the source and destination sides. "action" events - * with a "none" action will be sent to both source and destination if - * there is no match. All further checks will effectively happen on - * (source actions ∩ destination actions). - * - * In addition, compositors may also pick different actions in - * reaction to key modifiers being pressed. One common design that - * is used in major toolkits (and the behavior recommended for - * compositors) is: - * - * - If no modifiers are pressed, the first match (in bit order) - * will be used. - * - Pressing Shift selects "move", if enabled in the mask. - * - Pressing Control selects "copy", if enabled in the mask. - * - * Behavior beyond that is considered implementation-dependent. - * Compositors may for example bind other modifiers (like Alt/Meta) - * or drags initiated with other buttons than BTN_LEFT to specific - * actions (e.g. "ask"). - */ enum wl_data_device_manager_dnd_action { - /** - * no action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE = 0, - /** - * copy action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY = 1, - /** - * move action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE = 2, - /** - * ask action - */ WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK = 4, }; -#endif /* WL_DATA_DEVICE_MANAGER_DND_ACTION_ENUM */ +#endif #define WL_DATA_DEVICE_MANAGER_CREATE_DATA_SOURCE 0 #define WL_DATA_DEVICE_MANAGER_GET_DATA_DEVICE 1 #define WL_DATA_DEVICE_MANAGER_RELEASE 2 -/** - * @ingroup iface_wl_data_device_manager - */ #define WL_DATA_DEVICE_MANAGER_CREATE_DATA_SOURCE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device_manager - */ #define WL_DATA_DEVICE_MANAGER_GET_DATA_DEVICE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_data_device_manager - */ #define WL_DATA_DEVICE_MANAGER_RELEASE_SINCE_VERSION 4 -/** @ingroup iface_wl_data_device_manager */ static inline void wl_data_device_manager_set_user_data(struct wl_data_device_manager *wl_data_device_manager, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_data_device_manager, user_data); } -/** @ingroup iface_wl_data_device_manager */ static inline void * wl_data_device_manager_get_user_data(struct wl_data_device_manager *wl_data_device_manager) { @@ -3228,18 +1081,12 @@ wl_data_device_manager_get_version(struct wl_data_device_manager *wl_data_device return wl_proxy_get_version((struct wl_proxy *) wl_data_device_manager); } -/** @ingroup iface_wl_data_device_manager */ static inline void wl_data_device_manager_destroy(struct wl_data_device_manager *wl_data_device_manager) { wl_proxy_destroy((struct wl_proxy *) wl_data_device_manager); } -/** - * @ingroup iface_wl_data_device_manager - * - * Create a new data source. - */ static inline struct wl_data_source * wl_data_device_manager_create_data_source(struct wl_data_device_manager *wl_data_device_manager) { @@ -3251,11 +1098,6 @@ wl_data_device_manager_create_data_source(struct wl_data_device_manager *wl_data return (struct wl_data_source *) id; } -/** - * @ingroup iface_wl_data_device_manager - * - * Create a new data device for a given seat. - */ static inline struct wl_data_device * wl_data_device_manager_get_data_device(struct wl_data_device_manager *wl_data_device_manager, struct wl_seat *seat) { @@ -3267,12 +1109,6 @@ wl_data_device_manager_get_data_device(struct wl_data_device_manager *wl_data_de return (struct wl_data_device *) id; } -/** - * @ingroup iface_wl_data_device_manager - * - * This request destroys the wl_data_device_manager. This has no effect on any other - * objects. - */ static inline void wl_data_device_manager_release(struct wl_data_device_manager *wl_data_device_manager) { @@ -3283,29 +1119,21 @@ wl_data_device_manager_release(struct wl_data_device_manager *wl_data_device_man #ifndef WL_SHELL_ERROR_ENUM #define WL_SHELL_ERROR_ENUM enum wl_shell_error { - /** - * given wl_surface has another role - */ WL_SHELL_ERROR_ROLE = 0, }; -#endif /* WL_SHELL_ERROR_ENUM */ +#endif #define WL_SHELL_GET_SHELL_SURFACE 0 -/** - * @ingroup iface_wl_shell - */ #define WL_SHELL_GET_SHELL_SURFACE_SINCE_VERSION 1 -/** @ingroup iface_wl_shell */ static inline void wl_shell_set_user_data(struct wl_shell *wl_shell, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_shell, user_data); } -/** @ingroup iface_wl_shell */ static inline void * wl_shell_get_user_data(struct wl_shell *wl_shell) { @@ -3318,22 +1146,12 @@ wl_shell_get_version(struct wl_shell *wl_shell) return wl_proxy_get_version((struct wl_proxy *) wl_shell); } -/** @ingroup iface_wl_shell */ static inline void wl_shell_destroy(struct wl_shell *wl_shell) { wl_proxy_destroy((struct wl_proxy *) wl_shell); } -/** - * @ingroup iface_wl_shell - * - * Create a shell surface for an existing surface. This gives - * the wl_surface the role of a shell surface. If the wl_surface - * already has another role, it raises a protocol error. - * - * Only one shell surface can be associated with a given surface. - */ static inline struct wl_shell_surface * wl_shell_get_shell_surface(struct wl_shell *wl_shell, struct wl_surface *surface) { @@ -3347,160 +1165,49 @@ wl_shell_get_shell_surface(struct wl_shell *wl_shell, struct wl_surface *surface #ifndef WL_SHELL_SURFACE_RESIZE_ENUM #define WL_SHELL_SURFACE_RESIZE_ENUM -/** - * @ingroup iface_wl_shell_surface - * edge values for resizing - * - * These values are used to indicate which edge of a surface - * is being dragged in a resize operation. The server may - * use this information to adapt its behavior, e.g. choose - * an appropriate cursor image. - */ enum wl_shell_surface_resize { - /** - * no edge - */ WL_SHELL_SURFACE_RESIZE_NONE = 0, - /** - * top edge - */ WL_SHELL_SURFACE_RESIZE_TOP = 1, - /** - * bottom edge - */ WL_SHELL_SURFACE_RESIZE_BOTTOM = 2, - /** - * left edge - */ WL_SHELL_SURFACE_RESIZE_LEFT = 4, - /** - * top and left edges - */ WL_SHELL_SURFACE_RESIZE_TOP_LEFT = 5, - /** - * bottom and left edges - */ WL_SHELL_SURFACE_RESIZE_BOTTOM_LEFT = 6, - /** - * right edge - */ WL_SHELL_SURFACE_RESIZE_RIGHT = 8, - /** - * top and right edges - */ WL_SHELL_SURFACE_RESIZE_TOP_RIGHT = 9, - /** - * bottom and right edges - */ WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT = 10, }; -#endif /* WL_SHELL_SURFACE_RESIZE_ENUM */ +#endif #ifndef WL_SHELL_SURFACE_TRANSIENT_ENUM #define WL_SHELL_SURFACE_TRANSIENT_ENUM -/** - * @ingroup iface_wl_shell_surface - * details of transient behaviour - * - * These flags specify details of the expected behaviour - * of transient surfaces. Used in the set_transient request. - */ enum wl_shell_surface_transient { - /** - * do not set keyboard focus - */ WL_SHELL_SURFACE_TRANSIENT_INACTIVE = 0x1, }; -#endif /* WL_SHELL_SURFACE_TRANSIENT_ENUM */ +#endif #ifndef WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM #define WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM -/** - * @ingroup iface_wl_shell_surface - * different method to set the surface fullscreen - * - * Hints to indicate to the compositor how to deal with a conflict - * between the dimensions of the surface and the dimensions of the - * output. The compositor is free to ignore this parameter. - */ enum wl_shell_surface_fullscreen_method { - /** - * no preference, apply default policy - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_DEFAULT = 0, - /** - * scale, preserve the surface's aspect ratio and center on output - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_SCALE = 1, - /** - * switch output mode to the smallest mode that can fit the surface, add black borders to compensate size mismatch - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_DRIVER = 2, - /** - * no upscaling, center on output and add black borders to compensate size mismatch - */ WL_SHELL_SURFACE_FULLSCREEN_METHOD_FILL = 3, }; -#endif /* WL_SHELL_SURFACE_FULLSCREEN_METHOD_ENUM */ +#endif -/** - * @ingroup iface_wl_shell_surface - * @struct wl_shell_surface_listener - */ struct wl_shell_surface_listener { - /** - * ping client - * - * Ping a client to check if it is receiving events and sending - * requests. A client is expected to reply with a pong request. - * @param serial serial number of the ping - */ void (*ping)(void *data, struct wl_shell_surface *wl_shell_surface, uint32_t serial); - /** - * suggest resize - * - * The configure event asks the client to resize its surface. - * - * The size is a hint, in the sense that the client is free to - * ignore it if it doesn't resize, pick a smaller size (to satisfy - * aspect ratio or resize in steps of NxM pixels). - * - * The edges parameter provides a hint about how the surface was - * resized. The client may use this information to decide how to - * adjust its content to the new size (e.g. a scrolling area might - * adjust its content position to leave the viewable content - * unmoved). - * - * The client is free to dismiss all but the last configure event - * it received. - * - * The width and height arguments specify the size of the window in - * surface-local coordinates. - * @param edges how the surface was resized - * @param width new width of the surface - * @param height new height of the surface - */ void (*configure)(void *data, struct wl_shell_surface *wl_shell_surface, uint32_t edges, int32_t width, int32_t height); - /** - * popup interaction is done - * - * The popup_done event is sent out when a popup grab is broken, - * that is, when the user clicks a surface that doesn't belong to - * the client owning the popup surface. - */ void (*popup_done)(void *data, struct wl_shell_surface *wl_shell_surface); }; -/** - * @ingroup iface_wl_shell_surface - */ static inline int wl_shell_surface_add_listener(struct wl_shell_surface *wl_shell_surface, const struct wl_shell_surface_listener *listener, void *data) @@ -3520,68 +1227,27 @@ wl_shell_surface_add_listener(struct wl_shell_surface *wl_shell_surface, #define WL_SHELL_SURFACE_SET_TITLE 8 #define WL_SHELL_SURFACE_SET_CLASS 9 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_PING_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_POPUP_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_PONG_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_MOVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_RESIZE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_TOPLEVEL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_TRANSIENT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_POPUP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_TITLE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_shell_surface - */ #define WL_SHELL_SURFACE_SET_CLASS_SINCE_VERSION 1 -/** @ingroup iface_wl_shell_surface */ static inline void wl_shell_surface_set_user_data(struct wl_shell_surface *wl_shell_surface, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_shell_surface, user_data); } -/** @ingroup iface_wl_shell_surface */ static inline void * wl_shell_surface_get_user_data(struct wl_shell_surface *wl_shell_surface) { @@ -3594,19 +1260,12 @@ wl_shell_surface_get_version(struct wl_shell_surface *wl_shell_surface) return wl_proxy_get_version((struct wl_proxy *) wl_shell_surface); } -/** @ingroup iface_wl_shell_surface */ static inline void wl_shell_surface_destroy(struct wl_shell_surface *wl_shell_surface) { wl_proxy_destroy((struct wl_proxy *) wl_shell_surface); } -/** - * @ingroup iface_wl_shell_surface - * - * A client must respond to a ping event with a pong request or - * the client may be deemed unresponsive. - */ static inline void wl_shell_surface_pong(struct wl_shell_surface *wl_shell_surface, uint32_t serial) { @@ -3614,15 +1273,6 @@ wl_shell_surface_pong(struct wl_shell_surface *wl_shell_surface, uint32_t serial WL_SHELL_SURFACE_PONG, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, serial); } -/** - * @ingroup iface_wl_shell_surface - * - * Start a pointer-driven move of the surface. - * - * This request must be used in response to a button press event. - * The server may ignore move requests depending on the state of - * the surface (e.g. fullscreen or maximized). - */ static inline void wl_shell_surface_move(struct wl_shell_surface *wl_shell_surface, struct wl_seat *seat, uint32_t serial) { @@ -3630,15 +1280,6 @@ wl_shell_surface_move(struct wl_shell_surface *wl_shell_surface, struct wl_seat WL_SHELL_SURFACE_MOVE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, seat, serial); } -/** - * @ingroup iface_wl_shell_surface - * - * Start a pointer-driven resizing of the surface. - * - * This request must be used in response to a button press event. - * The server may ignore resize requests depending on the state of - * the surface (e.g. fullscreen or maximized). - */ static inline void wl_shell_surface_resize(struct wl_shell_surface *wl_shell_surface, struct wl_seat *seat, uint32_t serial, uint32_t edges) { @@ -3646,13 +1287,6 @@ wl_shell_surface_resize(struct wl_shell_surface *wl_shell_surface, struct wl_sea WL_SHELL_SURFACE_RESIZE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, seat, serial, edges); } -/** - * @ingroup iface_wl_shell_surface - * - * Map the surface as a toplevel surface. - * - * A toplevel surface is not fullscreen, maximized or transient. - */ static inline void wl_shell_surface_set_toplevel(struct wl_shell_surface *wl_shell_surface) { @@ -3660,17 +1294,6 @@ wl_shell_surface_set_toplevel(struct wl_shell_surface *wl_shell_surface) WL_SHELL_SURFACE_SET_TOPLEVEL, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0); } -/** - * @ingroup iface_wl_shell_surface - * - * Map the surface relative to an existing surface. - * - * The x and y arguments specify the location of the upper left - * corner of the surface relative to the upper left corner of the - * parent surface, in surface-local coordinates. - * - * The flags argument controls details of the transient behaviour. - */ static inline void wl_shell_surface_set_transient(struct wl_shell_surface *wl_shell_surface, struct wl_surface *parent, int32_t x, int32_t y, uint32_t flags) { @@ -3678,43 +1301,6 @@ wl_shell_surface_set_transient(struct wl_shell_surface *wl_shell_surface, struct WL_SHELL_SURFACE_SET_TRANSIENT, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, parent, x, y, flags); } -/** - * @ingroup iface_wl_shell_surface - * - * Map the surface as a fullscreen surface. - * - * If an output parameter is given then the surface will be made - * fullscreen on that output. If the client does not specify the - * output then the compositor will apply its policy - usually - * choosing the output on which the surface has the biggest surface - * area. - * - * The client may specify a method to resolve a size conflict - * between the output size and the surface size - this is provided - * through the method parameter. - * - * The framerate parameter is used only when the method is set - * to "driver", to indicate the preferred framerate. A value of 0 - * indicates that the client does not care about framerate. The - * framerate is specified in mHz, that is framerate of 60000 is 60Hz. - * - * A method of "scale" or "driver" implies a scaling operation of - * the surface, either via a direct scaling operation or a change of - * the output mode. This will override any kind of output scaling, so - * that mapping a surface with a buffer size equal to the mode can - * fill the screen independent of buffer_scale. - * - * A method of "fill" means we don't scale up the buffer, however - * any output scale is applied. This means that you may run into - * an edge case where the application maps a buffer with the same - * size of the output mode but buffer_scale 1 (thus making a - * surface larger than the output). In this case it is allowed to - * downscale the results to fit the screen. - * - * The compositor must reply to this request with a configure event - * with the dimensions for the output on which the surface will - * be made fullscreen. - */ static inline void wl_shell_surface_set_fullscreen(struct wl_shell_surface *wl_shell_surface, uint32_t method, uint32_t framerate, struct wl_output *output) { @@ -3722,29 +1308,6 @@ wl_shell_surface_set_fullscreen(struct wl_shell_surface *wl_shell_surface, uint3 WL_SHELL_SURFACE_SET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, method, framerate, output); } -/** - * @ingroup iface_wl_shell_surface - * - * Map the surface as a popup. - * - * A popup surface is a transient surface with an added pointer - * grab. - * - * An existing implicit grab will be changed to owner-events mode, - * and the popup grab will continue after the implicit grab ends - * (i.e. releasing the mouse button does not cause the popup to - * be unmapped). - * - * The popup grab continues until the window is destroyed or a - * mouse button is pressed in any other client's window. A click - * in any of the client's surfaces is reported as normal, however, - * clicks in other clients' surfaces will be discarded and trigger - * the callback. - * - * The x and y arguments specify the location of the upper left - * corner of the surface relative to the upper left corner of the - * parent surface, in surface-local coordinates. - */ static inline void wl_shell_surface_set_popup(struct wl_shell_surface *wl_shell_surface, struct wl_seat *seat, uint32_t serial, struct wl_surface *parent, int32_t x, int32_t y, uint32_t flags) { @@ -3752,28 +1315,6 @@ wl_shell_surface_set_popup(struct wl_shell_surface *wl_shell_surface, struct wl_ WL_SHELL_SURFACE_SET_POPUP, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, seat, serial, parent, x, y, flags); } -/** - * @ingroup iface_wl_shell_surface - * - * Map the surface as a maximized surface. - * - * If an output parameter is given then the surface will be - * maximized on that output. If the client does not specify the - * output then the compositor will apply its policy - usually - * choosing the output on which the surface has the biggest surface - * area. - * - * The compositor will reply with a configure event telling - * the expected new surface size. The operation is completed - * on the next buffer attach to this surface. - * - * A maximized surface typically fills the entire output it is - * bound to, except for desktop elements such as panels. This is - * the main difference between a maximized shell surface and a - * fullscreen shell surface. - * - * The details depend on the compositor implementation. - */ static inline void wl_shell_surface_set_maximized(struct wl_shell_surface *wl_shell_surface, struct wl_output *output) { @@ -3781,17 +1322,6 @@ wl_shell_surface_set_maximized(struct wl_shell_surface *wl_shell_surface, struct WL_SHELL_SURFACE_SET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, output); } -/** - * @ingroup iface_wl_shell_surface - * - * Set a short title for the surface. - * - * This string may be used to identify the surface in a task bar, - * window list, or other user interface elements provided by the - * compositor. - * - * The string must be encoded in UTF-8. - */ static inline void wl_shell_surface_set_title(struct wl_shell_surface *wl_shell_surface, const char *title) { @@ -3799,16 +1329,6 @@ wl_shell_surface_set_title(struct wl_shell_surface *wl_shell_surface, const char WL_SHELL_SURFACE_SET_TITLE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_shell_surface), 0, title); } -/** - * @ingroup iface_wl_shell_surface - * - * Set a class for the surface. - * - * The surface class identifies the general class of applications - * to which the surface belongs. A common convention is to use the - * file name (or the full path if it is a non-standard location) of - * the application's .desktop file as the class. - */ static inline void wl_shell_surface_set_class(struct wl_shell_surface *wl_shell_surface, const char *class_) { @@ -3818,122 +1338,31 @@ wl_shell_surface_set_class(struct wl_shell_surface *wl_shell_surface, const char #ifndef WL_SURFACE_ERROR_ENUM #define WL_SURFACE_ERROR_ENUM -/** - * @ingroup iface_wl_surface - * wl_surface error values - * - * These errors can be emitted in response to wl_surface requests. - */ enum wl_surface_error { - /** - * buffer scale value is invalid - */ WL_SURFACE_ERROR_INVALID_SCALE = 0, - /** - * buffer transform value is invalid - */ WL_SURFACE_ERROR_INVALID_TRANSFORM = 1, - /** - * buffer size is invalid - */ WL_SURFACE_ERROR_INVALID_SIZE = 2, - /** - * buffer offset is invalid - */ WL_SURFACE_ERROR_INVALID_OFFSET = 3, - /** - * surface was destroyed before its role object - */ WL_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 4, - /** - * no buffer was attached - */ WL_SURFACE_ERROR_NO_BUFFER = 5, }; -#endif /* WL_SURFACE_ERROR_ENUM */ +#endif -/** - * @ingroup iface_wl_surface - * @struct wl_surface_listener - */ struct wl_surface_listener { - /** - * surface enters an output - * - * This is emitted whenever a surface's creation, movement, or - * resizing results in some part of it being within the scanout - * region of an output. - * - * Note that a surface may be overlapping with zero or more - * outputs. - * @param output output entered by the surface - */ void (*enter)(void *data, struct wl_surface *wl_surface, struct wl_output *output); - /** - * surface leaves an output - * - * This is emitted whenever a surface's creation, movement, or - * resizing results in it no longer having any part of it within - * the scanout region of an output. - * - * Clients should not use the number of outputs the surface is on - * for frame throttling purposes. The surface might be hidden even - * if no leave event has been sent, and the compositor might expect - * new surface content updates even if no enter event has been - * sent. The frame event should be used instead. - * @param output output left by the surface - */ void (*leave)(void *data, struct wl_surface *wl_surface, struct wl_output *output); - /** - * preferred buffer scale for the surface - * - * This event indicates the preferred buffer scale for this - * surface. It is sent whenever the compositor's preference - * changes. - * - * Before receiving this event the preferred buffer scale for this - * surface is 1. - * - * It is intended that scaling aware clients use this event to - * scale their content and use wl_surface.set_buffer_scale to - * indicate the scale they have rendered with. This allows clients - * to supply a higher detail buffer. - * - * The compositor shall emit a scale value greater than 0. - * @param factor preferred scaling factor - * @since 6 - */ void (*preferred_buffer_scale)(void *data, struct wl_surface *wl_surface, int32_t factor); - /** - * preferred buffer transform for the surface - * - * This event indicates the preferred buffer transform for this - * surface. It is sent whenever the compositor's preference - * changes. - * - * Before receiving this event the preferred buffer transform for - * this surface is normal. - * - * Applying this transformation to the surface buffer contents and - * using wl_surface.set_buffer_transform might allow the compositor - * to use the surface buffer more efficiently. - * @param transform preferred transform - * @since 6 - */ void (*preferred_buffer_transform)(void *data, struct wl_surface *wl_surface, uint32_t transform); }; -/** - * @ingroup iface_wl_surface - */ static inline int wl_surface_add_listener(struct wl_surface *wl_surface, const struct wl_surface_listener *listener, void *data) @@ -3955,80 +1384,30 @@ wl_surface_add_listener(struct wl_surface *wl_surface, #define WL_SURFACE_OFFSET 10 #define WL_SURFACE_GET_RELEASE 11 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION 6 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_PREFERRED_BUFFER_TRANSFORM_SINCE_VERSION 6 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_ATTACH_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_DAMAGE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_FRAME_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_OPAQUE_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_INPUT_REGION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_COMMIT_SINCE_VERSION 1 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_BUFFER_TRANSFORM_SINCE_VERSION 2 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_SET_BUFFER_SCALE_SINCE_VERSION 3 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION 4 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_OFFSET_SINCE_VERSION 5 -/** - * @ingroup iface_wl_surface - */ #define WL_SURFACE_GET_RELEASE_SINCE_VERSION 7 -/** @ingroup iface_wl_surface */ static inline void wl_surface_set_user_data(struct wl_surface *wl_surface, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_surface, user_data); } -/** @ingroup iface_wl_surface */ static inline void * wl_surface_get_user_data(struct wl_surface *wl_surface) { @@ -4041,11 +1420,6 @@ wl_surface_get_version(struct wl_surface *wl_surface) return wl_proxy_get_version((struct wl_proxy *) wl_surface); } -/** - * @ingroup iface_wl_surface - * - * Deletes the surface and invalidates its object ID. - */ static inline void wl_surface_destroy(struct wl_surface *wl_surface) { @@ -4053,77 +1427,6 @@ wl_surface_destroy(struct wl_surface *wl_surface) WL_SURFACE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_surface - * - * Set a buffer as the content of this surface. - * - * The new size of the surface is calculated based on the buffer - * size transformed by the inverse buffer_transform and the - * inverse buffer_scale. This means that at commit time the supplied - * buffer size must be an integer multiple of the buffer_scale. If - * that's not the case, an invalid_size error is sent. - * - * The x and y arguments specify the location of the new pending - * buffer's upper left corner, relative to the current buffer's upper - * left corner, in surface-local coordinates. In other words, the - * x and y, combined with the new surface size define in which - * directions the surface's size changes. Setting anything other than 0 - * as x and y arguments is discouraged, and should instead be replaced - * with using the separate wl_surface.offset request. - * - * When the bound wl_surface version is 5 or higher, passing any - * non-zero x or y is a protocol violation, and will result in an - * 'invalid_offset' error being raised. The x and y arguments are ignored - * and do not change the pending state. To achieve equivalent semantics, - * use wl_surface.offset. - * - * Surface contents are double-buffered state, see wl_surface.commit. - * - * The initial surface contents are void; there is no content. - * wl_surface.attach assigns the given wl_buffer as the pending - * wl_buffer. wl_surface.commit makes the pending wl_buffer the new - * surface contents, and the size of the surface becomes the size - * calculated from the wl_buffer, as described above. After commit, - * there is no pending buffer until the next attach. - * - * Committing a pending wl_buffer allows the compositor to read the - * pixels in the wl_buffer. The compositor may access the pixels at - * any time after the wl_surface.commit request. When the compositor - * will not access the pixels anymore, it will send the - * wl_buffer.release event. Only after receiving wl_buffer.release, - * the client may reuse the wl_buffer. A wl_buffer that has been - * attached and then replaced by another attach instead of committed - * will not receive a release event, and is not used by the - * compositor. - * - * If a pending wl_buffer has been committed to more than one wl_surface, - * the delivery of wl_buffer.release events becomes undefined. A well - * behaved client should not rely on wl_buffer.release events in this - * case. Instead, clients hitting this case should use - * wl_surface.get_release or use a protocol extension providing per-commit - * release notifications (if none of these options are available, a - * fallback can be implemented by creating multiple wl_buffer objects from - * the same backing storage). - * - * Destroying the wl_buffer after wl_buffer.release does not change - * the surface contents. Destroying the wl_buffer before wl_buffer.release - * is allowed as long as the underlying buffer storage isn't re-used (this - * can happen e.g. on client process termination). However, if the client - * destroys the wl_buffer before receiving the wl_buffer.release event and - * mutates the underlying buffer storage, the surface contents become - * undefined immediately. - * - * If wl_surface.attach is sent with a NULL wl_buffer, the - * following wl_surface.commit will remove the surface content. - * - * If a pending wl_buffer has been destroyed, the result is not specified. - * Many compositors are known to remove the surface content on the following - * wl_surface.commit, but this behaviour is not universal. Clients seeking to - * maximise compatibility should not destroy pending buffers and should - * ensure that they explicitly remove content from surfaces, even after - * destroying buffers. - */ static inline void wl_surface_attach(struct wl_surface *wl_surface, struct wl_buffer *buffer, int32_t x, int32_t y) { @@ -4131,31 +1434,6 @@ wl_surface_attach(struct wl_surface *wl_surface, struct wl_buffer *buffer, int32 WL_SURFACE_ATTACH, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, buffer, x, y); } -/** - * @ingroup iface_wl_surface - * - * This request is used to describe the regions where the pending - * buffer is different from the current surface contents, and where - * the surface therefore needs to be repainted. The compositor - * ignores the parts of the damage that fall outside of the surface. - * - * Damage is double-buffered state, see wl_surface.commit. - * - * The damage rectangle is specified in surface-local coordinates, - * where x and y specify the upper left corner of the damage rectangle. - * - * The initial value for pending damage is empty: no damage. - * wl_surface.damage adds pending damage: the new pending damage - * is the union of old pending damage and the given rectangle. - * - * wl_surface.commit assigns pending damage as the current damage, - * and clears pending damage. The server will clear the current - * damage as it repaints the surface. - * - * Note! New clients should not use this request. Instead damage can be - * posted with wl_surface.damage_buffer which uses buffer coordinates - * instead of surface coordinates. - */ static inline void wl_surface_damage(struct wl_surface *wl_surface, int32_t x, int32_t y, int32_t width, int32_t height) { @@ -4163,42 +1441,6 @@ wl_surface_damage(struct wl_surface *wl_surface, int32_t x, int32_t y, int32_t w WL_SURFACE_DAMAGE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, x, y, width, height); } -/** - * @ingroup iface_wl_surface - * - * Request a notification when it is a good time to start drawing a new - * frame, by creating a frame callback. This is useful for throttling - * redrawing operations, and driving animations. - * - * When a client is animating on a wl_surface, it can use the 'frame' - * request to get notified when it is a good time to draw and commit the - * next frame of animation. If the client commits an update earlier than - * that, it is likely that some updates will not make it to the display, - * and the client is wasting resources by drawing too often. - * - * The frame request will take effect on the next wl_surface.commit. - * The notification will only be posted for one frame unless - * requested again. For a wl_surface, the notifications are posted in - * the order the frame requests were committed. - * - * The server must send the notifications so that a client - * will not send excessive updates, while still allowing - * the highest possible update rate for clients that wait for the reply - * before drawing again. The server should give some time for the client - * to draw and commit after sending the frame callback events to let it - * hit the next output refresh. - * - * A server should avoid signaling the frame callbacks if the - * surface is not visible in any way, e.g. the surface is off-screen, - * or completely obscured by other opaque surfaces. - * - * The object returned by this request will be destroyed by the - * compositor after the callback is fired and as such the client must not - * attempt to use it after that point. - * - * The callback_data passed in the callback is the current time, in - * milliseconds, with an undefined base. - */ static inline struct wl_callback * wl_surface_frame(struct wl_surface *wl_surface) { @@ -4210,34 +1452,6 @@ wl_surface_frame(struct wl_surface *wl_surface) return (struct wl_callback *) callback; } -/** - * @ingroup iface_wl_surface - * - * This request sets the region of the surface that contains - * opaque content. - * - * The opaque region is an optimization hint for the compositor - * that lets it optimize the redrawing of content behind opaque - * regions. Setting an opaque region is not required for correct - * behaviour, but marking transparent content as opaque will result - * in repaint artifacts. - * - * The opaque region is specified in surface-local coordinates. - * - * The compositor ignores the parts of the opaque region that fall - * outside of the surface. - * - * Opaque region is double-buffered state, see wl_surface.commit. - * - * wl_surface.set_opaque_region changes the pending opaque region. - * wl_surface.commit copies the pending region to the current region. - * Otherwise, the pending and current regions are never changed. - * - * The initial value for an opaque region is empty. Setting the pending - * opaque region has copy semantics, and the wl_region object can be - * destroyed immediately. A NULL wl_region causes the pending opaque - * region to be set to empty. - */ static inline void wl_surface_set_opaque_region(struct wl_surface *wl_surface, struct wl_region *region) { @@ -4245,32 +1459,6 @@ wl_surface_set_opaque_region(struct wl_surface *wl_surface, struct wl_region *re WL_SURFACE_SET_OPAQUE_REGION, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, region); } -/** - * @ingroup iface_wl_surface - * - * This request sets the region of the surface that can receive - * pointer and touch events. - * - * Input events happening outside of this region will try the next - * surface in the server surface stack. The compositor ignores the - * parts of the input region that fall outside of the surface. - * - * The input region is specified in surface-local coordinates. - * - * Input region is double-buffered state, see wl_surface.commit. - * - * wl_surface.set_input_region changes the pending input region. - * wl_surface.commit copies the pending region to the current region. - * Otherwise the pending and current regions are never changed, - * except cursor and icon surfaces are special cases, see - * wl_pointer.set_cursor and wl_data_device.start_drag. - * - * The initial value for an input region is infinite. That means the - * whole surface will accept input. Setting the pending input region - * has copy semantics, and the wl_region object can be destroyed - * immediately. A NULL wl_region causes the input region to be set - * to infinite. - */ static inline void wl_surface_set_input_region(struct wl_surface *wl_surface, struct wl_region *region) { @@ -4278,56 +1466,6 @@ wl_surface_set_input_region(struct wl_surface *wl_surface, struct wl_region *reg WL_SURFACE_SET_INPUT_REGION, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, region); } -/** - * @ingroup iface_wl_surface - * - * Surface state (input, opaque, and damage regions, attached buffers, - * etc.) is double-buffered. Protocol requests modify the pending state, - * as opposed to the active state in use by the compositor. - * - * All requests that need a commit to become effective are documented - * to affect double-buffered state. - * - * Other interfaces may add further double-buffered surface state. - * - * A commit request atomically creates a Content Update (CU) from the - * pending state, even if the pending state has not been touched. The - * content update is placed at the end of a per-surface queue until it - * becomes active. After commit, the new pending state is as documented for - * each related request. - * - * A CU is either a Desync Content Update (DCU) or a Sync Content Update - * (SCU). If the surface is effectively synchronized at the commit request, - * it is a SCU, otherwise a DCU. - * - * When a surface transitions from effectively synchronized to effectively - * desynchronized, all SCUs in its queue which are not reachable by any - * DCU become DCUs and dependency edges from outside the queue to these CUs - * are removed. - * - * See wl_subsurface for the definition of 'effectively synchronized' and - * 'effectively desynchronized'. - * - * When a CU is placed in the queue, the CU has a dependency on the CU in - * front of it and to the SCU at end of the queue of every direct child - * surface if that SCU exists and does not have another dependent. This can - * form a directed acyclic graph of CUs with dependencies as edges. - * - * In addition to surface state, the CU can have constraints that must be - * satisfied before it can be applied. Other interfaces may add CU - * constraints. - * - * All DCUs which do not have a SCU in front of themselves in their queue, - * are candidates. If the graph that's reachable by a candidate does not - * have any unsatisfied constraints, the entire graph must be applied - * atomically. - * - * When a CU is applied, the wl_buffer is applied before all other state. - * This means that all coordinates in double-buffered state are relative to - * the newly attached wl_buffers, except for wl_surface.attach itself. If - * there is no newly attached wl_buffer, the coordinates are relative to - * the previous content update. - */ static inline void wl_surface_commit(struct wl_surface *wl_surface) { @@ -4335,41 +1473,6 @@ wl_surface_commit(struct wl_surface *wl_surface) WL_SURFACE_COMMIT, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0); } -/** - * @ingroup iface_wl_surface - * - * This request sets the transformation that the client has already applied - * to the content of the buffer. The accepted values for the transform - * parameter are the values for wl_output.transform. - * - * The compositor applies the inverse of this transformation whenever it - * uses the buffer contents. - * - * Buffer transform is double-buffered state, see wl_surface.commit. - * - * A newly created surface has its buffer transformation set to normal. - * - * wl_surface.set_buffer_transform changes the pending buffer - * transformation. wl_surface.commit copies the pending buffer - * transformation to the current one. Otherwise, the pending and current - * values are never changed. - * - * The purpose of this request is to allow clients to render content - * according to the output transform, thus permitting the compositor to - * use certain optimizations even if the display is rotated. Using - * hardware overlays and scanning out a client buffer for fullscreen - * surfaces are examples of such optimizations. Those optimizations are - * highly dependent on the compositor implementation, so the use of this - * request should be considered on a case-by-case basis. - * - * Note that if the transform value includes 90 or 270 degree rotation, - * the width of the buffer will become the surface height and the height - * of the buffer will become the surface width. - * - * If transform is not one of the values from the - * wl_output.transform enum the invalid_transform protocol error - * is raised. - */ static inline void wl_surface_set_buffer_transform(struct wl_surface *wl_surface, int32_t transform) { @@ -4377,33 +1480,6 @@ wl_surface_set_buffer_transform(struct wl_surface *wl_surface, int32_t transform WL_SURFACE_SET_BUFFER_TRANSFORM, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, transform); } -/** - * @ingroup iface_wl_surface - * - * This request sets an optional scaling factor on how the compositor - * interprets the contents of the buffer attached to the window. - * - * Buffer scale is double-buffered state, see wl_surface.commit. - * - * A newly created surface has its buffer scale set to 1. - * - * wl_surface.set_buffer_scale changes the pending buffer scale. - * wl_surface.commit copies the pending buffer scale to the current one. - * Otherwise, the pending and current values are never changed. - * - * The purpose of this request is to allow clients to supply higher - * resolution buffer data for use on high resolution outputs. It is - * intended that you pick the same buffer scale as the scale of the - * output that the surface is displayed on. This means the compositor - * can avoid scaling when rendering the surface on that output. - * - * Note that if the scale is larger than 1, then you have to attach - * a buffer that is larger (by a factor of scale in each dimension) - * than the desired surface size. - * - * If scale is not greater than 0 the invalid_scale protocol error is - * raised. - */ static inline void wl_surface_set_buffer_scale(struct wl_surface *wl_surface, int32_t scale) { @@ -4411,42 +1487,6 @@ wl_surface_set_buffer_scale(struct wl_surface *wl_surface, int32_t scale) WL_SURFACE_SET_BUFFER_SCALE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, scale); } -/** - * @ingroup iface_wl_surface - * - * This request is used to describe the regions where the pending - * buffer is different from the current surface contents, and where - * the surface therefore needs to be repainted. The compositor - * ignores the parts of the damage that fall outside of the surface. - * - * Damage is double-buffered state, see wl_surface.commit. - * - * The damage rectangle is specified in buffer coordinates, - * where x and y specify the upper left corner of the damage rectangle. - * - * The initial value for pending damage is empty: no damage. - * wl_surface.damage_buffer adds pending damage: the new pending - * damage is the union of old pending damage and the given rectangle. - * - * wl_surface.commit assigns pending damage as the current damage, - * and clears pending damage. The server will clear the current - * damage as it repaints the surface. - * - * This request differs from wl_surface.damage in only one way - it - * takes damage in buffer coordinates instead of surface-local - * coordinates. While this generally is more intuitive than surface - * coordinates, it is especially desirable when using wp_viewport - * or when a drawing library (like EGL) is unaware of buffer scale - * and buffer transform. - * - * Note: Because buffer transformation changes and damage requests may - * be interleaved in the protocol stream, it is impossible to determine - * the actual mapping between surface and buffer damage until - * wl_surface.commit time. Therefore, compositors wishing to take both - * kinds of damage into account will have to accumulate damage from the - * two requests separately and only transform from one to the other - * after receiving the wl_surface.commit. - */ static inline void wl_surface_damage_buffer(struct wl_surface *wl_surface, int32_t x, int32_t y, int32_t width, int32_t height) { @@ -4454,25 +1494,6 @@ wl_surface_damage_buffer(struct wl_surface *wl_surface, int32_t x, int32_t y, in WL_SURFACE_DAMAGE_BUFFER, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, x, y, width, height); } -/** - * @ingroup iface_wl_surface - * - * The x and y arguments specify the location of the new pending - * buffer's upper left corner, relative to the current buffer's upper - * left corner, in surface-local coordinates. In other words, the - * x and y, combined with the new surface size define in which - * directions the surface's size changes. - * - * The exact semantics of wl_surface.offset are role-specific. Refer to - * the documentation of specific roles for more information. - * - * Surface location offset is double-buffered state, see - * wl_surface.commit. - * - * This request is semantically equivalent to and the replaces the x and y - * arguments in the wl_surface.attach request in wl_surface versions prior - * to 5. See wl_surface.attach for details. - */ static inline void wl_surface_offset(struct wl_surface *wl_surface, int32_t x, int32_t y) { @@ -4480,28 +1501,6 @@ wl_surface_offset(struct wl_surface *wl_surface, int32_t x, int32_t y) WL_SURFACE_OFFSET, NULL, wl_proxy_get_version((struct wl_proxy *) wl_surface), 0, x, y); } -/** - * @ingroup iface_wl_surface - * - * Create a callback for the release of the buffer attached by the client - * with wl_surface.attach. - * - * The compositor will release the buffer when it has finished its usage of - * the underlying storage for the relevant commit. Once the client receives - * this event, and assuming the associated buffer is not pending release - * from other wl_surface.commit requests, the client can safely re-use the - * buffer. - * - * Release callbacks are double-buffered state, and will be associated - * with the pending buffer at wl_surface.commit time. - * - * The callback_data passed in the wl_callback.done event is unused and - * is always zero. - * - * Sending this request without attaching a non-null buffer in the same - * content update is a protocol error. The compositor will send the - * no_buffer error in this case. - */ static inline struct wl_callback * wl_surface_get_release(struct wl_surface *wl_surface) { @@ -4515,118 +1514,29 @@ wl_surface_get_release(struct wl_surface *wl_surface) #ifndef WL_SEAT_CAPABILITY_ENUM #define WL_SEAT_CAPABILITY_ENUM -/** - * @ingroup iface_wl_seat - * seat capability bitmask - * - * This is a bitmask of capabilities this seat has; if a member is - * set, then it is present on the seat. - */ enum wl_seat_capability { - /** - * the seat has pointer devices - */ WL_SEAT_CAPABILITY_POINTER = 1, - /** - * the seat has one or more keyboards - */ WL_SEAT_CAPABILITY_KEYBOARD = 2, - /** - * the seat has touch devices - */ WL_SEAT_CAPABILITY_TOUCH = 4, }; -#endif /* WL_SEAT_CAPABILITY_ENUM */ +#endif #ifndef WL_SEAT_ERROR_ENUM #define WL_SEAT_ERROR_ENUM -/** - * @ingroup iface_wl_seat - * wl_seat error values - * - * These errors can be emitted in response to wl_seat requests. - */ enum wl_seat_error { - /** - * get_pointer, get_keyboard or get_touch called on seat without the matching capability - */ WL_SEAT_ERROR_MISSING_CAPABILITY = 0, }; -#endif /* WL_SEAT_ERROR_ENUM */ +#endif -/** - * @ingroup iface_wl_seat - * @struct wl_seat_listener - */ struct wl_seat_listener { - /** - * seat capabilities changed - * - * This is sent on binding to the seat global or whenever a seat - * gains or loses the pointer, keyboard or touch capabilities. The - * argument is a capability enum containing the complete set of - * capabilities this seat has. - * - * When the pointer capability is added, a client may create a - * wl_pointer object using the wl_seat.get_pointer request. This - * object will receive pointer events until the capability is - * removed in the future. - * - * When the pointer capability is removed, a client should destroy - * the wl_pointer objects associated with the seat where the - * capability was removed, using the wl_pointer.release request. No - * further pointer events will be received on these objects. - * - * In some compositors, if a seat regains the pointer capability - * and a client has a previously obtained wl_pointer object of - * version 4 or less, that object may start sending pointer events - * again. This behavior is considered a misinterpretation of the - * intended behavior and must not be relied upon by the client. - * wl_pointer objects of version 5 or later must not send events if - * created before the most recent event notifying the client of an - * added pointer capability. - * - * The above behavior also applies to wl_keyboard and wl_touch with - * the keyboard and touch capabilities, respectively. - * @param capabilities capabilities of the seat - */ void (*capabilities)(void *data, struct wl_seat *wl_seat, uint32_t capabilities); - /** - * unique identifier for this seat - * - * In a multi-seat configuration the seat name can be used by - * clients to help identify which physical devices the seat - * represents. - * - * The seat name is a UTF-8 string with no convention defined for - * its contents. Each name is unique among all wl_seat globals. The - * name is only guaranteed to be unique for the current compositor - * instance. - * - * The same seat names are used for all clients. Thus, the name can - * be shared across processes to refer to a specific wl_seat - * global. - * - * The name event is sent after binding to the seat global, and - * should be sent before announcing capabilities. This event is - * only sent once per seat object, and the name does not change - * over the lifetime of the wl_seat global. - * - * Compositors may re-use the same seat name if the wl_seat global - * is destroyed and re-created later. - * @param name seat identifier - * @since 2 - */ void (*name)(void *data, struct wl_seat *wl_seat, const char *name); }; -/** - * @ingroup iface_wl_seat - */ static inline int wl_seat_add_listener(struct wl_seat *wl_seat, const struct wl_seat_listener *listener, void *data) @@ -4640,40 +1550,20 @@ wl_seat_add_listener(struct wl_seat *wl_seat, #define WL_SEAT_GET_TOUCH 2 #define WL_SEAT_RELEASE 3 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_CAPABILITIES_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_NAME_SINCE_VERSION 2 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_GET_POINTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_GET_KEYBOARD_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_GET_TOUCH_SINCE_VERSION 1 -/** - * @ingroup iface_wl_seat - */ #define WL_SEAT_RELEASE_SINCE_VERSION 5 -/** @ingroup iface_wl_seat */ static inline void wl_seat_set_user_data(struct wl_seat *wl_seat, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_seat, user_data); } -/** @ingroup iface_wl_seat */ static inline void * wl_seat_get_user_data(struct wl_seat *wl_seat) { @@ -4686,25 +1576,12 @@ wl_seat_get_version(struct wl_seat *wl_seat) return wl_proxy_get_version((struct wl_proxy *) wl_seat); } -/** @ingroup iface_wl_seat */ static inline void wl_seat_destroy(struct wl_seat *wl_seat) { wl_proxy_destroy((struct wl_proxy *) wl_seat); } -/** - * @ingroup iface_wl_seat - * - * The ID provided will be initialized to the wl_pointer interface - * for this seat. - * - * This request only takes effect if the seat has the pointer - * capability, or has had the pointer capability in the past. - * It is a protocol violation to issue this request on a seat that has - * never had the pointer capability. The missing_capability error will - * be sent in this case. - */ static inline struct wl_pointer * wl_seat_get_pointer(struct wl_seat *wl_seat) { @@ -4716,18 +1593,6 @@ wl_seat_get_pointer(struct wl_seat *wl_seat) return (struct wl_pointer *) id; } -/** - * @ingroup iface_wl_seat - * - * The ID provided will be initialized to the wl_keyboard interface - * for this seat. - * - * This request only takes effect if the seat has the keyboard - * capability, or has had the keyboard capability in the past. - * It is a protocol violation to issue this request on a seat that has - * never had the keyboard capability. The missing_capability error will - * be sent in this case. - */ static inline struct wl_keyboard * wl_seat_get_keyboard(struct wl_seat *wl_seat) { @@ -4739,18 +1604,6 @@ wl_seat_get_keyboard(struct wl_seat *wl_seat) return (struct wl_keyboard *) id; } -/** - * @ingroup iface_wl_seat - * - * The ID provided will be initialized to the wl_touch interface - * for this seat. - * - * This request only takes effect if the seat has the touch - * capability, or has had the touch capability in the past. - * It is a protocol violation to issue this request on a seat that has - * never had the touch capability. The missing_capability error will - * be sent in this case. - */ static inline struct wl_touch * wl_seat_get_touch(struct wl_seat *wl_seat) { @@ -4762,12 +1615,6 @@ wl_seat_get_touch(struct wl_seat *wl_seat) return (struct wl_touch *) id; } -/** - * @ingroup iface_wl_seat - * - * Using this request a client can tell the server that it is not going to - * use the seat object anymore. - */ static inline void wl_seat_release(struct wl_seat *wl_seat) { @@ -4778,483 +1625,99 @@ wl_seat_release(struct wl_seat *wl_seat) #ifndef WL_POINTER_ERROR_ENUM #define WL_POINTER_ERROR_ENUM enum wl_pointer_error { - /** - * given wl_surface has another role - */ WL_POINTER_ERROR_ROLE = 0, }; -#endif /* WL_POINTER_ERROR_ENUM */ +#endif #ifndef WL_POINTER_BUTTON_STATE_ENUM #define WL_POINTER_BUTTON_STATE_ENUM -/** - * @ingroup iface_wl_pointer - * physical button state - * - * Describes the physical state of a button that produced the button - * event. - */ enum wl_pointer_button_state { - /** - * the button is not pressed - */ WL_POINTER_BUTTON_STATE_RELEASED = 0, - /** - * the button is pressed - */ WL_POINTER_BUTTON_STATE_PRESSED = 1, }; -#endif /* WL_POINTER_BUTTON_STATE_ENUM */ +#endif #ifndef WL_POINTER_AXIS_ENUM #define WL_POINTER_AXIS_ENUM -/** - * @ingroup iface_wl_pointer - * axis types - * - * Describes the axis types of scroll events. - */ enum wl_pointer_axis { - /** - * vertical axis - */ WL_POINTER_AXIS_VERTICAL_SCROLL = 0, - /** - * horizontal axis - */ WL_POINTER_AXIS_HORIZONTAL_SCROLL = 1, }; -#endif /* WL_POINTER_AXIS_ENUM */ +#endif #ifndef WL_POINTER_AXIS_SOURCE_ENUM #define WL_POINTER_AXIS_SOURCE_ENUM -/** - * @ingroup iface_wl_pointer - * axis source types - * - * Describes the source types for axis events. This indicates to the - * client how an axis event was physically generated; a client may - * adjust the user interface accordingly. For example, scroll events - * from a "finger" source may be in a smooth coordinate space with - * kinetic scrolling whereas a "wheel" source may be in discrete steps - * of a number of lines. - * - * The "continuous" axis source is a device generating events in a - * continuous coordinate space, but using something other than a - * finger. One example for this source is button-based scrolling where - * the vertical motion of a device is converted to scroll events while - * a button is held down. - * - * The "wheel tilt" axis source indicates that the actual device is a - * wheel but the scroll event is not caused by a rotation but a - * (usually sideways) tilt of the wheel. - */ enum wl_pointer_axis_source { - /** - * a physical wheel rotation - */ WL_POINTER_AXIS_SOURCE_WHEEL = 0, - /** - * finger on a touch surface - */ WL_POINTER_AXIS_SOURCE_FINGER = 1, - /** - * continuous coordinate space - */ WL_POINTER_AXIS_SOURCE_CONTINUOUS = 2, - /** - * a physical wheel tilt - * @since 6 - */ WL_POINTER_AXIS_SOURCE_WHEEL_TILT = 3, }; -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_SOURCE_WHEEL_TILT_SINCE_VERSION 6 -#endif /* WL_POINTER_AXIS_SOURCE_ENUM */ +#endif #ifndef WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM #define WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM -/** - * @ingroup iface_wl_pointer - * axis relative direction - * - * This specifies the direction of the physical motion that caused a - * wl_pointer.axis event, relative to the wl_pointer.axis direction. - */ enum wl_pointer_axis_relative_direction { - /** - * physical motion matches axis direction - */ WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL = 0, - /** - * physical motion is the inverse of the axis direction - */ WL_POINTER_AXIS_RELATIVE_DIRECTION_INVERTED = 1, }; -#endif /* WL_POINTER_AXIS_RELATIVE_DIRECTION_ENUM */ +#endif -/** - * @ingroup iface_wl_pointer - * @struct wl_pointer_listener - */ struct wl_pointer_listener { - /** - * enter event - * - * Notification that this seat's pointer is focused on a certain - * surface. - * - * When a seat's focus enters a surface, the pointer image is - * undefined and a client should respond to this event by setting - * an appropriate pointer image with the set_cursor request. - * @param serial serial number of the enter event - * @param surface surface entered by the pointer - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - */ void (*enter)(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, wl_fixed_t surface_x, wl_fixed_t surface_y); - /** - * leave event - * - * Notification that this seat's pointer is no longer focused on - * a certain surface. - * - * The leave notification is sent before the enter notification for - * the new focus. - * @param serial serial number of the leave event - * @param surface surface left by the pointer - */ void (*leave)(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface); - /** - * pointer motion event - * - * Notification of pointer location change. The arguments - * surface_x and surface_y are the location relative to the focused - * surface. - * @param time timestamp with millisecond granularity - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - */ void (*motion)(void *data, struct wl_pointer *wl_pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y); - /** - * pointer button event - * - * Mouse button click and release notifications. - * - * The location of the click is given by the last motion, warp or - * enter event. The time argument is a timestamp with millisecond - * granularity, with an undefined base. - * - * The button is a button code as defined in the Linux kernel's - * linux/input-event-codes.h header file, e.g. BTN_LEFT. - * - * Any 16-bit button code value is reserved for future additions to - * the kernel's event code list. All other button codes above - * 0xFFFF are currently undefined but may be used in future - * versions of this protocol. - * @param serial serial number of the button event - * @param time timestamp with millisecond granularity - * @param button button that produced the event - * @param state physical state of the button - */ void (*button)(void *data, struct wl_pointer *wl_pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state); - /** - * axis event - * - * Scroll and other axis notifications. - * - * For scroll events (vertical and horizontal scroll axes), the - * value parameter is the length of a vector along the specified - * axis in a coordinate space identical to those of motion events, - * representing a relative movement along the specified axis. - * - * For devices that support movements non-parallel to axes multiple - * axis events will be emitted. - * - * When applicable, for example for touch pads, the server can - * choose to emit scroll events where the motion vector is - * equivalent to a motion event vector. - * - * When applicable, a client can transform its content relative to - * the scroll distance. - * @param time timestamp with millisecond granularity - * @param axis axis type - * @param value length of vector in surface-local coordinate space - */ void (*axis)(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis, wl_fixed_t value); - /** - * end of a pointer event sequence - * - * Indicates the end of a set of events that logically belong - * together. A client is expected to accumulate the data in all - * events within the frame before proceeding. - * - * All wl_pointer events before a wl_pointer.frame event belong - * logically together. For example, in a diagonal scroll motion the - * compositor will send an optional wl_pointer.axis_source event, - * two wl_pointer.axis events (horizontal and vertical) and finally - * a wl_pointer.frame event. The client may use this information to - * calculate a diagonal vector for scrolling. - * - * When multiple wl_pointer.axis events occur within the same - * frame, the motion vector is the combined motion of all events. - * When a wl_pointer.axis and a wl_pointer.axis_stop event occur - * within the same frame, this indicates that axis movement in one - * axis has stopped but continues in the other axis. When multiple - * wl_pointer.axis_stop events occur within the same frame, this - * indicates that these axes stopped in the same instance. - * - * A wl_pointer.frame event is sent for every logical event group, - * even if the group only contains a single wl_pointer event. - * Specifically, a client may get a sequence: motion, frame, - * button, frame, axis, frame, axis_stop, frame. - * - * The wl_pointer.enter and wl_pointer.leave events are logical - * events generated by the compositor and not the hardware. These - * events are also grouped by a wl_pointer.frame. When a pointer - * moves from one surface to another, a compositor should group the - * wl_pointer.leave event within the same wl_pointer.frame. - * However, a client must not rely on wl_pointer.leave and - * wl_pointer.enter being in the same wl_pointer.frame. - * Compositor-specific policies may require the wl_pointer.leave - * and wl_pointer.enter event being split across multiple - * wl_pointer.frame groups. - * @since 5 - */ void (*frame)(void *data, struct wl_pointer *wl_pointer); - /** - * axis source event - * - * Source information for scroll and other axes. - * - * This event does not occur on its own. It is sent before a - * wl_pointer.frame event and carries the source information for - * all events within that frame. - * - * The source specifies how this event was generated. If the source - * is wl_pointer.axis_source.finger, a wl_pointer.axis_stop event - * will be sent when the user lifts the finger off the device. - * - * If the source is wl_pointer.axis_source.wheel, - * wl_pointer.axis_source.wheel_tilt or - * wl_pointer.axis_source.continuous, a wl_pointer.axis_stop event - * may or may not be sent. Whether a compositor sends an axis_stop - * event for these sources is hardware-specific and - * implementation-dependent; clients must not rely on receiving an - * axis_stop event for these scroll sources and should treat scroll - * sequences from these scroll sources as unterminated by default. - * - * This event is optional. If the source is unknown for a - * particular axis event sequence, no event is sent. Only one - * wl_pointer.axis_source event is permitted per frame. - * - * The order of wl_pointer.axis_discrete and wl_pointer.axis_source - * is not guaranteed. - * @param axis_source source of the axis event - * @since 5 - */ void (*axis_source)(void *data, struct wl_pointer *wl_pointer, uint32_t axis_source); - /** - * axis stop event - * - * Stop notification for scroll and other axes. - * - * For some wl_pointer.axis_source types, a wl_pointer.axis_stop - * event is sent to notify a client that the axis sequence has - * terminated. This enables the client to implement kinetic - * scrolling. See the wl_pointer.axis_source documentation for - * information on when this event may be generated. - * - * Any wl_pointer.axis events with the same axis_source after this - * event should be considered as the start of a new axis motion. - * - * The timestamp is to be interpreted identical to the timestamp in - * the wl_pointer.axis event. The timestamp value may be the same - * as a preceding wl_pointer.axis event. - * @param time timestamp with millisecond granularity - * @param axis the axis stopped with this event - * @since 5 - */ void (*axis_stop)(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis); - /** - * axis click event - * - * Discrete step information for scroll and other axes. - * - * This event carries the axis value of the wl_pointer.axis event - * in discrete steps (e.g. mouse wheel clicks). - * - * This event is deprecated with wl_pointer version 8 - this event - * is not sent to clients supporting version 8 or later. - * - * This event does not occur on its own, it is coupled with a - * wl_pointer.axis event that represents this axis value on a - * continuous scale. The protocol guarantees that each - * axis_discrete event is always followed by exactly one axis event - * with the same axis number within the same wl_pointer.frame. Note - * that the protocol allows for other events to occur between the - * axis_discrete and its coupled axis event, including other - * axis_discrete or axis events. A wl_pointer.frame must not - * contain more than one axis_discrete event per axis type. - * - * This event is optional; continuous scrolling devices like - * two-finger scrolling on touchpads do not have discrete steps and - * do not generate this event. - * - * The discrete value carries the directional information. e.g. a - * value of -2 is two steps towards the negative direction of this - * axis. - * - * The axis number is identical to the axis number in the - * associated axis event. - * - * The order of wl_pointer.axis_discrete and wl_pointer.axis_source - * is not guaranteed. - * @param axis axis type - * @param discrete number of steps - * @since 5 - * @deprecated Deprecated since version 8 - */ void (*axis_discrete)(void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t discrete); - /** - * axis high-resolution scroll event - * - * Discrete high-resolution scroll information. - * - * This event carries high-resolution wheel scroll information, - * with each multiple of 120 representing one logical scroll step - * (a wheel detent). For example, an axis_value120 of 30 is one - * quarter of a logical scroll step in the positive direction, a - * value120 of -240 are two logical scroll steps in the negative - * direction within the same hardware event. Clients that rely on - * discrete scrolling should accumulate the value120 to multiples - * of 120 before processing the event. - * - * The value120 must not be zero. - * - * This event replaces the wl_pointer.axis_discrete event in - * clients supporting wl_pointer version 8 or later. - * - * Where a wl_pointer.axis_source event occurs in the same - * wl_pointer.frame, the axis source applies to this event. - * - * The order of wl_pointer.axis_value120 and wl_pointer.axis_source - * is not guaranteed. - * @param axis axis type - * @param value120 scroll distance as fraction of 120 - * @since 8 - */ void (*axis_value120)(void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t value120); - /** - * axis relative physical direction event - * - * Relative directional information of the entity causing the - * axis motion. - * - * For a wl_pointer.axis event, the - * wl_pointer.axis_relative_direction event specifies the movement - * direction of the entity causing the wl_pointer.axis event. For - * example: - if a user's fingers on a touchpad move down and this - * causes a wl_pointer.axis vertical_scroll down event, the - * physical direction is 'identical' - if a user's fingers on a - * touchpad move down and this causes a wl_pointer.axis - * vertical_scroll up scroll up event ('natural scrolling'), the - * physical direction is 'inverted'. - * - * A client may use this information to adjust scroll motion of - * components. Specifically, enabling natural scrolling causes the - * content to change direction compared to traditional scrolling. - * Some widgets like volume control sliders should usually match - * the physical direction regardless of whether natural scrolling - * is active. This event enables clients to match the scroll - * direction of a widget to the physical direction. - * - * This event does not occur on its own, it is coupled with a - * wl_pointer.axis event that represents this axis value. The - * protocol guarantees that each axis_relative_direction event is - * always followed by exactly one axis event with the same axis - * number within the same wl_pointer.frame. Note that the protocol - * allows for other events to occur between the - * axis_relative_direction and its coupled axis event. - * - * The axis number is identical to the axis number in the - * associated axis event. - * - * The order of wl_pointer.axis_relative_direction, - * wl_pointer.axis_discrete and wl_pointer.axis_source is not - * guaranteed. - * @param axis axis type - * @param direction physical direction relative to axis motion - * @since 9 - */ void (*axis_relative_direction)(void *data, struct wl_pointer *wl_pointer, uint32_t axis, uint32_t direction); - /** - * pointer warp event - * - * Notification of pointer location change within a surface. - * - * This location change is not due to events on the input device, - * but because either the surface under the pointer was moved and - * thus the relative position of the pointer changed, or because - * the compositor changed the pointer position in response to an - * event like pointer confinement being exited. - * - * The arguments surface_x and surface_y are the location relative - * to the focused surface. - * - * This event must not occur in the same wl_pointer.frame as a - * wl_pointer.enter or wl_pointer.motion event. - * @param surface_x surface-local x coordinate - * @param surface_y surface-local y coordinate - * @since 11 - */ void (*warp)(void *data, struct wl_pointer *wl_pointer, wl_fixed_t surface_x, wl_fixed_t surface_y); }; -/** - * @ingroup iface_wl_pointer - */ static inline int wl_pointer_add_listener(struct wl_pointer *wl_pointer, const struct wl_pointer_listener *listener, void *data) @@ -5266,72 +1729,28 @@ wl_pointer_add_listener(struct wl_pointer *wl_pointer, #define WL_POINTER_SET_CURSOR 0 #define WL_POINTER_RELEASE 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_BUTTON_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_FRAME_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_SOURCE_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_STOP_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_DISCRETE_SINCE_VERSION 5 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_VALUE120_SINCE_VERSION 8 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_AXIS_RELATIVE_DIRECTION_SINCE_VERSION 9 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_WARP_SINCE_VERSION 11 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_SET_CURSOR_SINCE_VERSION 1 -/** - * @ingroup iface_wl_pointer - */ #define WL_POINTER_RELEASE_SINCE_VERSION 3 -/** @ingroup iface_wl_pointer */ static inline void wl_pointer_set_user_data(struct wl_pointer *wl_pointer, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_pointer, user_data); } -/** @ingroup iface_wl_pointer */ static inline void * wl_pointer_get_user_data(struct wl_pointer *wl_pointer) { @@ -5344,50 +1763,12 @@ wl_pointer_get_version(struct wl_pointer *wl_pointer) return wl_proxy_get_version((struct wl_proxy *) wl_pointer); } -/** @ingroup iface_wl_pointer */ static inline void wl_pointer_destroy(struct wl_pointer *wl_pointer) { wl_proxy_destroy((struct wl_proxy *) wl_pointer); } -/** - * @ingroup iface_wl_pointer - * - * Set the pointer surface, i.e., the surface that contains the - * pointer image (cursor). This request gives the surface the role - * of a cursor. If the surface already has another role, it raises - * a protocol error. - * - * The cursor actually changes only if the pointer - * focus for this device is one of the requesting client's surfaces - * or the surface parameter is the current pointer surface. If - * there was a previous surface set with this request it is - * replaced. If surface is NULL, the pointer image is hidden. - * - * The parameters hotspot_x and hotspot_y define the position of - * the pointer surface relative to the pointer location. Its - * top-left corner is always at (x, y) - (hotspot_x, hotspot_y), - * where (x, y) are the coordinates of the pointer location, in - * surface-local coordinates. - * - * On wl_surface.offset requests to the pointer surface, hotspot_x - * and hotspot_y are decremented by the x and y parameters - * passed to the request. The offset must be applied by - * wl_surface.commit as usual. - * - * The hotspot can also be updated by passing the currently set - * pointer surface to this request with new values for hotspot_x - * and hotspot_y. - * - * The input region is ignored for wl_surfaces with the role of - * a cursor. When the use as a cursor ends, the wl_surface is - * unmapped. - * - * The serial parameter must match the latest wl_pointer.enter - * serial number sent to the client. Otherwise the request will be - * ignored. - */ static inline void wl_pointer_set_cursor(struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, int32_t hotspot_x, int32_t hotspot_y) { @@ -5395,15 +1776,6 @@ wl_pointer_set_cursor(struct wl_pointer *wl_pointer, uint32_t serial, struct wl_ WL_POINTER_SET_CURSOR, NULL, wl_proxy_get_version((struct wl_proxy *) wl_pointer), 0, serial, surface, hotspot_x, hotspot_y); } -/** - * @ingroup iface_wl_pointer - * - * Using this request a client can tell the server that it is not going to - * use the pointer object anymore. - * - * This request destroys the pointer proxy object, so clients must not call - * wl_pointer_destroy() after using this request. - */ static inline void wl_pointer_release(struct wl_pointer *wl_pointer) { @@ -5413,191 +1785,43 @@ wl_pointer_release(struct wl_pointer *wl_pointer) #ifndef WL_KEYBOARD_KEYMAP_FORMAT_ENUM #define WL_KEYBOARD_KEYMAP_FORMAT_ENUM -/** - * @ingroup iface_wl_keyboard - * keyboard mapping format - * - * This specifies the format of the keymap provided to the - * client with the wl_keyboard.keymap event. - */ enum wl_keyboard_keymap_format { - /** - * no keymap; client must understand how to interpret the raw keycode - */ WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP = 0, - /** - * libxkbcommon compatible, null-terminated string; to determine the xkb keycode, clients must add 8 to the key event keycode - */ WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 = 1, }; -#endif /* WL_KEYBOARD_KEYMAP_FORMAT_ENUM */ +#endif #ifndef WL_KEYBOARD_KEY_STATE_ENUM #define WL_KEYBOARD_KEY_STATE_ENUM -/** - * @ingroup iface_wl_keyboard - * physical key state - * - * Describes the physical state of a key that produced the key event. - * - * Since version 10, the key can be in a "repeated" pseudo-state which - * means the same as "pressed", but is used to signal repetition in the - * key event. - * - * The key may only enter the repeated state after entering the pressed - * state and before entering the released state. This event may be - * generated multiple times while the key is down. - */ enum wl_keyboard_key_state { - /** - * key is not pressed - */ WL_KEYBOARD_KEY_STATE_RELEASED = 0, - /** - * key is pressed - */ WL_KEYBOARD_KEY_STATE_PRESSED = 1, - /** - * key was repeated - * @since 10 - */ WL_KEYBOARD_KEY_STATE_REPEATED = 2, }; -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_KEY_STATE_REPEATED_SINCE_VERSION 10 -#endif /* WL_KEYBOARD_KEY_STATE_ENUM */ +#endif -/** - * @ingroup iface_wl_keyboard - * @struct wl_keyboard_listener - */ struct wl_keyboard_listener { - /** - * keyboard mapping - * - * This event provides a file descriptor to the client which can - * be memory-mapped in read-only mode to provide a keyboard mapping - * description. - * - * From version 7 onwards, the fd must be mapped with MAP_PRIVATE - * by the recipient, as MAP_SHARED may fail. - * @param format keymap format - * @param fd keymap file descriptor - * @param size keymap size, in bytes - */ void (*keymap)(void *data, struct wl_keyboard *wl_keyboard, uint32_t format, int32_t fd, uint32_t size); - /** - * enter event - * - * Notification that this seat's keyboard focus is on a certain - * surface. - * - * The compositor must send the wl_keyboard.modifiers event after - * this event. - * - * In the wl_keyboard logical state, this event sets the active - * surface to the surface argument and the keys currently logically - * down to the keys in the keys argument. The compositor must not - * send this event if the wl_keyboard already had an active surface - * immediately before this event. - * - * Clients should not use the list of pressed keys to emulate - * key-press events. The order of keys in the list is unspecified. - * @param serial serial number of the enter event - * @param surface surface gaining keyboard focus - * @param keys the keys currently logically down - */ void (*enter)(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, struct wl_surface *surface, struct wl_array *keys); - /** - * leave event - * - * Notification that this seat's keyboard focus is no longer on a - * certain surface. - * - * The leave notification is sent before the enter notification for - * the new focus. - * - * In the wl_keyboard logical state, this event resets all values - * to their defaults. The compositor must not send this event if - * the active surface of the wl_keyboard was not equal to the - * surface argument immediately before this event. - * @param serial serial number of the leave event - * @param surface surface that lost keyboard focus - */ void (*leave)(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, struct wl_surface *surface); - /** - * key event - * - * A key was pressed or released. The time argument is a - * timestamp with millisecond granularity, with an undefined base. - * - * The key is a platform-specific key code that can be interpreted - * by feeding it to the keyboard mapping (see the keymap event). - * - * If this event produces a change in modifiers, then the resulting - * wl_keyboard.modifiers event must be sent after this event. - * - * In the wl_keyboard logical state, this event adds the key to the - * keys currently logically down (if the state argument is pressed) - * or removes the key from the keys currently logically down (if - * the state argument is released). The compositor must not send - * this event if the wl_keyboard did not have an active surface - * immediately before this event. The compositor must not send this - * event if state is pressed (resp. released) and the key was - * already logically down (resp. was not logically down) - * immediately before this event. - * - * Since version 10, compositors may send key events with the - * "repeated" key state when a wl_keyboard.repeat_info event with a - * rate argument of 0 has been received. This allows the compositor - * to take over the responsibility of key repetition. - * @param serial serial number of the key event - * @param time timestamp with millisecond granularity - * @param key key that produced the event - * @param state physical state of the key - */ void (*key)(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state); - /** - * modifier and group state - * - * Notifies clients that the modifier and/or group state has - * changed, and it should update its local state. - * - * The compositor may send this event without a surface of the - * client having keyboard focus, for example to tie modifier - * information to pointer focus instead. If a modifier event with - * pressed modifiers is sent without a prior enter event, the - * client can assume the modifier state is valid until it receives - * the next wl_keyboard.modifiers event. In order to reset the - * modifier state again, the compositor can send a - * wl_keyboard.modifiers event with no pressed modifiers. - * - * In the wl_keyboard logical state, this event updates the - * modifiers and group. - * @param serial serial number of the modifiers event - * @param mods_depressed depressed modifiers - * @param mods_latched latched modifiers - * @param mods_locked locked modifiers - * @param group keyboard layout - */ void (*modifiers)(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, @@ -5605,35 +1829,12 @@ struct wl_keyboard_listener { uint32_t mods_latched, uint32_t mods_locked, uint32_t group); - /** - * repeat rate and delay - * - * Informs the client about the keyboard's repeat rate and delay. - * - * This event is sent as soon as the wl_keyboard object has been - * created, and is guaranteed to be received by the client before - * any key press event. - * - * Negative values for either rate or delay are illegal. A rate of - * zero will disable any repeating (regardless of the value of - * delay). - * - * This event can be sent later on as well with a new value if - * necessary, so clients should continue listening for the event - * past the creation of wl_keyboard. - * @param rate the rate of repeating keys in characters per second - * @param delay delay in milliseconds since key down until repeating starts - * @since 4 - */ void (*repeat_info)(void *data, struct wl_keyboard *wl_keyboard, int32_t rate, int32_t delay); }; -/** - * @ingroup iface_wl_keyboard - */ static inline int wl_keyboard_add_listener(struct wl_keyboard *wl_keyboard, const struct wl_keyboard_listener *listener, void *data) @@ -5644,44 +1845,21 @@ wl_keyboard_add_listener(struct wl_keyboard *wl_keyboard, #define WL_KEYBOARD_RELEASE 0 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_KEYMAP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_KEY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_MODIFIERS_SINCE_VERSION 1 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_REPEAT_INFO_SINCE_VERSION 4 -/** - * @ingroup iface_wl_keyboard - */ #define WL_KEYBOARD_RELEASE_SINCE_VERSION 3 -/** @ingroup iface_wl_keyboard */ static inline void wl_keyboard_set_user_data(struct wl_keyboard *wl_keyboard, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_keyboard, user_data); } -/** @ingroup iface_wl_keyboard */ static inline void * wl_keyboard_get_user_data(struct wl_keyboard *wl_keyboard) { @@ -5694,16 +1872,12 @@ wl_keyboard_get_version(struct wl_keyboard *wl_keyboard) return wl_proxy_get_version((struct wl_proxy *) wl_keyboard); } -/** @ingroup iface_wl_keyboard */ static inline void wl_keyboard_destroy(struct wl_keyboard *wl_keyboard) { wl_proxy_destroy((struct wl_proxy *) wl_keyboard); } -/** - * @ingroup iface_wl_keyboard - */ static inline void wl_keyboard_release(struct wl_keyboard *wl_keyboard) { @@ -5711,25 +1885,7 @@ wl_keyboard_release(struct wl_keyboard *wl_keyboard) WL_KEYBOARD_RELEASE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_keyboard), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_touch - * @struct wl_touch_listener - */ struct wl_touch_listener { - /** - * touch down event and beginning of a touch sequence - * - * A new touch point has appeared on the surface. This touch - * point is assigned a unique ID. Future events from this touch - * point reference this ID. The ID ceases to be valid after a touch - * up event and may be reused in the future. - * @param serial serial number of the touch down event - * @param time timestamp with millisecond granularity - * @param surface surface touched - * @param id the unique ID of this touch point - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ void (*down)(void *data, struct wl_touch *wl_touch, uint32_t serial, @@ -5738,142 +1894,32 @@ struct wl_touch_listener { int32_t id, wl_fixed_t x, wl_fixed_t y); - /** - * end of a touch event sequence - * - * The touch point has disappeared. No further events will be - * sent for this touch point and the touch point's ID is released - * and may be reused in a future touch down event. - * @param serial serial number of the touch up event - * @param time timestamp with millisecond granularity - * @param id the unique ID of this touch point - */ void (*up)(void *data, struct wl_touch *wl_touch, uint32_t serial, uint32_t time, int32_t id); - /** - * update of touch point coordinates - * - * A touch point has changed coordinates. - * @param time timestamp with millisecond granularity - * @param id the unique ID of this touch point - * @param x surface-local x coordinate - * @param y surface-local y coordinate - */ void (*motion)(void *data, struct wl_touch *wl_touch, uint32_t time, int32_t id, wl_fixed_t x, wl_fixed_t y); - /** - * end of touch frame event - * - * Indicates the end of a set of events that logically belong - * together. A client is expected to accumulate the data in all - * events within the frame before proceeding. - * - * A wl_touch.frame terminates at least one event but otherwise no - * guarantee is provided about the set of events within a frame. A - * client must assume that any state not updated in a frame is - * unchanged from the previously known state. - */ void (*frame)(void *data, struct wl_touch *wl_touch); - /** - * touch session cancelled - * - * Sent if the compositor decides the touch stream is a global - * gesture. No further events are sent to the clients from that - * particular gesture. Touch cancellation applies to all touch - * points currently active on this client's surface. The client is - * responsible for finalizing the touch points, future touch points - * on this surface may reuse the touch point ID. - * - * No frame event is required after the cancel event. - */ void (*cancel)(void *data, struct wl_touch *wl_touch); - /** - * update shape of touch point - * - * Sent when a touchpoint has changed its shape. - * - * This event does not occur on its own. It is sent before a - * wl_touch.frame event and carries the new shape information for - * any previously reported, or new touch points of that frame. - * - * Other events describing the touch point such as wl_touch.down, - * wl_touch.motion or wl_touch.orientation may be sent within the - * same wl_touch.frame. A client should treat these events as a - * single logical touch point update. The order of wl_touch.shape, - * wl_touch.orientation and wl_touch.motion is not guaranteed. A - * wl_touch.down event is guaranteed to occur before the first - * wl_touch.shape event for this touch ID but both events may occur - * within the same wl_touch.frame. - * - * A touchpoint shape is approximated by an ellipse through the - * major and minor axis length. The major axis length describes the - * longer diameter of the ellipse, while the minor axis length - * describes the shorter diameter. Major and minor are orthogonal - * and both are specified in surface-local coordinates. The center - * of the ellipse is always at the touchpoint location as reported - * by wl_touch.down or wl_touch.motion. - * - * This event is only sent by the compositor if the touch device - * supports shape reports. The client has to make reasonable - * assumptions about the shape if it did not receive this event. - * @param id the unique ID of this touch point - * @param major length of the major axis in surface-local coordinates - * @param minor length of the minor axis in surface-local coordinates - * @since 6 - */ void (*shape)(void *data, struct wl_touch *wl_touch, int32_t id, wl_fixed_t major, wl_fixed_t minor); - /** - * update orientation of touch point - * - * Sent when a touchpoint has changed its orientation. - * - * This event does not occur on its own. It is sent before a - * wl_touch.frame event and carries the new shape information for - * any previously reported, or new touch points of that frame. - * - * Other events describing the touch point such as wl_touch.down, - * wl_touch.motion or wl_touch.shape may be sent within the same - * wl_touch.frame. A client should treat these events as a single - * logical touch point update. The order of wl_touch.shape, - * wl_touch.orientation and wl_touch.motion is not guaranteed. A - * wl_touch.down event is guaranteed to occur before the first - * wl_touch.orientation event for this touch ID but both events may - * occur within the same wl_touch.frame. - * - * The orientation describes the clockwise angle of a touchpoint's - * major axis to the positive surface y-axis and is normalized to - * the -180 to +180 degree range. The granularity of orientation - * depends on the touch device, some devices only support binary - * rotation values between 0 and 90 degrees. - * - * This event is only sent by the compositor if the touch device - * supports orientation reports. - * @param id the unique ID of this touch point - * @param orientation angle between major axis and positive surface y-axis in degrees - * @since 6 - */ void (*orientation)(void *data, struct wl_touch *wl_touch, int32_t id, wl_fixed_t orientation); }; -/** - * @ingroup iface_wl_touch - */ static inline int wl_touch_add_listener(struct wl_touch *wl_touch, const struct wl_touch_listener *listener, void *data) @@ -5884,48 +1930,22 @@ wl_touch_add_listener(struct wl_touch *wl_touch, #define WL_TOUCH_RELEASE 0 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_DOWN_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_UP_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_MOTION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_FRAME_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_CANCEL_SINCE_VERSION 1 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_SHAPE_SINCE_VERSION 6 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_ORIENTATION_SINCE_VERSION 6 -/** - * @ingroup iface_wl_touch - */ #define WL_TOUCH_RELEASE_SINCE_VERSION 3 -/** @ingroup iface_wl_touch */ static inline void wl_touch_set_user_data(struct wl_touch *wl_touch, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_touch, user_data); } -/** @ingroup iface_wl_touch */ static inline void * wl_touch_get_user_data(struct wl_touch *wl_touch) { @@ -5938,16 +1958,12 @@ wl_touch_get_version(struct wl_touch *wl_touch) return wl_proxy_get_version((struct wl_proxy *) wl_touch); } -/** @ingroup iface_wl_touch */ static inline void wl_touch_destroy(struct wl_touch *wl_touch) { wl_proxy_destroy((struct wl_proxy *) wl_touch); } -/** - * @ingroup iface_wl_touch - */ static inline void wl_touch_release(struct wl_touch *wl_touch) { @@ -5957,153 +1973,39 @@ wl_touch_release(struct wl_touch *wl_touch) #ifndef WL_OUTPUT_SUBPIXEL_ENUM #define WL_OUTPUT_SUBPIXEL_ENUM -/** - * @ingroup iface_wl_output - * subpixel geometry information - * - * This enumeration describes how the physical - * pixels on an output are laid out. - */ enum wl_output_subpixel { - /** - * unknown geometry - */ WL_OUTPUT_SUBPIXEL_UNKNOWN = 0, - /** - * no geometry - */ WL_OUTPUT_SUBPIXEL_NONE = 1, - /** - * horizontal RGB - */ WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB = 2, - /** - * horizontal BGR - */ WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR = 3, - /** - * vertical RGB - */ WL_OUTPUT_SUBPIXEL_VERTICAL_RGB = 4, - /** - * vertical BGR - */ WL_OUTPUT_SUBPIXEL_VERTICAL_BGR = 5, }; -#endif /* WL_OUTPUT_SUBPIXEL_ENUM */ +#endif #ifndef WL_OUTPUT_TRANSFORM_ENUM #define WL_OUTPUT_TRANSFORM_ENUM -/** - * @ingroup iface_wl_output - * transformation applied to buffer contents - * - * This describes transformations that clients and compositors apply to - * buffer contents. - * - * The flipped values correspond to an initial flip around a - * vertical axis followed by rotation. - * - * The purpose is mainly to allow clients to render accordingly and - * tell the compositor, so that for fullscreen surfaces, the - * compositor will still be able to scan out directly from client - * surfaces. - */ enum wl_output_transform { - /** - * no transform - */ WL_OUTPUT_TRANSFORM_NORMAL = 0, - /** - * 90 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_90 = 1, - /** - * 180 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_180 = 2, - /** - * 270 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_270 = 3, - /** - * 180 degree flip around a vertical axis - */ WL_OUTPUT_TRANSFORM_FLIPPED = 4, - /** - * flip and rotate 90 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_FLIPPED_90 = 5, - /** - * flip and rotate 180 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_FLIPPED_180 = 6, - /** - * flip and rotate 270 degrees counter-clockwise - */ WL_OUTPUT_TRANSFORM_FLIPPED_270 = 7, }; -#endif /* WL_OUTPUT_TRANSFORM_ENUM */ +#endif #ifndef WL_OUTPUT_MODE_ENUM #define WL_OUTPUT_MODE_ENUM -/** - * @ingroup iface_wl_output - * mode information - * - * These flags describe properties of an output mode. - * They are used in the flags bitfield of the mode event. - */ enum wl_output_mode { - /** - * indicates this is the current mode - */ WL_OUTPUT_MODE_CURRENT = 0x1, - /** - * indicates this is the preferred mode - */ WL_OUTPUT_MODE_PREFERRED = 0x2, }; -#endif /* WL_OUTPUT_MODE_ENUM */ +#endif -/** - * @ingroup iface_wl_output - * @struct wl_output_listener - */ struct wl_output_listener { - /** - * properties of the output - * - * The geometry event describes geometric properties of the - * output. The event is sent when binding to the output object and - * whenever any of the properties change. - * - * The physical size can be set to zero if it doesn't make sense - * for this output (e.g. for projectors or virtual outputs). - * - * The geometry event will be followed by a done event (starting - * from version 2). - * - * Clients should use wl_surface.preferred_buffer_transform instead - * of the transform advertised by this event to find the preferred - * buffer transform to use for a surface. - * - * Note: wl_output only advertises partial information about the - * output position and identification. Some compositors, for - * instance those not implementing a desktop-style output layout or - * those exposing virtual outputs, might fake this information. - * Instead of using x and y, clients should use - * xdg_output.logical_position. Instead of using make and model, - * clients should use name and description. - * @param x x position within the global compositor space - * @param y y position within the global compositor space - * @param physical_width width in millimeters of the output - * @param physical_height height in millimeters of the output - * @param subpixel subpixel orientation of the output - * @param make textual description of the manufacturer - * @param model textual description of the model - * @param transform additional transformation applied to buffer contents during presentation - */ void (*geometry)(void *data, struct wl_output *wl_output, int32_t x, @@ -6114,159 +2016,25 @@ struct wl_output_listener { const char *make, const char *model, int32_t transform); - /** - * advertise available modes for the output - * - * The mode event describes an available mode for the output. - * - * The event is sent when binding to the output object and there - * will always be one mode, the current mode. The event is sent - * again if an output changes mode, for the mode that is now - * current. In other words, the current mode is always the last - * mode that was received with the current flag set. - * - * Non-current modes are deprecated. A compositor can decide to - * only advertise the current mode and never send other modes. - * Clients should not rely on non-current modes. - * - * The size of a mode is given in physical hardware units of the - * output device. This is not necessarily the same as the output - * size in the global compositor space. For instance, the output - * may be scaled, as described in wl_output.scale, or transformed, - * as described in wl_output.transform. Clients willing to retrieve - * the output size in the global compositor space should use - * xdg_output.logical_size instead. - * - * The vertical refresh rate can be set to zero if it doesn't make - * sense for this output (e.g. for virtual outputs). - * - * The mode event will be followed by a done event (starting from - * version 2). - * - * Clients should not use the refresh rate to schedule frames. - * Instead, they should use the wl_surface.frame event or the - * presentation-time protocol. - * - * Note: this information is not always meaningful for all outputs. - * Some compositors, such as those exposing virtual outputs, might - * fake the refresh rate or the size. - * @param flags bitfield of mode flags - * @param width width of the mode in hardware units - * @param height height of the mode in hardware units - * @param refresh vertical refresh rate in mHz - */ void (*mode)(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh); - /** - * sent all information about output - * - * This event is sent after all other properties have been sent - * after binding to the output object and after any other property - * changes done after that. This allows changes to the output - * properties to be seen as atomic, even if they happen via - * multiple events. - * @since 2 - */ void (*done)(void *data, struct wl_output *wl_output); - /** - * output scaling properties - * - * This event contains scaling geometry information that is not - * in the geometry event. It may be sent after binding the output - * object or if the output scale changes later. The compositor will - * emit a non-zero, positive value for scale. If it is not sent, - * the client should assume a scale of 1. - * - * A scale larger than 1 means that the compositor will - * automatically scale surface buffers by this amount when - * rendering. This is used for very high resolution displays where - * applications rendering at the native resolution would be too - * small to be legible. - * - * Clients should use wl_surface.preferred_buffer_scale instead of - * this event to find the preferred buffer scale to use for a - * surface. - * - * The scale event will be followed by a done event. - * @param factor scaling factor of output - * @since 2 - */ void (*scale)(void *data, struct wl_output *wl_output, int32_t factor); - /** - * name of this output - * - * Many compositors will assign user-friendly names to their - * outputs, show them to the user, allow the user to refer to an - * output, etc. The client may wish to know this name as well to - * offer the user similar behaviors. - * - * The name is a UTF-8 string with no convention defined for its - * contents. Each name is unique among all wl_output globals. The - * name is only guaranteed to be unique for the compositor - * instance. - * - * The same output name is used for all clients for a given - * wl_output global. Thus, the name can be shared across processes - * to refer to a specific wl_output global. - * - * The name is not guaranteed to be persistent across sessions, - * thus cannot be used to reliably identify an output in e.g. - * configuration files. - * - * Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. - * However, do not assume that the name is a reflection of an - * underlying DRM connector, X11 connection, etc. - * - * The name event is sent after binding the output object. This - * event is only sent once per output object, and the name does not - * change over the lifetime of the wl_output global. - * - * Compositors may re-use the same output name if the wl_output - * global is destroyed and re-created later. Compositors should - * avoid re-using the same name if possible. - * - * The name event will be followed by a done event. - * @param name output name - * @since 4 - */ void (*name)(void *data, struct wl_output *wl_output, const char *name); - /** - * human-readable description of this output - * - * Many compositors can produce human-readable descriptions of - * their outputs. The client may wish to know this description as - * well, e.g. for output selection purposes. - * - * The description is a UTF-8 string with no convention defined for - * its contents. The description is not guaranteed to be unique - * among all wl_output globals. Examples might include 'Foocorp 11" - * Display' or 'Virtual X11 output via :1'. - * - * The description event is sent after binding the output object - * and whenever the description changes. The description is - * optional, and may not be sent at all. - * - * The description event will be followed by a done event. - * @param description output description - * @since 4 - */ void (*description)(void *data, struct wl_output *wl_output, const char *description); }; -/** - * @ingroup iface_wl_output - */ static inline int wl_output_add_listener(struct wl_output *wl_output, const struct wl_output_listener *listener, void *data) @@ -6277,44 +2045,21 @@ wl_output_add_listener(struct wl_output *wl_output, #define WL_OUTPUT_RELEASE 0 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_GEOMETRY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_MODE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_DONE_SINCE_VERSION 2 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_SCALE_SINCE_VERSION 2 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_NAME_SINCE_VERSION 4 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_DESCRIPTION_SINCE_VERSION 4 -/** - * @ingroup iface_wl_output - */ #define WL_OUTPUT_RELEASE_SINCE_VERSION 3 -/** @ingroup iface_wl_output */ static inline void wl_output_set_user_data(struct wl_output *wl_output, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_output, user_data); } -/** @ingroup iface_wl_output */ static inline void * wl_output_get_user_data(struct wl_output *wl_output) { @@ -6327,19 +2072,12 @@ wl_output_get_version(struct wl_output *wl_output) return wl_proxy_get_version((struct wl_proxy *) wl_output); } -/** @ingroup iface_wl_output */ static inline void wl_output_destroy(struct wl_output *wl_output) { wl_proxy_destroy((struct wl_proxy *) wl_output); } -/** - * @ingroup iface_wl_output - * - * Using this request a client can tell the server that it is not going to - * use the output object anymore. - */ static inline void wl_output_release(struct wl_output *wl_output) { @@ -6352,27 +2090,16 @@ wl_output_release(struct wl_output *wl_output) #define WL_REGION_SUBTRACT 2 -/** - * @ingroup iface_wl_region - */ #define WL_REGION_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_region - */ #define WL_REGION_ADD_SINCE_VERSION 1 -/** - * @ingroup iface_wl_region - */ #define WL_REGION_SUBTRACT_SINCE_VERSION 1 -/** @ingroup iface_wl_region */ static inline void wl_region_set_user_data(struct wl_region *wl_region, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_region, user_data); } -/** @ingroup iface_wl_region */ static inline void * wl_region_get_user_data(struct wl_region *wl_region) { @@ -6385,11 +2112,6 @@ wl_region_get_version(struct wl_region *wl_region) return wl_proxy_get_version((struct wl_proxy *) wl_region); } -/** - * @ingroup iface_wl_region - * - * Destroy the region. This will invalidate the object ID. - */ static inline void wl_region_destroy(struct wl_region *wl_region) { @@ -6397,11 +2119,6 @@ wl_region_destroy(struct wl_region *wl_region) WL_REGION_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_region), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_region - * - * Add the specified rectangle to the region. - */ static inline void wl_region_add(struct wl_region *wl_region, int32_t x, int32_t y, int32_t width, int32_t height) { @@ -6409,11 +2126,6 @@ wl_region_add(struct wl_region *wl_region, int32_t x, int32_t y, int32_t width, WL_REGION_ADD, NULL, wl_proxy_get_version((struct wl_proxy *) wl_region), 0, x, y, width, height); } -/** - * @ingroup iface_wl_region - * - * Subtract the specified rectangle from the region. - */ static inline void wl_region_subtract(struct wl_region *wl_region, int32_t x, int32_t y, int32_t width, int32_t height) { @@ -6424,38 +2136,24 @@ wl_region_subtract(struct wl_region *wl_region, int32_t x, int32_t y, int32_t wi #ifndef WL_SUBCOMPOSITOR_ERROR_ENUM #define WL_SUBCOMPOSITOR_ERROR_ENUM enum wl_subcompositor_error { - /** - * the to-be sub-surface is invalid - */ WL_SUBCOMPOSITOR_ERROR_BAD_SURFACE = 0, - /** - * the to-be sub-surface parent is invalid - */ WL_SUBCOMPOSITOR_ERROR_BAD_PARENT = 1, }; -#endif /* WL_SUBCOMPOSITOR_ERROR_ENUM */ +#endif #define WL_SUBCOMPOSITOR_DESTROY 0 #define WL_SUBCOMPOSITOR_GET_SUBSURFACE 1 -/** - * @ingroup iface_wl_subcompositor - */ #define WL_SUBCOMPOSITOR_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subcompositor - */ #define WL_SUBCOMPOSITOR_GET_SUBSURFACE_SINCE_VERSION 1 -/** @ingroup iface_wl_subcompositor */ static inline void wl_subcompositor_set_user_data(struct wl_subcompositor *wl_subcompositor, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_subcompositor, user_data); } -/** @ingroup iface_wl_subcompositor */ static inline void * wl_subcompositor_get_user_data(struct wl_subcompositor *wl_subcompositor) { @@ -6468,13 +2166,6 @@ wl_subcompositor_get_version(struct wl_subcompositor *wl_subcompositor) return wl_proxy_get_version((struct wl_proxy *) wl_subcompositor); } -/** - * @ingroup iface_wl_subcompositor - * - * Informs the server that the client will not be using this - * protocol object anymore. This does not affect any other - * objects, wl_subsurface objects included. - */ static inline void wl_subcompositor_destroy(struct wl_subcompositor *wl_subcompositor) { @@ -6482,29 +2173,6 @@ wl_subcompositor_destroy(struct wl_subcompositor *wl_subcompositor) WL_SUBCOMPOSITOR_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_subcompositor), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_subcompositor - * - * Create a sub-surface interface for the given surface, and - * associate it with the given parent surface. This turns a - * plain wl_surface into a sub-surface. - * - * The to-be sub-surface must not already have another role, and it - * must not have an existing wl_subsurface object. Otherwise the - * bad_surface protocol error is raised. - * - * Adding sub-surfaces to a parent is a double-buffered operation on the - * parent (see wl_surface.commit). The effect of adding a sub-surface - * becomes visible on the next time the state of the parent surface is - * applied. - * - * The parent surface must not be one of the child surface's descendants, - * and the parent must be different from the child surface, otherwise the - * bad_parent protocol error is raised. - * - * This request modifies the behaviour of wl_surface.commit request on - * the sub-surface, see the documentation on wl_subsurface interface. - */ static inline struct wl_subsurface * wl_subcompositor_get_subsurface(struct wl_subcompositor *wl_subcompositor, struct wl_surface *surface, struct wl_surface *parent) { @@ -6519,12 +2187,9 @@ wl_subcompositor_get_subsurface(struct wl_subcompositor *wl_subcompositor, struc #ifndef WL_SUBSURFACE_ERROR_ENUM #define WL_SUBSURFACE_ERROR_ENUM enum wl_subsurface_error { - /** - * wl_surface is not a sibling or the parent - */ WL_SUBSURFACE_ERROR_BAD_SURFACE = 0, }; -#endif /* WL_SUBSURFACE_ERROR_ENUM */ +#endif #define WL_SUBSURFACE_DESTROY 0 #define WL_SUBSURFACE_SET_POSITION 1 @@ -6534,39 +2199,19 @@ enum wl_subsurface_error { #define WL_SUBSURFACE_SET_DESYNC 5 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_SET_POSITION_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_PLACE_ABOVE_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_PLACE_BELOW_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_SET_SYNC_SINCE_VERSION 1 -/** - * @ingroup iface_wl_subsurface - */ #define WL_SUBSURFACE_SET_DESYNC_SINCE_VERSION 1 -/** @ingroup iface_wl_subsurface */ static inline void wl_subsurface_set_user_data(struct wl_subsurface *wl_subsurface, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_subsurface, user_data); } -/** @ingroup iface_wl_subsurface */ static inline void * wl_subsurface_get_user_data(struct wl_subsurface *wl_subsurface) { @@ -6579,14 +2224,6 @@ wl_subsurface_get_version(struct wl_subsurface *wl_subsurface) return wl_proxy_get_version((struct wl_proxy *) wl_subsurface); } -/** - * @ingroup iface_wl_subsurface - * - * The sub-surface interface is removed from the wl_surface object - * that was turned into a sub-surface with a - * wl_subcompositor.get_subsurface request. The wl_surface's association - * to the parent is deleted. The wl_surface is unmapped immediately. - */ static inline void wl_subsurface_destroy(struct wl_subsurface *wl_subsurface) { @@ -6594,22 +2231,6 @@ wl_subsurface_destroy(struct wl_subsurface *wl_subsurface) WL_SUBSURFACE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_subsurface), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_subsurface - * - * This sets the position of the sub-surface, relative to the parent - * surface. - * - * The sub-surface will be moved so that its origin (top left - * corner pixel) will be at the location x, y of the parent surface - * coordinate system. The coordinates are not restricted to the parent - * surface area. Negative values are allowed. - * - * The initial position is 0, 0. - * - * Position is double-buffered state on the parent surface, see - * wl_subsurface and wl_surface.commit for more information. - */ static inline void wl_subsurface_set_position(struct wl_subsurface *wl_subsurface, int32_t x, int32_t y) { @@ -6617,21 +2238,6 @@ wl_subsurface_set_position(struct wl_subsurface *wl_subsurface, int32_t x, int32 WL_SUBSURFACE_SET_POSITION, NULL, wl_proxy_get_version((struct wl_proxy *) wl_subsurface), 0, x, y); } -/** - * @ingroup iface_wl_subsurface - * - * This sub-surface is taken from the stack, and put back just - * above the reference surface, changing the z-order of the sub-surfaces. - * The reference surface must be one of the sibling surfaces, or the - * parent surface. Using any other surface, including this sub-surface, - * will cause a protocol error. - * - * A new sub-surface is initially added as the top-most in the stack - * of its siblings and parent. - * - * Z-order is double-buffered state on the parent surface, see - * wl_subsurface and wl_surface.commit for more information. - */ static inline void wl_subsurface_place_above(struct wl_subsurface *wl_subsurface, struct wl_surface *sibling) { @@ -6639,13 +2245,6 @@ wl_subsurface_place_above(struct wl_subsurface *wl_subsurface, struct wl_surface WL_SUBSURFACE_PLACE_ABOVE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_subsurface), 0, sibling); } -/** - * @ingroup iface_wl_subsurface - * - * The sub-surface is placed just below the reference surface. - * - * See wl_subsurface.place_above. - */ static inline void wl_subsurface_place_below(struct wl_subsurface *wl_subsurface, struct wl_surface *sibling) { @@ -6653,14 +2252,6 @@ wl_subsurface_place_below(struct wl_subsurface *wl_subsurface, struct wl_surface WL_SUBSURFACE_PLACE_BELOW, NULL, wl_proxy_get_version((struct wl_proxy *) wl_subsurface), 0, sibling); } -/** - * @ingroup iface_wl_subsurface - * - * Change the commit behaviour of the sub-surface to synchronized - * mode. - * - * See wl_subsurface and wl_surface.commit for more information. - */ static inline void wl_subsurface_set_sync(struct wl_subsurface *wl_subsurface) { @@ -6668,14 +2259,6 @@ wl_subsurface_set_sync(struct wl_subsurface *wl_subsurface) WL_SUBSURFACE_SET_SYNC, NULL, wl_proxy_get_version((struct wl_proxy *) wl_subsurface), 0); } -/** - * @ingroup iface_wl_subsurface - * - * Change the commit behaviour of the sub-surface to desynchronized - * mode. - * - * See wl_subsurface and wl_surface.commit for more information. - */ static inline void wl_subsurface_set_desync(struct wl_subsurface *wl_subsurface) { @@ -6685,46 +2268,26 @@ wl_subsurface_set_desync(struct wl_subsurface *wl_subsurface) #ifndef WL_FIXES_ERROR_ENUM #define WL_FIXES_ERROR_ENUM -/** - * @ingroup iface_wl_fixes - * wl_fixes error values - * - * These errors can be emitted in response to wl_fixes requests. - */ enum wl_fixes_error { - /** - * unknown global or the global is not removed - */ WL_FIXES_ERROR_INVALID_ACK_REMOVE = 0, }; -#endif /* WL_FIXES_ERROR_ENUM */ +#endif #define WL_FIXES_DESTROY 0 #define WL_FIXES_DESTROY_REGISTRY 1 #define WL_FIXES_ACK_GLOBAL_REMOVE 2 -/** - * @ingroup iface_wl_fixes - */ #define WL_FIXES_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_fixes - */ #define WL_FIXES_DESTROY_REGISTRY_SINCE_VERSION 1 -/** - * @ingroup iface_wl_fixes - */ #define WL_FIXES_ACK_GLOBAL_REMOVE_SINCE_VERSION 2 -/** @ingroup iface_wl_fixes */ static inline void wl_fixes_set_user_data(struct wl_fixes *wl_fixes, void *user_data) { wl_proxy_set_user_data((struct wl_proxy *) wl_fixes, user_data); } -/** @ingroup iface_wl_fixes */ static inline void * wl_fixes_get_user_data(struct wl_fixes *wl_fixes) { @@ -6737,9 +2300,6 @@ wl_fixes_get_version(struct wl_fixes *wl_fixes) return wl_proxy_get_version((struct wl_proxy *) wl_fixes); } -/** - * @ingroup iface_wl_fixes - */ static inline void wl_fixes_destroy(struct wl_fixes *wl_fixes) { @@ -6747,19 +2307,6 @@ wl_fixes_destroy(struct wl_fixes *wl_fixes) WL_FIXES_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_fixes), WL_MARSHAL_FLAG_DESTROY); } -/** - * @ingroup iface_wl_fixes - * - * This request destroys a wl_registry object. - * - * The client should no longer use the wl_registry after making this - * request. - * - * The compositor will emit a wl_display.delete_id event with the object ID - * of the registry and will no longer emit any events on the registry. The - * client should re-use the object ID once it receives the - * wl_display.delete_id event. - */ static inline void wl_fixes_destroy_registry(struct wl_fixes *wl_fixes, struct wl_registry *registry) { @@ -6767,32 +2314,6 @@ wl_fixes_destroy_registry(struct wl_fixes *wl_fixes, struct wl_registry *registr WL_FIXES_DESTROY_REGISTRY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_fixes), 0, registry); } -/** - * @ingroup iface_wl_fixes - * - * Acknowledge the removal of the specified global. - * - * If no global with the specified name exists or the global is not removed, - * the wl_fixes.invalid_ack_remove protocol error will be posted. - * - * Due to the Wayland protocol being asynchronous, the wl_global objects - * cannot be destroyed immediately. For example, if a wl_global is removed - * and a client attempts to bind that global around same time, it can - * result in a protocol error due to an unknown global name in the bind - * request. - * - * In order to avoid crashing clients, the compositor should remove the - * wl_global once it is guaranteed that no more bind requests will come. - * - * The wl_fixes.ack_global_remove() request is used to signal to the - * compositor that the client will not bind the given global anymore. After - * all clients acknowledge the removal of the global, the compositor can - * safely destroy it. - * - * The client must call the wl_fixes.ack_global_remove() request in - * response to a wl_registry.global_remove() event even if it did not bind - * the corresponding global. - */ static inline void wl_fixes_ack_global_remove(struct wl_fixes *wl_fixes, struct wl_registry *registry, uint32_t name) { diff --git a/tools/vr/third_party/wayland/wayland-client.h b/tools/vr/third_party/wayland/wayland-client.h index 9f70fa3fb..dd6676f6a 100644 --- a/tools/vr/third_party/wayland/wayland-client.h +++ b/tools/vr/third_party/wayland/wayland-client.h @@ -23,15 +23,6 @@ * SOFTWARE. */ -/** \file - * - * \brief Include the client API and protocol C API. - * - * \warning Use of this header file is discouraged. Prefer including - * wayland-client-core.h instead, which does not include the - * client protocol header and as such only defines the library - * API. - */ #ifndef WAYLAND_CLIENT_H #define WAYLAND_CLIENT_H diff --git a/tools/vr/third_party/wayland/wayland-util.h b/tools/vr/third_party/wayland/wayland-util.h index 98c72fded..d411ddd4f 100644 --- a/tools/vr/third_party/wayland/wayland-util.h +++ b/tools/vr/third_party/wayland/wayland-util.h @@ -23,10 +23,6 @@ * SOFTWARE. */ -/** \file wayland-util.h - * - * \brief Utility classes, functions, and macros. - */ #ifndef WAYLAND_UTIL_H #define WAYLAND_UTIL_H @@ -40,14 +36,12 @@ extern "C" { #endif -/** Visibility attribute */ #if defined(__GNUC__) && __GNUC__ >= 4 #define WL_EXPORT __attribute__ ((visibility("default"))) #else #define WL_EXPORT #endif -/** Deprecated attribute */ #if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) || (defined(__cplusplus) && __cplusplus >= 201402L) #define WL_DEPRECATED [[deprecated]] #elif defined(__GNUC__) && __GNUC__ >= 4 @@ -56,14 +50,6 @@ extern "C" { #define WL_DEPRECATED #endif -/** - * Printf-style argument attribute - * - * \param x Ordinality of the format string argument - * \param y Ordinality of the argument to check against the format string - * - * \sa https://gcc.gnu.org/onlinedocs/gcc-3.2.1/gcc/Function-Attributes.html - */ #if defined(__GNUC__) && __GNUC__ >= 4 #define WL_PRINTF(x, y) __attribute__((__format__(__printf__, x, y))) #else @@ -76,405 +62,57 @@ extern "C" { #define WL_TYPEOF(expr) __typeof__(expr) #endif -/** \class wl_object - * - * \brief A protocol object. - * - * A `wl_object` is an opaque struct identifying the protocol object - * underlying a `wl_proxy` or `wl_resource`. - * - * \note Functions accessing a `wl_object` are not normally used by client code. - * Clients should normally use the higher level interface generated by the - * scanner to interact with compositor objects. - * - */ struct wl_object; -/** - * The maximum size of a protocol message. - * - * If a message size exceeds this value, the connection will be dropped. - * Servers will send an invalid_method error before disconnecting. - */ #define WL_MAX_MESSAGE_SIZE 4096 -/** - * Protocol message signature - * - * A wl_message describes the signature of an actual protocol message, such as a - * request or event, that adheres to the Wayland protocol wire format. The - * protocol implementation uses a wl_message within its demarshal machinery for - * decoding messages between a compositor and its clients. In a sense, a - * wl_message is to a protocol message like a class is to an object. - * - * The `name` of a wl_message is the name of the corresponding protocol message. - * - * The `signature` is an ordered list of symbols representing the data types - * of message arguments and, optionally, a protocol version and indicators for - * nullability. A leading integer in the `signature` indicates the _since_ - * version of the protocol message. A `?` preceding a data type symbol indicates - * that the following argument type is nullable. While it is a protocol violation - * to send messages with non-nullable arguments set to `NULL`, event handlers in - * clients might still get called with non-nullable object arguments set to - * `NULL`. This can happen when the client destroyed the object being used as - * argument on its side and an event referencing that object was sent before the - * server knew about its destruction. As this race cannot be prevented, clients - * should - as a general rule - program their event handlers such that they can - * handle object arguments declared non-nullable being `NULL` gracefully. - * - * When no arguments accompany a message, `signature` is an empty string. - * - * Symbols: - * - * * `i`: int - * * `u`: uint - * * `f`: fixed - * * `s`: string - * * `o`: object - * * `n`: new_id - * * `a`: array - * * `h`: fd - * * `?`: following argument (`o` or `s`) is nullable - * - * While demarshaling primitive arguments is straightforward, when demarshaling - * messages containing `object` or `new_id` arguments, the protocol - * implementation often must determine the type of the object. The `types` of a - * wl_message is an array of wl_interface references that correspond to `o` and - * `n` arguments in `signature`, with `NULL` placeholders for arguments with - * non-object types. - * - * Consider the protocol event wl_display `delete_id` that has a single `uint` - * argument. The wl_message is: - * - * \code - * { "delete_id", "u", [NULL] } - * \endcode - * - * Here, the message `name` is `"delete_id"`, the `signature` is `"u"`, and the - * argument `types` is `[NULL]`, indicating that the `uint` argument has no - * corresponding wl_interface since it is a primitive argument. - * - * In contrast, consider a `wl_foo` interface supporting protocol request `bar` - * that has existed since version 2, and has two arguments: a `uint` and an - * object of type `wl_baz_interface` that may be `NULL`. Such a `wl_message` - * might be: - * - * \code - * { "bar", "2u?o", [NULL, &wl_baz_interface] } - * \endcode - * - * Here, the message `name` is `"bar"`, and the `signature` is `"2u?o"`. Notice - * how the `2` indicates the protocol version, the `u` indicates the first - * argument type is `uint`, and the `?o` indicates that the second argument - * is an object that may be `NULL`. Lastly, the argument `types` array indicates - * that no wl_interface corresponds to the first argument, while the type - * `wl_baz_interface` corresponds to the second argument. - * - * \sa wl_argument - * \sa wl_interface - * \sa <a href="https://wayland.freedesktop.org/docs/html/ch04.html#sect-Protocol-Wire-Format">Wire Format</a> - */ struct wl_message { - /** Message name */ const char *name; - /** Message signature */ const char *signature; - /** Object argument interfaces */ const struct wl_interface **types; }; -/** - * Protocol object interface - * - * A wl_interface describes the API of a protocol object defined in the Wayland - * protocol specification. The protocol implementation uses a wl_interface - * within its marshalling machinery for encoding client requests. - * - * The `name` of a wl_interface is the name of the corresponding protocol - * interface, and `version` represents the version of the interface. The members - * `method_count` and `event_count` represent the number of `methods` (requests) - * and `events` in the respective wl_message members. - * - * For example, consider a protocol interface `foo`, marked as version `1`, with - * two requests and one event. - * - * \code{.xml} - * <interface name="foo" version="1"> - * <request name="a"></request> - * <request name="b"></request> - * <event name="c"></event> - * </interface> - * \endcode - * - * Given two wl_message arrays `foo_requests` and `foo_events`, a wl_interface - * for `foo` might be: - * - * \code - * struct wl_interface foo_interface = { - * "foo", 1, - * 2, foo_requests, - * 1, foo_events - * }; - * \endcode - * - * \note The server side of the protocol may define interface <em>implementation - * types</em> that incorporate the term `interface` in their name. Take - * care to not confuse these server-side `struct`s with a wl_interface - * variable whose name also ends in `interface`. For example, while the - * server may define a type `struct wl_foo_interface`, the client may - * define a `struct wl_interface wl_foo_interface`. - * - * \sa wl_message - * \sa wl_proxy - * \sa <a href="https://wayland.freedesktop.org/docs/html/ch04.html#sect-Protocol-Interfaces">Interfaces</a> - * \sa <a href="https://wayland.freedesktop.org/docs/html/ch04.html#sect-Protocol-Versioning">Versioning</a> - */ struct wl_interface { - /** Interface name */ const char *name; - /** Interface version */ int version; - /** Number of methods (requests) */ int method_count; - /** Method (request) signatures */ const struct wl_message *methods; - /** Number of events */ int event_count; - /** Event signatures */ const struct wl_message *events; }; -/** \class wl_list - * - * \brief Doubly-linked list - * - * On its own, an instance of `struct wl_list` represents the sentinel head of - * a doubly-linked list, and must be initialized using wl_list_init(). - * When empty, the list head's `next` and `prev` members point to the list head - * itself, otherwise `next` references the first element in the list, and `prev` - * refers to the last element in the list. - * - * Use the `struct wl_list` type to represent both the list head and the links - * between elements within the list. Use wl_list_empty() to determine if the - * list is empty in O(1). - * - * All elements in the list must be of the same type. The element type must have - * a `struct wl_list` member, often named `link` by convention. Prior to - * insertion, there is no need to initialize an element's `link` - invoking - * wl_list_init() on an individual list element's `struct wl_list` member is - * unnecessary if the very next operation is wl_list_insert(). However, a - * common idiom is to initialize an element's `link` prior to removal - ensure - * safety by invoking wl_list_init() before wl_list_remove(). - * - * Consider a list reference `struct wl_list foo_list`, an element type as - * `struct element`, and an element's link member as `struct wl_list link`. - * - * The following code initializes a list and adds three elements to it. - * - * \code - * struct wl_list foo_list; - * - * struct element { - * int foo; - * struct wl_list link; - * }; - * struct element e1, e2, e3; - * - * wl_list_init(&foo_list); - * wl_list_insert(&foo_list, &e1.link); // e1 is the first element - * wl_list_insert(&foo_list, &e2.link); // e2 is now the first element - * wl_list_insert(&e2.link, &e3.link); // insert e3 after e2 - * \endcode - * - * The list now looks like <em>[e2, e3, e1]</em>. - * - * The `wl_list` API provides some iterator macros. For example, to iterate - * a list in ascending order: - * - * \code - * struct element *e; - * wl_list_for_each(e, foo_list, link) { - * do_something_with_element(e); - * } - * \endcode - * - * See the documentation of each iterator for details. - * \sa http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/include/linux/list.h - */ struct wl_list { - /** Previous list element */ struct wl_list *prev; - /** Next list element */ struct wl_list *next; }; -/** - * Initializes the list. - * - * \param list List to initialize - * - * \memberof wl_list - */ void wl_list_init(struct wl_list *list); -/** - * Inserts an element into the list, after the element represented by \p list. - * When \p list is a reference to the list itself (the head), set the containing - * struct of \p elm as the first element in the list. - * - * \note If \p elm is already part of a list, inserting it again will lead to - * list corruption. - * - * \param list List element after which the new element is inserted - * \param elm Link of the containing struct to insert into the list - * - * \memberof wl_list - */ void wl_list_insert(struct wl_list *list, struct wl_list *elm); -/** - * Removes an element from the list. - * - * \note This operation leaves \p elm in an invalid state. - * - * \param elm Link of the containing struct to remove from the list - * - * \memberof wl_list - */ void wl_list_remove(struct wl_list *elm); -/** - * Determines the length of the list. - * - * \note This is an O(n) operation. - * - * \param list List whose length is to be determined - * - * \return Number of elements in the list - * - * \memberof wl_list - */ int wl_list_length(const struct wl_list *list); -/** - * Determines if the list is empty. - * - * \param list List whose emptiness is to be determined - * - * \return 1 if empty, or 0 if not empty - * - * \memberof wl_list - */ int wl_list_empty(const struct wl_list *list); -/** - * Inserts all of the elements of one list into another, after the element - * represented by \p list. - * - * \note This leaves \p other in an invalid state. - * - * \param list List element after which the other list elements will be inserted - * \param other List of elements to insert - * - * \memberof wl_list - */ void wl_list_insert_list(struct wl_list *list, struct wl_list *other); -/** - * Retrieves a pointer to a containing struct, given a member name. - * - * This macro allows "conversion" from a pointer to a member to its containing - * struct. This is useful if you have a contained item like a wl_list, - * wl_listener, or wl_signal, provided via a callback or other means, and would - * like to retrieve the struct that contains it. - * - * To demonstrate, the following example retrieves a pointer to - * `example_container` given only its `destroy_listener` member: - * - * \code - * struct example_container { - * struct wl_listener destroy_listener; - * // other members... - * }; - * - * void example_container_destroy(struct wl_listener *listener, void *data) - * { - * struct example_container *ctr; - * - * ctr = wl_container_of(listener, ctr, destroy_listener); - * // destroy ctr... - * } - * \endcode - * - * \note `sample` need not be a valid pointer. A null or uninitialised pointer - * is sufficient. - * - * \param ptr Valid pointer to the contained member - * \param sample Pointer to a struct whose type contains \p ptr - * \param member Named location of \p ptr within the \p sample type - * - * \return The container for the specified pointer - */ #define wl_container_of(ptr, sample, member) \ (WL_TYPEOF(sample))((char *)(ptr) - \ offsetof(WL_TYPEOF(*sample), member)) -/** - * Iterates over a list. - * - * This macro expresses a for-each iterator for wl_list. Given a list and - * wl_list link member name (often named `link` by convention), this macro - * assigns each element in the list to \p pos, which can then be referenced in - * a trailing code block. For example, given a wl_list of `struct message` - * elements: - * - * \code - * struct message { - * char *contents; - * wl_list link; - * }; - * - * struct wl_list *message_list; - * // Assume message_list now "contains" many messages - * - * struct message *m; - * wl_list_for_each(m, message_list, link) { - * do_something_with_message(m); - * } - * \endcode - * - * \param pos Cursor that each list element will be assigned to - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each(pos, head, member) \ for (pos = wl_container_of((head)->next, pos, member); \ &pos->member != (head); \ pos = wl_container_of(pos->member.next, pos, member)) -/** - * Iterates over a list, safe against removal of the list element. - * - * \note Only removal of the current element, \p pos, is safe. Removing - * any other element during traversal may lead to a loop malfunction. - * - * \sa wl_list_for_each() - * - * \param pos Cursor that each list element will be assigned to - * \param tmp Temporary pointer of the same type as \p pos - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each_safe(pos, tmp, head, member) \ for (pos = wl_container_of((head)->next, pos, member), \ tmp = wl_container_of((pos)->member.next, tmp, member); \ @@ -482,37 +120,11 @@ wl_list_insert_list(struct wl_list *list, struct wl_list *other); pos = tmp, \ tmp = wl_container_of(pos->member.next, tmp, member)) -/** - * Iterates backwards over a list. - * - * \sa wl_list_for_each() - * - * \param pos Cursor that each list element will be assigned to - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each_reverse(pos, head, member) \ for (pos = wl_container_of((head)->prev, pos, member); \ &pos->member != (head); \ pos = wl_container_of(pos->member.prev, pos, member)) -/** - * Iterates backwards over a list, safe against removal of the list element. - * - * \note Only removal of the current element, \p pos, is safe. Removing - * any other element during traversal may lead to a loop malfunction. - * - * \sa wl_list_for_each() - * - * \param pos Cursor that each list element will be assigned to - * \param tmp Temporary pointer of the same type as \p pos - * \param head Head of the list to iterate over - * \param member Name of the link member within the element struct - * - * \relates wl_list - */ #define wl_list_for_each_reverse_safe(pos, tmp, head, member) \ for (pos = wl_container_of((head)->prev, pos, member), \ tmp = wl_container_of((pos)->member.prev, tmp, member); \ @@ -520,244 +132,75 @@ wl_list_insert_list(struct wl_list *list, struct wl_list *other); pos = tmp, \ tmp = wl_container_of(pos->member.prev, tmp, member)) -/** - * \class wl_array - * - * Dynamic array - * - * A wl_array is a dynamic array that can only grow until released. It is - * intended for relatively small allocations whose size is variable or not known - * in advance. While construction of a wl_array does not require all elements to - * be of the same size, wl_array_for_each() does require all elements to have - * the same type and size. - * - */ struct wl_array { - /** Array size */ size_t size; - /** Allocated space */ size_t alloc; - /** Array data */ void *data; }; -/** - * Initializes the array. - * - * \param array Array to initialize - * - * \memberof wl_array - */ void wl_array_init(struct wl_array *array); -/** - * Releases the array data. - * - * \note Leaves the array in an invalid state. - * - * \param array Array whose data is to be released - * - * \memberof wl_array - */ void wl_array_release(struct wl_array *array); -/** - * Increases the size of the array by \p size bytes. - * - * \param array Array whose size is to be increased - * \param size Number of bytes to increase the size of the array by - * - * \return A pointer to the beginning of the newly appended space, or NULL when - * resizing fails. - * - * \memberof wl_array - */ void * wl_array_add(struct wl_array *array, size_t size); -/** - * Copies the contents of \p source to \p array. - * - * \param array Destination array to copy to - * \param source Source array to copy from - * - * \return 0 on success, or -1 on failure - * - * \memberof wl_array - */ int wl_array_copy(struct wl_array *array, struct wl_array *source); -/** - * Iterates over an array. - * - * This macro expresses a for-each iterator for wl_array. It assigns each - * element in the array to \p pos, which can then be referenced in a trailing - * code block. \p pos must be a pointer to the array element type, and all - * array elements must be of the same type and size. - * - * \param pos Cursor that each array element will be assigned to - * \param array Array to iterate over - * - * \relates wl_array - * \sa wl_list_for_each() - */ #define wl_array_for_each(pos, array) \ for (pos = (array)->data; \ (array)->size != 0 && \ (const char *) pos < ((const char *) (array)->data + (array)->size); \ (pos)++) -/** - * Fixed-point number - * - * A `wl_fixed_t` is a 24.8 signed fixed-point number with a sign bit, 23 bits - * of integer precision and 8 bits of decimal precision. Consider `wl_fixed_t` - * as an opaque struct with methods that facilitate conversion to and from - * `double` and `int` types. - */ typedef int32_t wl_fixed_t; -/** - * Converts a fixed-point number to a floating-point number. - * - * \param f Fixed-point number to convert - * - * \return Floating-point representation of the fixed-point argument - */ static inline double wl_fixed_to_double(wl_fixed_t f) { return f / 256.0; } -/** - * Converts a floating-point number to a fixed-point number. - * - * \param d Floating-point number to convert - * - * \return Fixed-point representation of the floating-point argument - */ static inline wl_fixed_t wl_fixed_from_double(double d) { return (wl_fixed_t) (round(d * 256.0)); } -/** - * Converts a fixed-point number to an integer. - * - * \param f Fixed-point number to convert - * - * \return Integer component of the fixed-point argument - */ static inline int wl_fixed_to_int(wl_fixed_t f) { return f / 256; } -/** - * Converts an integer to a fixed-point number. - * - * \param i Integer to convert - * - * \return Fixed-point representation of the integer argument - */ static inline wl_fixed_t wl_fixed_from_int(int i) { return i * 256; } -/** - * Protocol message argument data types - * - * This union represents all of the argument types in the Wayland protocol wire - * format. The protocol implementation uses wl_argument within its marshalling - * machinery for dispatching messages between a client and a compositor. - * - * \sa wl_message - * \sa wl_interface - * \sa <a href="https://wayland.freedesktop.org/docs/html/ch04.html#sect-Protocol-wire-Format">Wire Format</a> - */ union wl_argument { - int32_t i; /**< `int` */ - uint32_t u; /**< `uint` */ - wl_fixed_t f; /**< `fixed` */ - const char *s; /**< `string` */ - struct wl_object *o; /**< `object` */ - uint32_t n; /**< `new_id` */ - struct wl_array *a; /**< `array` */ - int32_t h; /**< `fd` */ + int32_t i; + uint32_t u; + wl_fixed_t f; + const char *s; + struct wl_object *o; + uint32_t n; + struct wl_array *a; + int32_t h; }; -/** - * Dispatcher function type alias - * - * A dispatcher is a function that handles the emitting of callbacks in client - * code. For programs directly using the C library, this is done by using - * libffi to call function pointers. When binding to languages other than C, - * dispatchers provide a way to abstract the function calling process to be - * friendlier to other function calling systems. - * - * A dispatcher takes five arguments: The first is the dispatcher-specific - * implementation associated with the target object. The second is the object - * upon which the callback is being invoked (either wl_proxy or wl_resource). - * The third and fourth arguments are the opcode and the wl_message - * corresponding to the callback. The final argument is an array of arguments - * received from the other process via the wire protocol. - * - * \param user_data Dispatcher-specific implementation data - * \param target Callback invocation target (wl_proxy or `wl_resource`) - * \param opcode Callback opcode - * \param msg Callback message signature - * \param args Array of received arguments - * - * \return 0 on success, or -1 on failure - */ typedef int (*wl_dispatcher_func_t)(const void *user_data, void *target, uint32_t opcode, const struct wl_message *msg, union wl_argument *args); -/** - * Log function type alias - * - * The C implementation of the Wayland protocol abstracts the details of - * logging. Users may customize the logging behavior, with a function conforming - * to the `wl_log_func_t` type, via `wl_log_set_handler_client` and - * `wl_log_set_handler_server`. - * - * A `wl_log_func_t` must conform to the expectations of `vprintf`, and - * expects two arguments: a string to write and a corresponding variable - * argument list. While the string to write may contain format specifiers and - * use values in the variable argument list, the behavior of any `wl_log_func_t` - * depends on the implementation. - * - * \note Take care to not confuse this with `wl_protocol_logger_func_t`, which - * is a specific server-side logger for requests and events. - * - * \param fmt String to write to the log, containing optional format - * specifiers - * \param args Variable argument list - * - * \sa wl_log_set_handler_client - * \sa wl_log_set_handler_server - */ typedef void (*wl_log_func_t)(const char *fmt, va_list args) WL_PRINTF(1, 0); -/** - * Return value of an iterator function - * - * \sa wl_client_for_each_resource_iterator_func_t - * \sa wl_client_for_each_resource - */ enum wl_iterator_result { - /** Stop the iteration */ WL_ITERATOR_STOP, - /** Continue the iteration */ WL_ITERATOR_CONTINUE };